archsight 0.3.3 → 0.4.1
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/README.md +7 -1
- data/docs/archimate.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/diagram.md +15 -3
- data/docs/docker.md +4 -1
- data/docs/icons.md +8 -2
- data/docs/import.md +178 -55
- data/docs/index.md.erb +24 -18
- data/docs/modeling.md +645 -34
- data/docs/pages.md +46 -15
- data/docs/search.md +21 -1
- data/docs/togaf.md +2 -1
- data/lib/archsight/analysis/result.rb +8 -2
- data/lib/archsight/analysis/sandbox.rb +8 -0
- data/lib/archsight/analysis/typst_renderer.rb +245 -0
- data/lib/archsight/analysis.rb +1 -0
- data/lib/archsight/annotations/annotation.rb +30 -3
- data/lib/archsight/annotations/architecture_annotations.rb +47 -0
- data/lib/archsight/annotations/link_annotations.rb +15 -0
- data/lib/archsight/annotations/resource_reference.rb +12 -0
- data/lib/archsight/cli.rb +124 -23
- data/lib/archsight/database.rb +40 -0
- data/lib/archsight/diagram/cli.rb +50 -2
- data/lib/archsight/diagram/formatter.rb +112 -0
- data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
- data/lib/archsight/diagram/routing/edge_router.rb +29 -4
- data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
- data/lib/archsight/diagram/style/theme.rb +7 -2
- data/lib/archsight/documentation.rb +4 -4
- data/lib/archsight/editor/page_source.rb +13 -1
- data/lib/archsight/export/confluence/analysis_output.rb +100 -0
- data/lib/archsight/export/confluence/client.rb +13 -0
- data/lib/archsight/export/confluence/diagram_links.rb +1 -1
- data/lib/archsight/export/confluence/exporter.rb +43 -14
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +6 -9
- data/lib/archsight/export/confluence/storage.rb +63 -14
- data/lib/archsight/export/confluence/tables.rb +2 -3
- data/lib/archsight/git_source.rb +81 -0
- data/lib/archsight/helpers/asset_images.rb +3 -4
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +140 -16
- data/lib/archsight/import/handler.rb +55 -6
- data/lib/archsight/import/handler_loader.rb +64 -0
- data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
- data/lib/archsight/import/handlers/command.rb +120 -0
- data/lib/archsight/import/handlers/github.rb +4 -1
- data/lib/archsight/import/handlers/gitlab.rb +4 -1
- data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
- data/lib/archsight/import/handlers/go_grapher.rb +102 -5
- data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
- data/lib/archsight/import/handlers/grapher.rb +40 -1
- data/lib/archsight/import/handlers/repository.rb +3 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
- data/lib/archsight/import/registry.rb +22 -1
- data/lib/archsight/import.rb +89 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +123 -16
- data/lib/archsight/markdown.rb +48 -0
- data/lib/archsight/openapi.rb +169 -0
- data/lib/archsight/page_loader.rb +12 -1
- data/lib/archsight/page_tree.rb +2 -3
- data/lib/archsight/query/evaluator.rb +13 -6
- data/lib/archsight/query/parser.rb +73 -6
- data/lib/archsight/query.rb +1 -1
- data/lib/archsight/references.rb +3 -3
- data/lib/archsight/renderer.rb +80 -23
- data/lib/archsight/resources/analysis.rb +3 -2
- data/lib/archsight/resources/application_component.rb +75 -7
- data/lib/archsight/resources/application_interface.rb +12 -0
- data/lib/archsight/resources/application_service.rb +12 -1
- data/lib/archsight/resources/base.rb +58 -5
- data/lib/archsight/resources/business_actor.rb +5 -0
- data/lib/archsight/resources/business_contract.rb +72 -0
- data/lib/archsight/resources/business_product.rb +12 -0
- data/lib/archsight/resources/business_role.rb +1 -0
- data/lib/archsight/resources/implementation_deliverable.rb +1 -0
- data/lib/archsight/resources/implementation_plateau.rb +8 -2
- data/lib/archsight/resources/implementation_work_package.rb +2 -1
- data/lib/archsight/resources/import.rb +37 -11
- data/lib/archsight/resources/location.rb +56 -0
- data/lib/archsight/resources/motivation_requirement.rb +6 -0
- data/lib/archsight/resources/motivation_stakeholder.rb +14 -0
- data/lib/archsight/resources/page.rb +4 -7
- data/lib/archsight/resources/strategy_capability.rb +9 -0
- data/lib/archsight/resources/technology_artifact.rb +11 -4
- data/lib/archsight/resources/technology_communication_network.rb +91 -0
- data/lib/archsight/resources/technology_device.rb +88 -0
- data/lib/archsight/resources/technology_equipment.rb +75 -0
- data/lib/archsight/resources/technology_facility.rb +70 -0
- data/lib/archsight/resources/technology_node.rb +50 -0
- data/lib/archsight/resources/technology_service.rb +33 -0
- data/lib/archsight/resources/technology_system_software.rb +28 -0
- data/lib/archsight/resources.rb +12 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/analysis_helpers.rb +55 -0
- data/lib/archsight/web/api/docs.rb +2 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +146 -35
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +9 -4
- data/lib/archsight/web/application.rb +40 -4
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
- data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
- data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
- data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
- data/lib/archsight/web/public/vue/DocPage-C2ubTXH7.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-Dso6PLMn.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-DnZ6YJLi.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-CTl03Yre.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C339nihQ.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/KindList-C0oaM3CF.js +1 -0
- data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
- data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
- data/lib/archsight/web/public/vue/PageView-OZB6G9Wu.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-C2xLkCEm.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/ResourceList-DooWFwQP.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-BK-HjFnv.js +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-BewvsfOc.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/WikiPage-B2FYmeHL.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
- data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
- data/lib/archsight/web/public/vue/index-BLSE5DSv.css +1 -0
- data/lib/archsight/web/public/vue/index-CLuLTE5I.js +3 -0
- data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
- data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
- data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
- data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
- data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
- data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
- data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
- data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
- data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DlSnFeCL.js → useGraphviz-Dnm2pkon.js} +2 -2
- data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
- data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
- data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +68 -41
- data/lib/archsight/web/public/vue/ApiDocsPage-BgqnQgwa.js +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
- data/lib/archsight/web/public/vue/DocPage-CNO71nBH.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-DR0FCNTz.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-BfYArv6s.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-BVTW30Ak.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-Wk9P4Pma.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-BV8ycydz.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
- data/lib/archsight/web/public/vue/KindList-C8yMsN5J.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BaN6TyJB.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-D790wH-a.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
- data/lib/archsight/web/public/vue/ResourceList-D-66nas2.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CHyqIerQ.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-D2svpk6B.js +0 -13
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
- data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
- data/lib/archsight/web/public/vue/index-BXXkUK1V.js +0 -3
- data/lib/archsight/web/public/vue/index-Cov1SnzY.css +0 -1
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
- data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
- data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +0 -3334
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
- data/lib/archsight/web/public/vue/useHighlight-D5Gf8DLl.js +0 -10
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require "uri"
|
|
5
5
|
require "digest"
|
|
6
|
-
|
|
7
|
-
require "kramdown-parser-gfm"
|
|
6
|
+
require_relative "../../markdown"
|
|
8
7
|
require_relative "../../assets"
|
|
9
8
|
require_relative "../../diagram"
|
|
10
9
|
require_relative "../../helpers/macros"
|
|
@@ -17,6 +16,7 @@ require_relative "drawio"
|
|
|
17
16
|
require_relative "page_url"
|
|
18
17
|
require_relative "rasterizer"
|
|
19
18
|
require_relative "tables"
|
|
19
|
+
require_relative "analysis_output"
|
|
20
20
|
|
|
21
21
|
module Archsight
|
|
22
22
|
module Export
|
|
@@ -79,8 +79,9 @@ module Archsight
|
|
|
79
79
|
|
|
80
80
|
MXFILE = "application/vnd.jgraph.mxfile"
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
HOMEPAGE = "https://github.com/ionos-cloud/archsight"
|
|
83
|
+
BANNER = "This page is generated by <a href=\"#{HOMEPAGE}\">Archsight</a> from %<source>s. " \
|
|
84
|
+
"Changes made here are overwritten by the next export: edit the source instead.".freeze
|
|
84
85
|
|
|
85
86
|
LANGUAGES = {
|
|
86
87
|
"sh" => "bash", "shell" => "bash", "zsh" => "bash", "yml" => "yaml", "json" => "javascript", "js" => "javascript",
|
|
@@ -94,6 +95,8 @@ module Archsight
|
|
|
94
95
|
|
|
95
96
|
# @param page_name [String] used to name generated attachments
|
|
96
97
|
# @param source [String] the markdown file, shown in the banner
|
|
98
|
+
# @param git [Archsight::GitSource::Location, nil] where the file is in its git repository: the banner then
|
|
99
|
+
# links it to the file in the web UI of the git host and names the repository
|
|
97
100
|
# @param base_dir [String] directory of the markdown file relative to the resources directory
|
|
98
101
|
# @param resources_dir [String]
|
|
99
102
|
# @param wiki [Archsight::Helpers::WikiLinks] resolves [[Targets]]
|
|
@@ -102,8 +105,10 @@ module Archsight
|
|
|
102
105
|
# @param database [Archsight::Database, nil] lets macros that name pages (pagetree) find them
|
|
103
106
|
# @param diagram_links [#call, nil] resolves a diagram's `resource` references (DiagramLinks); none leaves them inert
|
|
104
107
|
# @param drawio [Boolean] the Confluence has the draw.io app: diagrams go in as draw.io macros, else as images
|
|
105
|
-
def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil
|
|
108
|
+
def initialize(page_name:, source:, base_dir:, resources_dir:, wiki:, page_id: nil, base: nil, drawio: false, diagram_links: nil, database: nil,
|
|
109
|
+
git: nil)
|
|
106
110
|
@database = database
|
|
111
|
+
@git = git
|
|
107
112
|
@page_name = page_name
|
|
108
113
|
@source = source
|
|
109
114
|
@base_dir = base_dir
|
|
@@ -124,7 +129,7 @@ module Archsight
|
|
|
124
129
|
# @param header [String] storage format of the page properties, shown top left next to the banner
|
|
125
130
|
# @return [Converted]
|
|
126
131
|
def convert(markdown, toc: false, header: "")
|
|
127
|
-
document =
|
|
132
|
+
document = Archsight::Markdown.unwrapped(protect(markdown), auto_ids: false, entity_output: :as_char, smart_quotes: %w[apos apos quot quot])
|
|
128
133
|
html, = Converter.convert(document.root, document.options.merge(storage: self))
|
|
129
134
|
Converted.new(body: layout(header, banner, (toc ? %(<ac:structured-macro ac:name="toc" />\n) : "") + html), attachments: @attachments, problems: @problems)
|
|
130
135
|
end
|
|
@@ -225,13 +230,45 @@ module Archsight
|
|
|
225
230
|
end
|
|
226
231
|
|
|
227
232
|
def banner
|
|
228
|
-
%(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source:
|
|
233
|
+
%(<ac:structured-macro ac:name="info"><ac:rich-text-body><p>#{format(BANNER, source: banner_source)}</p></ac:rich-text-body></ac:structured-macro>\n)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# `<code>path</code>`, in a git repository the path from its root, linked to the file on the git host, and
|
|
237
|
+
# the repository
|
|
238
|
+
def banner_source
|
|
239
|
+
return "<code>#{h(@source)}</code>" unless @git
|
|
240
|
+
|
|
241
|
+
file = "<code>#{h(@git.path)}</code>"
|
|
242
|
+
file = %(<a href="#{h(@git.url)}">#{file}</a>) if @git.url
|
|
243
|
+
%(#{file} in the repository <a href="#{h(@git.repository_url)}">#{h(@git.repository)}</a>)
|
|
229
244
|
end
|
|
230
245
|
|
|
231
246
|
# {macros}, [[Target|label]] and ![[View/Name]] become placeholders before kramdown sees them (it would split the
|
|
232
|
-
# brackets), except in code.
|
|
247
|
+
# brackets), except in code; embedded descriptions are written in first.
|
|
233
248
|
def protect(markdown)
|
|
234
|
-
markdown
|
|
249
|
+
outside_code(embed_descriptions(markdown)) { |text| protect_text(text) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def outside_code(markdown, &)
|
|
253
|
+
markdown.split(CODE).each_with_index.map { |part, i| i.odd? ? part : yield(part) }.join
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The description of a resource embedded on a line of its own (`![[MotivationGoal/Name]]`) is written into the
|
|
257
|
+
# page: it is text, not live content. Embeds in that description stay notes (they are not expanded again).
|
|
258
|
+
def embed_descriptions(markdown)
|
|
259
|
+
outside_code(markdown) do |text|
|
|
260
|
+
text.gsub(/^[ \t]*!\[\[([^\]|]+)\]\][ \t]*$/) { embedded_description(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def embedded_description(reference)
|
|
265
|
+
kind, name = reference.split("/", 2)
|
|
266
|
+
return if name.nil? || Archsight::Helpers::Embeds::LIVE_KINDS.include?(kind) ||
|
|
267
|
+
!Archsight::Helpers::Embeds.embeddable_kind?(kind)
|
|
268
|
+
|
|
269
|
+
instance = @database&.instances_by_kind(kind)&.[](name)
|
|
270
|
+
description = instance&.annotations.to_h["architecture/description"].to_s.strip
|
|
271
|
+
description.empty? ? nil : description
|
|
235
272
|
end
|
|
236
273
|
|
|
237
274
|
def protect_text(text)
|
|
@@ -240,6 +277,7 @@ module Archsight
|
|
|
240
277
|
xml && placeholder("LINK", xml, plain: (macro.plain(value) if macro.respond_to?(:plain)))
|
|
241
278
|
end
|
|
242
279
|
text = text.gsub(%r{^[ \t]*!\[\[View/([^\]|]+)\]\][ \t]*$}) { view_embed(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
280
|
+
text = text.gsub(%r{^[ \t]*!\[\[Analysis/([^\]|]+)\]\][ \t]*$}) { analysis_embed(Regexp.last_match(1).strip) || Regexp.last_match(0) }
|
|
243
281
|
text = text.gsub(/!\[\[([^\]|]+)\]\]/) { placeholder("EMBED", embed_note(Regexp.last_match(1).strip)) }
|
|
244
282
|
text.gsub(Archsight::Helpers::WikiLinks::PATTERN) do
|
|
245
283
|
placeholder("LINK", wiki_link(Regexp.last_match(1).strip, Regexp.last_match(2)&.strip))
|
|
@@ -256,6 +294,18 @@ module Archsight
|
|
|
256
294
|
placeholder("TABLE", xml, plain: embed_note("View/#{name}"))
|
|
257
295
|
end
|
|
258
296
|
|
|
297
|
+
# An Analysis on a line of its own is run and its result written into the page (the data of the moment, like a
|
|
298
|
+
# view); an unknown Analysis stays an embed (a note), the linter reports it.
|
|
299
|
+
def analysis_embed(name)
|
|
300
|
+
analysis = @database&.instances_by_kind("Analysis")&.[](name)
|
|
301
|
+
return nil unless analysis
|
|
302
|
+
|
|
303
|
+
require_relative "../../analysis"
|
|
304
|
+
result = Archsight::Analysis::Executor.new(@database).execute(analysis)
|
|
305
|
+
output = AnalysisOutput.new(link: ->(target, label) { wiki_link(target, label) }, code: ->(source, language) { code(source, language) })
|
|
306
|
+
placeholder("TABLE", output.xml(name, result), plain: embed_note("Analysis/#{name}"))
|
|
307
|
+
end
|
|
308
|
+
|
|
259
309
|
# The XML of a table, or nothing and a problem (the page is not exported) if it cannot be built
|
|
260
310
|
def table_xml(what)
|
|
261
311
|
yield
|
|
@@ -358,13 +408,12 @@ module Archsight
|
|
|
358
408
|
%(<p><ac:image><ri:attachment ri:filename="#{h(filename)}" /></ac:image></p>\n)
|
|
359
409
|
end
|
|
360
410
|
|
|
361
|
-
# The diagram file of a draw.io macro and its `.png` preview, which must share the name
|
|
411
|
+
# The diagram file of a draw.io macro and its `.png` preview, which must share the name. The name carries a hash
|
|
412
|
+
# of the content: the macro shows revision 1 of the attachment it names, so a changed diagram uploaded under the
|
|
413
|
+
# old name (as a new version of it) would not be shown. A new name is a new attachment, the old one is removed.
|
|
362
414
|
# @return [String] the name the macro refers to
|
|
363
415
|
def attach_diagram(stem, xml, png)
|
|
364
|
-
|
|
365
|
-
name = stem
|
|
366
|
-
taken = ->(file, data) { @attachments.key?(file) && @attachments[file].data != data }
|
|
367
|
-
name = "#{stem}-#{sha[0, 8]}" if taken.call(stem, xml) || taken.call("#{stem}.png", png)
|
|
416
|
+
name = "#{stem}-#{Digest::SHA256.hexdigest(xml)[0, 8]}"
|
|
368
417
|
@attachments[name] = Attachment.new(filename: name, data: xml, content_type: MXFILE)
|
|
369
418
|
@attachments["#{name}.png"] = Attachment.new(filename: "#{name}.png", data: png, content_type: "image/png")
|
|
370
419
|
name
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
-
|
|
5
|
-
require "kramdown-parser-gfm"
|
|
4
|
+
require_relative "../../markdown"
|
|
6
5
|
require_relative "../../view_table"
|
|
7
6
|
require_relative "../../helpers/macros"
|
|
8
7
|
require_relative "diagram_links"
|
|
@@ -67,7 +66,7 @@ module Archsight
|
|
|
67
66
|
return "" if text.to_s.strip.empty?
|
|
68
67
|
|
|
69
68
|
safe = text.gsub(%r{<(?!(?:https?://|mailto:))}, "<")
|
|
70
|
-
html =
|
|
69
|
+
html = Archsight::Markdown.unwrapped(safe, entity_output: :as_char, auto_ids: false).to_html.strip
|
|
71
70
|
html.match(%r{\A<p>((?:(?!</?p>).)*)</p>\z}m) ? Regexp.last_match(1) : html
|
|
72
71
|
end
|
|
73
72
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Archsight
|
|
9
|
+
# Where a file lives in its git repository and its page in the web UI of the git host, for links to the source of
|
|
10
|
+
# generated content (the banner of an exported Confluence page). Only local git is asked (the repository of the
|
|
11
|
+
# file, its `origin` remote, the checked out branch), nothing goes over the network.
|
|
12
|
+
#
|
|
13
|
+
# The web URL is `https://<host>/<owner>/<repo>` of the remote, without the user, password or token a remote URL
|
|
14
|
+
# may carry. GitHub files are `/blob/<ref>/<path>`, GitLab files `/-/blob/<ref>/<path>`; on other hosts, and for a
|
|
15
|
+
# file git does not track, only the repository is linked. The ref is the checked out branch, the commit when the
|
|
16
|
+
# HEAD is detached (CI).
|
|
17
|
+
module GitSource
|
|
18
|
+
# repository: "owner/repo"; repository_url: its web page; path: the file relative to the repository root;
|
|
19
|
+
# url: the web page of the file, nil if the host is not known
|
|
20
|
+
Location = Struct.new(:repository, :repository_url, :path, :url, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param file [String] path of a file
|
|
25
|
+
# @return [Location, nil] nil if the file is not in a git repository with an `origin` remote
|
|
26
|
+
def locate(file)
|
|
27
|
+
dir = File.dirname(File.expand_path(file))
|
|
28
|
+
root = git(dir, "rev-parse", "--show-toplevel") or return nil
|
|
29
|
+
remote = git(dir, "remote", "get-url", "origin") or return nil
|
|
30
|
+
host, repository = host_and_repository(remote)
|
|
31
|
+
return nil unless host && repository
|
|
32
|
+
|
|
33
|
+
path = Pathname.new(File.realpath(file)).relative_path_from(Pathname.new(File.realpath(root))).to_s
|
|
34
|
+
ref = git(dir, "symbolic-ref", "--short", "-q", "HEAD") || git(dir, "rev-parse", "HEAD")
|
|
35
|
+
repository_url = "https://#{host}/#{repository}"
|
|
36
|
+
url = tracked?(root, path) ? file_url(host, repository_url, ref, path) : nil
|
|
37
|
+
Location.new(repository: repository, repository_url: repository_url, path: path, url: url)
|
|
38
|
+
rescue SystemCallError, ArgumentError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo.git`, `https://user:token@host/owner/repo` ->
|
|
43
|
+
# ["host", "owner/repo"]; the port of an ssh remote is not the port of the web UI and is left out
|
|
44
|
+
def host_and_repository(remote)
|
|
45
|
+
if (scp = remote.match(%r{\A(?:[^@/\s]+@)?([^:/\s]+):(?!//)(.+)\z}))
|
|
46
|
+
host, path = scp.captures
|
|
47
|
+
else
|
|
48
|
+
uri = URI.parse(remote)
|
|
49
|
+
return [nil, nil] unless uri.host && %w[http https ssh git].include?(uri.scheme)
|
|
50
|
+
|
|
51
|
+
host = uri.scheme.start_with?("http") && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
|
|
52
|
+
path = uri.path
|
|
53
|
+
end
|
|
54
|
+
repository = path.delete_prefix("/").delete_suffix("/").delete_suffix(".git")
|
|
55
|
+
repository.empty? ? [nil, nil] : [host, repository]
|
|
56
|
+
rescue URI::InvalidURIError
|
|
57
|
+
[nil, nil]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def file_url(host, repository_url, ref, path)
|
|
61
|
+
blob = if host.match?(/(\A|\.)github\./) then "blob"
|
|
62
|
+
elsif host.match?(/(\A|\.)gitlab\./) then "-/blob"
|
|
63
|
+
end
|
|
64
|
+
return nil unless blob && ref
|
|
65
|
+
|
|
66
|
+
"#{repository_url}/#{blob}/#{escape(ref)}/#{escape(path)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# An untracked or ignored file is not on the git host: no link that leads nowhere
|
|
70
|
+
def tracked?(root, path) = !git(root, "ls-files", "--", path).nil?
|
|
71
|
+
|
|
72
|
+
# each segment escaped, the slashes kept
|
|
73
|
+
def escape(path) = path.split("/").map { |segment| ERB::Util.url_encode(segment) }.join("/")
|
|
74
|
+
|
|
75
|
+
def git(dir, *)
|
|
76
|
+
out, _err, status = Open3.capture3("git", "-C", dir, *)
|
|
77
|
+
value = out.strip
|
|
78
|
+
status.success? && !value.empty? ? value : nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
4
|
require "erb"
|
|
5
|
-
|
|
6
|
-
require "kramdown-parser-gfm"
|
|
5
|
+
require_relative "../markdown"
|
|
7
6
|
require_relative "../assets"
|
|
8
7
|
|
|
9
8
|
module Archsight
|
|
@@ -52,7 +51,7 @@ module Archsight
|
|
|
52
51
|
# Problems with the image references of a markdown text.
|
|
53
52
|
# @return [Array<Hash>] `{ reference:, status: :outside|:type|:missing, path: }`
|
|
54
53
|
def audit(markdown, base_dir:, resources_dir:)
|
|
55
|
-
html =
|
|
54
|
+
html = Archsight::Markdown.document(markdown).to_html
|
|
56
55
|
html.scan(IMG).filter_map do |tag|
|
|
57
56
|
reference = attribute(tag, SRC)
|
|
58
57
|
next if reference.nil? || Assets.external?(reference)
|
|
@@ -65,7 +64,7 @@ module Archsight
|
|
|
65
64
|
# The existing .asd files a markdown text embeds (for the linter)
|
|
66
65
|
# @return [Array<Array(String, String)>] normalized asset path and real file
|
|
67
66
|
def asd_files(markdown, base_dir:, resources_dir:)
|
|
68
|
-
html =
|
|
67
|
+
html = Archsight::Markdown.document(markdown).to_html
|
|
69
68
|
html.scan(IMG).filter_map do |tag|
|
|
70
69
|
reference = attribute(tag, SRC)
|
|
71
70
|
next if reference.nil? || Assets.external?(reference)
|
|
@@ -4,26 +4,37 @@ require "erb"
|
|
|
4
4
|
|
|
5
5
|
module Archsight
|
|
6
6
|
module Helpers
|
|
7
|
-
# Replaces `![[
|
|
8
|
-
#
|
|
9
|
-
# loads and runs
|
|
7
|
+
# Replaces `![[Kind/Name]]` in rendered HTML by a placeholder for the content of that resource: the result list of
|
|
8
|
+
# a View, the result of an Analysis, the description of any other resource (a goal, a capability, ...). The
|
|
9
|
+
# frontend turns each placeholder into that content (it loads and runs it itself, so rendering a page never
|
|
10
|
+
# executes a query or a script):
|
|
10
11
|
#
|
|
11
12
|
# <div class="kind-embed" data-kind="View" data-name="View:Services"><a href="/kinds/View/instances/...">...</a></div>
|
|
12
13
|
#
|
|
13
14
|
# The link inside is what API and MCP consumers (and a browser without script) get. A reference that does not
|
|
14
|
-
# name an existing
|
|
15
|
+
# name an existing resource (or names a page) renders as a `broken-link` marker. Code (`<pre>`, `<code>`) is
|
|
16
|
+
# left alone.
|
|
15
17
|
class Embeds
|
|
16
|
-
|
|
18
|
+
# Embeds with live content; every other kind shows its description
|
|
19
|
+
LIVE_KINDS = %w[View Analysis].freeze
|
|
20
|
+
# Pages and menus are not resources with a description
|
|
21
|
+
NOT_EMBEDDABLE = %w[Page PageMenu].freeze
|
|
17
22
|
PATTERN = /!\[\[([^\]|]+)\]\]/
|
|
18
23
|
PARAGRAPH = %r{<p>\s*(#{PATTERN.source})\s*</p>}
|
|
19
24
|
CODE = %r{(<pre\b.*?</pre>|<code\b.*?</code>)}m
|
|
20
25
|
|
|
21
26
|
REASONS = {
|
|
22
|
-
kind: "
|
|
27
|
+
kind: "embed a resource as Kind/Name (a View, an Analysis or the description of any other resource); " \
|
|
28
|
+
"pages cannot be embedded",
|
|
23
29
|
missing: "no such resource",
|
|
24
30
|
ambiguous: "ambiguous reference"
|
|
25
31
|
}.freeze
|
|
26
32
|
|
|
33
|
+
# Whether `![[kind/...]]` can be embedded: a known kind that is not a page
|
|
34
|
+
def self.embeddable_kind?(kind)
|
|
35
|
+
!Archsight::Resources[kind].nil? && !NOT_EMBEDDABLE.include?(Archsight::Resources.canonical(kind))
|
|
36
|
+
end
|
|
37
|
+
|
|
27
38
|
def initialize(database, resolver: ResourceResolver.new(database))
|
|
28
39
|
@database = database
|
|
29
40
|
@resolver = resolver
|
|
@@ -66,7 +77,7 @@ module Archsight
|
|
|
66
77
|
# @return [Array(Array(String, String), Symbol)] kind and name, and the problem (nil if it is embeddable)
|
|
67
78
|
def check(reference)
|
|
68
79
|
kind, name = reference.split("/", 2)
|
|
69
|
-
return [[kind, name], :kind] unless name &&
|
|
80
|
+
return [[kind, name], :kind] unless name && self.class.embeddable_kind?(kind)
|
|
70
81
|
|
|
71
82
|
problem = @resolver.call(reference)
|
|
72
83
|
[[kind, name], problem.is_a?(Symbol) ? problem : nil]
|
|
@@ -32,6 +32,31 @@ module Archsight
|
|
|
32
32
|
matches.first
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# What an annotation that names another resource (`lifecycle/replacedBy`) refers to. A bare `Name` is a
|
|
36
|
+
# resource of the same kind as the annotated one, another kind needs `Kind/Name`.
|
|
37
|
+
# @param value [String] the annotation value
|
|
38
|
+
# @param kind [String] the kind of the annotated resource
|
|
39
|
+
# @return [Array(String, String), Symbol] `[kind, name]`, or `:missing`
|
|
40
|
+
def find_annotated(value, kind)
|
|
41
|
+
reference = value.to_s.strip
|
|
42
|
+
found = find(reference.include?("/") ? reference : "#{kind}/#{reference}")
|
|
43
|
+
found.is_a?(Array) ? found : :missing
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Where the annotations of a resource that name another resource point to:
|
|
47
|
+
# `{ key => page path }`, `nil` for a reference that does not resolve.
|
|
48
|
+
# @param kind [String] the kind of the annotated resource, which knows the annotation formats
|
|
49
|
+
# @param annotations [Hash]
|
|
50
|
+
def annotation_links(kind, annotations)
|
|
51
|
+
klass = Archsight::Resources[kind]
|
|
52
|
+
annotations.each_with_object({}) do |(key, value), links|
|
|
53
|
+
next unless klass&.annotation_format(key) == :resource
|
|
54
|
+
|
|
55
|
+
found = find_annotated(value, kind)
|
|
56
|
+
links[key] = found.is_a?(Array) ? path(*found) : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
35
60
|
private
|
|
36
61
|
|
|
37
62
|
def in_kind(kind, name)
|
|
@@ -38,7 +38,9 @@ class Archsight::Import::Executor
|
|
|
38
38
|
# @param output [IO] Output stream for progress (default: $stdout)
|
|
39
39
|
# @param filter [String, nil] Regex pattern to match import names
|
|
40
40
|
# @param force [Boolean] Whether to bypass cache and re-run all imports
|
|
41
|
-
|
|
41
|
+
# @param deps [Boolean] With a filter, also run the imports the matched ones depend on (false: only the matched ones)
|
|
42
|
+
def initialize(database:, resources_dir:, verbose: false, max_concurrent: MAX_CONCURRENT, output: $stdout, filter: nil, force: false,
|
|
43
|
+
deps: true)
|
|
42
44
|
@database = database
|
|
43
45
|
@resources_dir = resources_dir
|
|
44
46
|
@verbose = verbose
|
|
@@ -47,7 +49,10 @@ class Archsight::Import::Executor
|
|
|
47
49
|
@filter = filter
|
|
48
50
|
@filter_regex = Regexp.new(filter, Regexp::IGNORECASE) if filter
|
|
49
51
|
@force = force
|
|
52
|
+
@deps = deps
|
|
50
53
|
@executed_this_run = Set.new
|
|
54
|
+
@ran_this_run = Set.new
|
|
55
|
+
@handler_problems = {}
|
|
51
56
|
@iteration = 0
|
|
52
57
|
@mutex = Mutex.new
|
|
53
58
|
end
|
|
@@ -154,18 +159,24 @@ class Archsight::Import::Executor
|
|
|
154
159
|
sorted = topological_sort(filtered_imports)
|
|
155
160
|
|
|
156
161
|
@output.puts "Filter: #{@filter}" if @filter
|
|
162
|
+
@output.puts "Dependencies: not run (--no-deps)" if only_matches?
|
|
157
163
|
|
|
158
164
|
sorted.each_with_index do |imp, idx|
|
|
159
165
|
enabled = import_enabled?(imp)
|
|
160
166
|
deps = import_dependency_names(imp)
|
|
161
167
|
deps_str = deps.empty? ? "(no dependencies)" : "depends on: #{deps.join(", ")}"
|
|
162
168
|
enabled_str = enabled ? "" : " [DISABLED]"
|
|
163
|
-
@output.puts " #{idx + 1}. #{imp.name}#{enabled_str} #{deps_str}"
|
|
169
|
+
@output.puts " #{idx + 1}. #{imp.name}#{enabled_str} #{deps_str}#{unmatched_patterns_note(imp)}"
|
|
164
170
|
end
|
|
165
171
|
|
|
166
172
|
sorted
|
|
167
173
|
end
|
|
168
174
|
|
|
175
|
+
# Names of the imports that executed in this run (not served from the cache)
|
|
176
|
+
def ran_imports
|
|
177
|
+
@mutex.synchronize { @ran_this_run.to_a.sort }
|
|
178
|
+
end
|
|
179
|
+
|
|
169
180
|
private
|
|
170
181
|
|
|
171
182
|
# Set up signal handlers for graceful shutdown
|
|
@@ -222,7 +233,13 @@ class Archsight::Import::Executor
|
|
|
222
233
|
@imports_to_run.include?(import.name)
|
|
223
234
|
end
|
|
224
235
|
|
|
225
|
-
#
|
|
236
|
+
# Whether only the imports matching the filter run (`-f` with `--no-deps`)
|
|
237
|
+
def only_matches?
|
|
238
|
+
!@deps && !@filter_regex.nil?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Compute the full set of imports to run: filtered imports + all their dependencies (only the filtered ones with
|
|
242
|
+
# `--no-deps`)
|
|
226
243
|
def compute_imports_to_run
|
|
227
244
|
all_imports = database.instances_by_kind("Import")&.values || []
|
|
228
245
|
imports_by_name = all_imports.to_h { |imp| [imp.name, imp] }
|
|
@@ -230,6 +247,7 @@ class Archsight::Import::Executor
|
|
|
230
247
|
# Start with imports matching the filter
|
|
231
248
|
matching = all_imports.select { |imp| @filter_regex.match?(imp.name) }.map(&:name)
|
|
232
249
|
to_run = Set.new(matching)
|
|
250
|
+
return to_run unless @deps
|
|
233
251
|
|
|
234
252
|
# Recursively add dependencies
|
|
235
253
|
queue = matching.dup
|
|
@@ -249,9 +267,12 @@ class Archsight::Import::Executor
|
|
|
249
267
|
to_run
|
|
250
268
|
end
|
|
251
269
|
|
|
252
|
-
# Check if all dependencies of an import are satisfied (executed successfully this run)
|
|
270
|
+
# Check if all dependencies of an import are satisfied (executed successfully this run).
|
|
271
|
+
# A parent that can never run (no handler or disabled, e.g. the repository was
|
|
272
|
+
# deleted upstream and only its generated `generates` entry is left) is not
|
|
273
|
+
# waited for, otherwise its children would deadlock the run.
|
|
253
274
|
def dependencies_satisfied?(import)
|
|
254
|
-
dep_names =
|
|
275
|
+
dep_names = runnable_dependency_names(import)
|
|
255
276
|
return true if dep_names.empty?
|
|
256
277
|
|
|
257
278
|
@mutex.synchronize do
|
|
@@ -261,30 +282,81 @@ class Archsight::Import::Executor
|
|
|
261
282
|
end
|
|
262
283
|
end
|
|
263
284
|
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
|
|
285
|
+
# With `--no-deps` a dependency outside the run is not waited for: it does not run, so the import uses the model
|
|
286
|
+
# as it is
|
|
287
|
+
def runnable_dependency_names(import)
|
|
288
|
+
imports_by_name = (database.instances_by_kind("Import")&.values || []).to_h { |imp| [imp.name, imp] }
|
|
289
|
+
import_dependency_names(import).select do |dep_name|
|
|
290
|
+
parent = imports_by_name[dep_name]
|
|
291
|
+
parent && import_enabled?(parent) && (!only_matches? || import_matches_filter?(parent))
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Get dependency names for an import: the parents that generate it (the inverse of `generates` - if Import A
|
|
296
|
+
# generates Import B, then B depends on A) and the imports it declares in `import/dependsOn`
|
|
267
297
|
def import_dependency_names(import)
|
|
268
|
-
# Find all imports that have this import in their generates.imports
|
|
269
298
|
all_imports = database.instances_by_kind("Import")&.values || []
|
|
270
299
|
parent_imports = all_imports.select do |parent|
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
generated_import_names(parent).include?(import.name)
|
|
301
|
+
end
|
|
302
|
+
(parent_imports.map(&:name) + declared_dependency_names(import, all_imports)).uniq
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The imports `import/dependsOn` names or matches. For a pattern this also includes the imports that may still
|
|
306
|
+
# generate a matching import in this run: one whose last run generated a match, and one that never ran but whose
|
|
307
|
+
# handler generates imports. So `Import:Repo:*` waits for the GitHub import and then for the repositories it found.
|
|
308
|
+
def declared_dependency_names(import, all_imports = database.instances_by_kind("Import")&.values || [])
|
|
309
|
+
entries = Archsight::Import.declared_dependencies(import)
|
|
310
|
+
return [] if entries.empty?
|
|
311
|
+
|
|
312
|
+
others = all_imports.reject { |imp| imp.name == import.name }
|
|
313
|
+
entries.flat_map do |entry|
|
|
314
|
+
matches = others.select { |imp| Archsight::Import.dependency_match?(entry, imp.name) }
|
|
315
|
+
matches += others.select { |imp| may_generate_match?(imp, entry) } if Archsight::Import.pattern?(entry)
|
|
316
|
+
matches.map(&:name)
|
|
317
|
+
end.uniq
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Whether an import generated a match of the pattern in its last run, or may do so on its first one
|
|
321
|
+
def may_generate_match?(import, pattern)
|
|
322
|
+
generated = generated_import_names(import)
|
|
323
|
+
return generated.any? { |name| Archsight::Import.dependency_match?(pattern, name) } if generated.any?
|
|
324
|
+
|
|
325
|
+
handler = Archsight::Import::Registry[import.annotations["import/handler"]]
|
|
326
|
+
!import.annotations["generated/at"] && handler.respond_to?(:generates_imports?) && handler.generates_imports?
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def generated_import_names(import)
|
|
330
|
+
(import.relations(:generates, :imports) || []).map { |gen| gen.is_a?(String) ? gen : gen.name }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# For the plan: patterns of `import/dependsOn` that match no import yet
|
|
334
|
+
def unmatched_patterns_note(import)
|
|
335
|
+
all_names = (database.instances_by_kind("Import")&.keys || []) - [import.name]
|
|
336
|
+
unmatched = Archsight::Import.declared_dependencies(import).select do |entry|
|
|
337
|
+
Archsight::Import.pattern?(entry) && all_names.none? { |name| Archsight::Import.dependency_match?(entry, name) }
|
|
273
338
|
end
|
|
274
|
-
|
|
339
|
+
unmatched.empty? ? "" : " (#{unmatched.join(", ")} matches no import yet)"
|
|
275
340
|
end
|
|
276
341
|
|
|
277
|
-
# Reload database without verbose output
|
|
342
|
+
# Reload database without verbose output, and load the handlers the imports declare (`import/handlerPath`)
|
|
278
343
|
def reload_database_quietly!
|
|
279
344
|
original_verbose = database.verbose
|
|
280
345
|
database.verbose = false
|
|
281
346
|
database.reload!
|
|
347
|
+
load_local_handlers
|
|
282
348
|
# Reset filter cache since new imports may have been discovered
|
|
283
349
|
@imports_to_run = nil if @filter_regex
|
|
284
350
|
ensure
|
|
285
351
|
database.verbose = original_verbose
|
|
286
352
|
end
|
|
287
353
|
|
|
354
|
+
# A handler file that does not load fails the imports that use it, not the whole run
|
|
355
|
+
def load_local_handlers
|
|
356
|
+
problems = Archsight::Import::HandlerLoader.load(database, resources_dir: resources_dir)
|
|
357
|
+
@handler_problems = problems.to_h { |problem| [problem.import.name, problem.message] }
|
|
358
|
+
end
|
|
359
|
+
|
|
288
360
|
# Reload database and update progress total for newly discovered imports
|
|
289
361
|
def reload_and_update_total!
|
|
290
362
|
reload_database_quietly!
|
|
@@ -315,6 +387,8 @@ class Archsight::Import::Executor
|
|
|
315
387
|
result = execute_single_import(imp, slot_progress)
|
|
316
388
|
if result == :cached
|
|
317
389
|
slot_progress.complete("Cached")
|
|
390
|
+
elsif result.is_a?(String)
|
|
391
|
+
slot_progress.complete(result)
|
|
318
392
|
else
|
|
319
393
|
@mutex.synchronize { @total_executed += 1 }
|
|
320
394
|
slot_progress.complete("Done")
|
|
@@ -339,7 +413,7 @@ class Archsight::Import::Executor
|
|
|
339
413
|
end
|
|
340
414
|
|
|
341
415
|
# Execute a single import (called from thread)
|
|
342
|
-
# @return [Symbol] :executed, :cached, or raises on error
|
|
416
|
+
# @return [Symbol, String] :executed, :cached, a message for a skipped import, or raises on error
|
|
343
417
|
def execute_single_import(import, import_progress)
|
|
344
418
|
# Check if import is still fresh based on generated/at and import/cacheTime
|
|
345
419
|
if import_fresh?(import)
|
|
@@ -347,6 +421,18 @@ class Archsight::Import::Executor
|
|
|
347
421
|
return :cached
|
|
348
422
|
end
|
|
349
423
|
|
|
424
|
+
problem = @handler_problems[import.name]
|
|
425
|
+
raise Archsight::Import::UnknownHandlerError, problem if problem
|
|
426
|
+
|
|
427
|
+
missing = missing_environment(import)
|
|
428
|
+
if missing.any?
|
|
429
|
+
message = "missing environment variable#{"s" if missing.length > 1} #{missing.join(", ")} (import/config/requiredEnv)"
|
|
430
|
+
raise Archsight::Import::ImportError, message unless import.annotations["import/config/optional"] == "true"
|
|
431
|
+
|
|
432
|
+
import_progress.warn("Skipped: #{message}")
|
|
433
|
+
return "Skipped"
|
|
434
|
+
end
|
|
435
|
+
|
|
350
436
|
# Execute the import
|
|
351
437
|
handler_class = Archsight::Import::Registry.handler_for(import)
|
|
352
438
|
handler = handler_class.new(
|
|
@@ -356,13 +442,32 @@ class Archsight::Import::Executor
|
|
|
356
442
|
progress: import_progress,
|
|
357
443
|
shared_writer: @shared_writer
|
|
358
444
|
)
|
|
445
|
+
Archsight::Import.never_index(handler.cache_root)
|
|
359
446
|
handler.execute
|
|
447
|
+
remove_stale_outputs(import, handler.owned_outputs)
|
|
448
|
+
@mutex.synchronize { @ran_this_run.add(import.name) }
|
|
360
449
|
:executed
|
|
361
450
|
end
|
|
362
451
|
|
|
452
|
+
# Variables of `import/config/requiredEnv` that are not set (or blank)
|
|
453
|
+
def missing_environment(import)
|
|
454
|
+
names = import.annotations["import/config/requiredEnv"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
455
|
+
names.select { |name| ENV[name].to_s.strip.empty? }
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Delete the files an earlier run of the import wrote (`generated/outputs`) that this run did not write again
|
|
459
|
+
def remove_stale_outputs(import, written)
|
|
460
|
+
previous = import.annotations["generated/outputs"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
461
|
+
(previous - written).each do |rel_path|
|
|
462
|
+
path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
463
|
+
FileUtils.rm_f(path) if path && File.file?(path)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
363
467
|
# Check if import is still fresh (cached)
|
|
364
|
-
# Uses generated/at annotation and import/cacheTime to determine freshness
|
|
365
|
-
#
|
|
468
|
+
# Uses generated/at annotation and import/cacheTime to determine freshness. The cache is also invalid when the
|
|
469
|
+
# output file or a recorded output (`generated/outputs`) is missing, the configuration or the files of
|
|
470
|
+
# `import/config/inputs` changed, or an import it declares in `import/dependsOn` executed in this run.
|
|
366
471
|
def import_fresh?(import)
|
|
367
472
|
# Honor force flag - always re-run when forced
|
|
368
473
|
return false if @force
|
|
@@ -377,6 +482,11 @@ class Archsight::Import::Executor
|
|
|
377
482
|
return false unless File.exist?(full_path)
|
|
378
483
|
end
|
|
379
484
|
|
|
485
|
+
return false unless recorded_outputs_exist?(import)
|
|
486
|
+
return false if dependency_ran?(import)
|
|
487
|
+
return false if import.annotations["import/config/inputs"] &&
|
|
488
|
+
import.annotations["generated/inputsHash"] != Archsight::Import.inputs_hash(import, resources_dir)
|
|
489
|
+
|
|
380
490
|
generated_at = import.annotations["generated/at"]
|
|
381
491
|
return false if generated_at.nil? || generated_at.empty?
|
|
382
492
|
|
|
@@ -399,6 +509,20 @@ class Archsight::Import::Executor
|
|
|
399
509
|
false
|
|
400
510
|
end
|
|
401
511
|
|
|
512
|
+
def recorded_outputs_exist?(import)
|
|
513
|
+
import.annotations["generated/outputs"].to_s.split(",").map(&:strip).reject(&:empty?).all? do |rel_path|
|
|
514
|
+
path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
515
|
+
path && File.exist?(path)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Whether an import declared in `import/dependsOn` executed (not just came from the cache) in this run. The
|
|
520
|
+
# generated imports of a parent are not affected: they keep their own cache time when the parent runs.
|
|
521
|
+
def dependency_ran?(import)
|
|
522
|
+
names = declared_dependency_names(import)
|
|
523
|
+
@mutex.synchronize { names.any? { |name| @ran_this_run.include?(name) } }
|
|
524
|
+
end
|
|
525
|
+
|
|
402
526
|
def parse_duration(str)
|
|
403
527
|
return nil if str.nil?
|
|
404
528
|
|