archsight 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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 +637 -34
- data/docs/pages.md +44 -13
- 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 +246 -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 +101 -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 +40 -13
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +4 -6
- data/lib/archsight/export/confluence/storage.rb +38 -2
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +139 -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 +64 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +121 -14
- data/lib/archsight/page_loader.rb +12 -1
- 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_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 +89 -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/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +10 -8
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +7 -2
- data/lib/archsight/web/application.rb +38 -2
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +1 -0
- data/lib/archsight/web/public/vue/DocPage-D52OwaKc.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +1 -0
- data/lib/archsight/web/public/vue/KindList-CyJuP_uq.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-FyygrS-b.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +2 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-BewvsfOc.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +1 -0
- data/lib/archsight/web/public/vue/index-BGlYCbi7.js +3 -0
- data/lib/archsight/web/public/vue/index-eUHmJ7K2.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +1 -0
- data/lib/archsight/web/public/vue/{mermaid-BjEi5URd.js → mermaid-DjsXQ_XN.js} +35 -35
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DlSnFeCL.js → useGraphviz-4evusK9i.js} +1 -1
- data/lib/archsight/web/public/vue/{useHighlight-D5Gf8DLl.js → useHighlight-DPguaxfm.js} +8 -8
- data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +12 -0
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +58 -38
- data/lib/archsight/web/public/vue/ApiDocsPage-BgqnQgwa.js +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/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/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/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require_relative "../handler"
|
|
8
|
+
require_relative "../registry"
|
|
9
|
+
|
|
10
|
+
# Command handler - runs a program of the resources repository as an import, in any language. The program writes its
|
|
11
|
+
# resources itself (usually to ARCHSIGHT_OUTPUT_PATH); archsight adds the self marker, so caching, `import/dependsOn`,
|
|
12
|
+
# `-f` and `-n` work as for any other import.
|
|
13
|
+
#
|
|
14
|
+
# The program runs in the root of the repository that holds the resources directory (the resources directory when it
|
|
15
|
+
# is not in a git repository), with a clean environment: PATH, HOME, LANG and the variables below, plus the variables
|
|
16
|
+
# listed in `import/config/env` (credentials, for example).
|
|
17
|
+
#
|
|
18
|
+
# ARCHSIGHT_RESOURCES_DIR - the resources directory
|
|
19
|
+
# ARCHSIGHT_CACHE_DIR - where the git-based imports clone repositories
|
|
20
|
+
# ARCHSIGHT_IMPORT_NAME - the name of the Import
|
|
21
|
+
# ARCHSIGHT_OUTPUT_PATH - a file for the YAML resources the program generates; archsight writes them unchanged to
|
|
22
|
+
# import/outputPath (generated/<name>.yaml without one), followed by the import's marker
|
|
23
|
+
#
|
|
24
|
+
# The output file belongs to the import: it is replaced on every run and removed when outputPath changes. Documents
|
|
25
|
+
# are not marked as generated; a program that wants that sets `generated/script: <import name>` itself (only on kinds
|
|
26
|
+
# that have the `generated` annotations), while documents that add relations to hand-written resources stay unmarked.
|
|
27
|
+
#
|
|
28
|
+
# Configuration:
|
|
29
|
+
# import/config/command - the command line (split like a shell does, but run without one; use `sh -c '...'` for
|
|
30
|
+
# pipes); a relative program path is relative to the working directory
|
|
31
|
+
# import/config/env - names of further environment variables to pass, comma separated
|
|
32
|
+
# import/config/outputs - files the program writes besides the output path, glob patterns relative to the
|
|
33
|
+
# resources directory, comma separated; recorded like Handler#write_file
|
|
34
|
+
# import/config/workdir - working directory relative to the resources directory (default: the repository root)
|
|
35
|
+
class Archsight::Import::Handlers::Command < Archsight::Import::Handler
|
|
36
|
+
PASSED_ENV = %w[PATH HOME LANG LC_ALL TMPDIR].freeze
|
|
37
|
+
|
|
38
|
+
def execute
|
|
39
|
+
command = Shellwords.split(config("command").to_s)
|
|
40
|
+
raise "Missing required config: command" if command.empty?
|
|
41
|
+
|
|
42
|
+
Dir.mktmpdir("archsight-command") do |tmp|
|
|
43
|
+
output_path = File.join(tmp, "output.yaml")
|
|
44
|
+
progress.update("Running #{command.first}")
|
|
45
|
+
out, err, status = Open3.capture3(environment(output_path), *command, chdir: working_directory, unsetenv_others: true)
|
|
46
|
+
raise "Command failed (exit #{status.exitstatus}): #{last_lines(err.empty? ? out : err)}" unless status.success?
|
|
47
|
+
|
|
48
|
+
record_declared_outputs
|
|
49
|
+
write_output(program_output(output_path))
|
|
50
|
+
end
|
|
51
|
+
rescue Errno::ENOENT => e
|
|
52
|
+
raise "Command not found: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# What the program wrote to ARCHSIGHT_OUTPUT_PATH (a temporary file), as written: it must be YAML documents that
|
|
58
|
+
# are mappings, and stays byte for byte as it is (comments, order, no added annotations)
|
|
59
|
+
def program_output(output_path)
|
|
60
|
+
return "" unless File.file?(output_path)
|
|
61
|
+
|
|
62
|
+
text = File.read(output_path)
|
|
63
|
+
YAML.load_stream(text).compact.each do |document|
|
|
64
|
+
raise "#{import_resource.name}: the command wrote a document that is not a resource" unless document.is_a?(Hash)
|
|
65
|
+
end
|
|
66
|
+
text.end_with?("\n") || text.empty? ? text : "#{text}\n"
|
|
67
|
+
rescue Psych::SyntaxError => e
|
|
68
|
+
raise "The command wrote invalid YAML: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The output file is the import's own: written whole (not through the shared writer, which would drop the
|
|
72
|
+
# unmarked documents when another import flushes the file) and recorded as an output, so a changed outputPath
|
|
73
|
+
# removes the old file. Recorded before the marker is built, so the marker lists it.
|
|
74
|
+
def write_output(text)
|
|
75
|
+
rel_path = Archsight::Import.output_path(import_resource)
|
|
76
|
+
record_output(rel_path)
|
|
77
|
+
write_file(rel_path, text + YAML.dump(self_marker))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def environment(output_path)
|
|
81
|
+
passed = PASSED_ENV + config("env").to_s.split(",").map(&:strip).reject(&:empty?)
|
|
82
|
+
env = passed.to_h { |name| [name, ENV.fetch(name, nil)] }.compact
|
|
83
|
+
env.merge(
|
|
84
|
+
"ARCHSIGHT_RESOURCES_DIR" => File.expand_path(resources_dir),
|
|
85
|
+
"ARCHSIGHT_CACHE_DIR" => cache_root,
|
|
86
|
+
"ARCHSIGHT_IMPORT_NAME" => import_resource.name,
|
|
87
|
+
"ARCHSIGHT_OUTPUT_PATH" => File.expand_path(output_path)
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def working_directory
|
|
92
|
+
workdir = config("workdir")
|
|
93
|
+
if workdir
|
|
94
|
+
root = File.expand_path(resources_dir)
|
|
95
|
+
path = File.expand_path(workdir, root)
|
|
96
|
+
return path if path == root || path.start_with?("#{root}/")
|
|
97
|
+
|
|
98
|
+
raise "workdir #{workdir} is outside the resources directory"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
toplevel, _, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: resources_dir)
|
|
102
|
+
status.success? ? toplevel.strip : File.expand_path(resources_dir)
|
|
103
|
+
rescue Errno::ENOENT
|
|
104
|
+
File.expand_path(resources_dir)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def record_declared_outputs
|
|
108
|
+
patterns = config("outputs").to_s.split(",").map(&:strip).reject(&:empty?)
|
|
109
|
+
root = File.expand_path(resources_dir)
|
|
110
|
+
patterns.flat_map { |pattern| Dir.glob(pattern, base: root) }.uniq.each do |file|
|
|
111
|
+
record_output(file) if File.file?(File.join(root, file))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def last_lines(text)
|
|
116
|
+
text.to_s.lines.last(5).join.strip
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
Archsight::Import::Registry.register("command", Archsight::Import::Handlers::Command)
|
|
@@ -35,6 +35,9 @@ require_relative "cache_pruner"
|
|
|
35
35
|
# the API (e.g. a repo that was renamed or deleted), so stale clones don't
|
|
36
36
|
# linger and get picked up by later importers.
|
|
37
37
|
class Archsight::Import::Handlers::Github < Archsight::Import::Handler
|
|
38
|
+
# generates child imports (see Handler.generates_imports?)
|
|
39
|
+
def self.generates_imports? = true
|
|
40
|
+
|
|
38
41
|
include Archsight::Import::Handlers::CachePruner
|
|
39
42
|
|
|
40
43
|
PER_PAGE = 100
|
|
@@ -49,7 +52,7 @@ class Archsight::Import::Handlers::Github < Archsight::Import::Handler
|
|
|
49
52
|
@repo_output_path = config("repoOutputPath")
|
|
50
53
|
@child_cache_time = config("childCacheTime")
|
|
51
54
|
@default_visibility = config("defaultVisibility", default: "internal")
|
|
52
|
-
cache_root = config("cacheRoot"
|
|
55
|
+
cache_root = Archsight::Import.cache_root(config("cacheRoot"))
|
|
53
56
|
@target_dir = File.join(cache_root, "github", @org)
|
|
54
57
|
|
|
55
58
|
# Fetch all repositories with pagination
|
|
@@ -38,6 +38,9 @@ require_relative "cache_pruner"
|
|
|
38
38
|
# API (e.g. a project that was renamed, moved, or deleted), so stale clones
|
|
39
39
|
# don't linger and get picked up by later importers.
|
|
40
40
|
class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
|
|
41
|
+
# generates child imports (see Handler.generates_imports?)
|
|
42
|
+
def self.generates_imports? = true
|
|
43
|
+
|
|
41
44
|
include Archsight::Import::Handlers::CachePruner
|
|
42
45
|
|
|
43
46
|
def execute
|
|
@@ -51,7 +54,7 @@ class Archsight::Import::Handlers::Gitlab < Archsight::Import::Handler
|
|
|
51
54
|
@child_cache_time = config("childCacheTime")
|
|
52
55
|
@default_visibility = config("defaultVisibility", default: "internal")
|
|
53
56
|
|
|
54
|
-
cache_root = config("cacheRoot"
|
|
57
|
+
cache_root = Archsight::Import.cache_root(config("cacheRoot"))
|
|
55
58
|
@target_dir = File.join(cache_root, "gitlab")
|
|
56
59
|
@explore_groups = config("exploreGroups") == "true"
|
|
57
60
|
@per_page = config("perPage", default: "100").to_i
|
|
@@ -20,13 +20,19 @@ require_relative "../registry"
|
|
|
20
20
|
#
|
|
21
21
|
# Configuration:
|
|
22
22
|
# import/config/path - Path to the Go repository root (same as GoGrapher)
|
|
23
|
+
# import/config/artifact - Repository artifact the modules belong to; names the component when two repositories
|
|
24
|
+
# declare the same module path (see GoModuleParser#owned_component_name)
|
|
23
25
|
class Archsight::Import::Handlers::GoDepResolver < Archsight::Import::Handler
|
|
24
26
|
include Archsight::Import::Handlers::GoModuleParser
|
|
25
27
|
|
|
26
28
|
def execute
|
|
27
29
|
@path = config_path("path")
|
|
28
30
|
raise "Missing required config: path" unless @path
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
unless File.directory?(@path)
|
|
33
|
+
progress.warn("Skipping, repository not cloned: #{@path}")
|
|
34
|
+
return
|
|
35
|
+
end
|
|
30
36
|
|
|
31
37
|
import_resource.annotations["import/outputPath"] ||= "generated/relationships.yaml"
|
|
32
38
|
|
|
@@ -37,11 +43,13 @@ class Archsight::Import::Handlers::GoDepResolver < Archsight::Import::Handler
|
|
|
37
43
|
output = +""
|
|
38
44
|
|
|
39
45
|
modules.each do |rel_dir, mod_name|
|
|
46
|
+
next if non_runtime_module?(rel_dir) || copied_module?(rel_dir, mod_name, config("artifact"))
|
|
47
|
+
|
|
40
48
|
mod_dir = rel_dir == "." ? @path : File.join(@path, rel_dir)
|
|
41
49
|
dep_names = compute_deps(mod_dir, mod_name, existing_components)
|
|
42
50
|
next if dep_names.empty?
|
|
43
51
|
|
|
44
|
-
comp_name =
|
|
52
|
+
comp_name = owned_component_name(mod_name, config("artifact"))
|
|
45
53
|
existing = existing_components[comp_name]
|
|
46
54
|
next unless existing
|
|
47
55
|
|
|
@@ -23,8 +23,18 @@ require_relative "../registry"
|
|
|
23
23
|
# import/config/nodesep - Vertical gap between nodes in a column (default: 0.15)
|
|
24
24
|
# import/config/interface_visibility - Visibility prefix for detected interfaces (default: Private)
|
|
25
25
|
class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grapher
|
|
26
|
+
# generates child imports (see Handler.generates_imports?)
|
|
27
|
+
def self.generates_imports? = true
|
|
28
|
+
|
|
26
29
|
include Archsight::Import::Handlers::GoModuleParser
|
|
27
30
|
|
|
31
|
+
# Directories not scanned for Go packages
|
|
32
|
+
PACKAGE_SKIP_DIRS = %w[vendor testdata node_modules].freeze
|
|
33
|
+
# Packages below these directories are not meant to be imported by other modules
|
|
34
|
+
NON_LIBRARY_DIRS = %w[internal cmd hack tools examples example e2e test tests].freeze
|
|
35
|
+
MAX_ENTRYPOINTS = 10
|
|
36
|
+
IGNORED_BUILD = %r{^(//go:build\s+ignore\b|//\s*\+build\s+ignore\b)}
|
|
37
|
+
|
|
28
38
|
OPENAPI_FILENAMES = %w[openapi.yaml openapi.yml openapi.json swagger.yaml swagger.yml swagger.json].freeze
|
|
29
39
|
OPENAPI_SUBDIRS = %w[api docs spec].freeze
|
|
30
40
|
|
|
@@ -55,7 +65,14 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
55
65
|
output = +""
|
|
56
66
|
|
|
57
67
|
modules.each do |rel_dir, mod_name|
|
|
58
|
-
|
|
68
|
+
next if non_runtime_module?(rel_dir)
|
|
69
|
+
|
|
70
|
+
if copied_module?(rel_dir, mod_name, artifact_name)
|
|
71
|
+
progress.warn("Skipping copied go.mod at #{rel_dir}: module #{mod_name} belongs to another repository")
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
comp_name = owned_component_name(mod_name, artifact_name)
|
|
59
76
|
next unless comp_name
|
|
60
77
|
|
|
61
78
|
mod_dir = rel_dir == "." ? path : File.join(path, rel_dir)
|
|
@@ -64,6 +81,9 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
64
81
|
interface_names = specs.filter_map { |s| interface_name_from_spec(s, visibility: visibility) }
|
|
65
82
|
.select { |n| existing_interfaces.key?(n) }
|
|
66
83
|
|
|
84
|
+
existing = database&.instances_by_kind("ApplicationComponent")&.[](comp_name)
|
|
85
|
+
facts = curated_component_type?(existing) ? {} : component_facts(path, rel_dir, mod_name)
|
|
86
|
+
|
|
67
87
|
comp_spec = { "realizedThrough" => { "technologyArtifacts" => [artifact_name] } }
|
|
68
88
|
comp_spec["exposes"] = { "applicationInterfaces" => interface_names } if interface_names.any?
|
|
69
89
|
|
|
@@ -73,7 +93,8 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
73
93
|
component = untracked_resource_yaml(
|
|
74
94
|
kind: "ApplicationComponent",
|
|
75
95
|
name: comp_name,
|
|
76
|
-
spec: comp_spec
|
|
96
|
+
spec: comp_spec,
|
|
97
|
+
annotations: component_annotations(facts, existing_tags: existing&.annotations&.[]("architecture/tags"))
|
|
77
98
|
)
|
|
78
99
|
output << YAML.dump(component)
|
|
79
100
|
end
|
|
@@ -85,7 +106,7 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
85
106
|
output << YAML.dump(import_yaml(
|
|
86
107
|
name: resolver_name,
|
|
87
108
|
handler: "go-dep-resolver",
|
|
88
|
-
config: { "path" => path }
|
|
109
|
+
config: { "path" => path, "artifact" => artifact_name }
|
|
89
110
|
))
|
|
90
111
|
|
|
91
112
|
output
|
|
@@ -130,7 +151,83 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
130
151
|
|
|
131
152
|
# ── OpenAPI detection ─────────────────────────────────────────────────────
|
|
132
153
|
|
|
133
|
-
|
|
154
|
+
# A component type another source set (a person or an earlier import) is kept; this import's own earlier
|
|
155
|
+
# output is regenerated.
|
|
156
|
+
def curated_component_type?(existing)
|
|
157
|
+
return false unless existing&.annotations&.key?("component/type")
|
|
158
|
+
|
|
159
|
+
existing.annotations["generated/script"] != import_resource.name
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Classify a go.mod module from its packages: with `package main` it is an executable, without it a library,
|
|
163
|
+
# and a module with both main packages and importable packages is a module.
|
|
164
|
+
def component_facts(repo_root, rel_dir, _mod_name)
|
|
165
|
+
mod_dir = rel_dir == "." ? repo_root : File.join(repo_root, rel_dir)
|
|
166
|
+
mains, libraries = scan_go_packages(mod_dir)
|
|
167
|
+
type = if mains.empty?
|
|
168
|
+
"library"
|
|
169
|
+
elsif libraries.empty?
|
|
170
|
+
"executable"
|
|
171
|
+
else
|
|
172
|
+
"module"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
{ type: type,
|
|
176
|
+
tags: %w[ecosystem:go packaging:go-module], entrypoints: mains.first(MAX_ENTRYPOINTS) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @return [Array<Array<String>>] [main package dirs, importable library package dirs], relative to mod_dir
|
|
180
|
+
def scan_go_packages(mod_dir)
|
|
181
|
+
mains = []
|
|
182
|
+
libraries = []
|
|
183
|
+
|
|
184
|
+
Find.find(mod_dir) do |path|
|
|
185
|
+
if File.directory?(path)
|
|
186
|
+
Find.prune if path != mod_dir && skip_go_dir?(path)
|
|
187
|
+
next
|
|
188
|
+
end
|
|
189
|
+
next unless go_source?(path)
|
|
190
|
+
|
|
191
|
+
name = go_package_name(path)
|
|
192
|
+
next unless name
|
|
193
|
+
|
|
194
|
+
dir = File.dirname(path).delete_prefix(mod_dir).delete_prefix("/")
|
|
195
|
+
dir = "." if dir.empty?
|
|
196
|
+
if name == "main"
|
|
197
|
+
mains << dir
|
|
198
|
+
elsif !dir.split("/").intersect?(NON_LIBRARY_DIRS)
|
|
199
|
+
libraries << dir
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
[mains.uniq.sort, libraries.uniq.sort]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Hidden and vendored directories, and nested modules (they are components of their own)
|
|
207
|
+
def skip_go_dir?(path)
|
|
208
|
+
base = File.basename(path)
|
|
209
|
+
PACKAGE_SKIP_DIRS.include?(base) || base.start_with?(".") || File.exist?(File.join(path, "go.mod"))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def go_source?(path)
|
|
213
|
+
path.end_with?(".go") && !path.end_with?("_test.go")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Package name of a Go file, nil for a file that is excluded from normal builds (`//go:build ignore`)
|
|
217
|
+
def go_package_name(path)
|
|
218
|
+
head = File.foreach(path, encoding: "utf-8").first(40)
|
|
219
|
+
return nil if head.any? { |line| line.match?(IGNORED_BUILD) }
|
|
220
|
+
|
|
221
|
+
head.each do |line|
|
|
222
|
+
match = line.match(/\Apackage\s+(\w+)/)
|
|
223
|
+
return match[1] if match
|
|
224
|
+
end
|
|
225
|
+
nil
|
|
226
|
+
rescue SystemCallError, ArgumentError
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def untracked_resource_yaml(kind:, name:, spec: {}, annotations: {})
|
|
134
231
|
{
|
|
135
232
|
"apiVersion" => "architecture/v1alpha1",
|
|
136
233
|
"kind" => kind,
|
|
@@ -139,7 +236,7 @@ class Archsight::Import::Handlers::GoGrapher < Archsight::Import::Handlers::Grap
|
|
|
139
236
|
"annotations" => {
|
|
140
237
|
"generated/script" => import_resource.name,
|
|
141
238
|
"generated/at" => Time.now.utc.iso8601
|
|
142
|
-
}
|
|
239
|
+
}.merge(annotations)
|
|
143
240
|
},
|
|
144
241
|
"spec" => spec
|
|
145
242
|
}
|
|
@@ -7,6 +7,13 @@ require "find"
|
|
|
7
7
|
# Provides go.mod discovery, module name reading, require parsing, and the
|
|
8
8
|
# component_name convention (strip SCM host, join path segments with ":").
|
|
9
9
|
module Archsight::Import::Handlers::GoModuleParser
|
|
10
|
+
# Directories whose nested modules hold code that never runs in production: tests, pinned tooling, examples and
|
|
11
|
+
# benchmarks. Whole path segments only, so `pkg/latest` is not a test directory.
|
|
12
|
+
NON_RUNTIME_MODULE_DIRS = %w[
|
|
13
|
+
hack tools examples example bench benchmarks
|
|
14
|
+
test tests e2e e2etest e2etests systemtest systemtests integrationtest integrationtests fullstack
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
10
17
|
# Discover all Go modules in a repository root.
|
|
11
18
|
# Handles go.work workspaces, single-module repos, and multi-module monorepos
|
|
12
19
|
# (root go.mod plus subdirectory go.mod files without go.work).
|
|
@@ -141,4 +148,85 @@ module Archsight::Import::Handlers::GoModuleParser
|
|
|
141
148
|
parts.shift
|
|
142
149
|
parts.join(":") unless parts.empty?
|
|
143
150
|
end
|
|
151
|
+
|
|
152
|
+
# The name of the component that the module of one repository gets. Two repositories may declare the same module
|
|
153
|
+
# path (a prototype and its successor, a fork, a copy of the module); without care both would emit the same
|
|
154
|
+
# component and the loader would merge them into one. The repository that owns the plain name is the one the module
|
|
155
|
+
# path names (see #module_path_of_repository?), then the one that is not archived, then the first by name; the others
|
|
156
|
+
# get the repository appended (`alice:shop@acme:shop-prototype`).
|
|
157
|
+
#
|
|
158
|
+
# The other repositories are found through the existing component of the plain name, so a collision is resolved on
|
|
159
|
+
# the import after both repositories were imported once. A component without `generated/script` is curated: a
|
|
160
|
+
# person deliberately lists several repositories, and it keeps the plain name.
|
|
161
|
+
# @return [String, nil]
|
|
162
|
+
def owned_component_name(mod_name, artifact_name)
|
|
163
|
+
base = component_name(mod_name)
|
|
164
|
+
return base unless base && artifact_name
|
|
165
|
+
|
|
166
|
+
others = colliding_artifacts(base, artifact_name)
|
|
167
|
+
return base if others.empty?
|
|
168
|
+
|
|
169
|
+
owner = ([artifact_name] + others).min_by do |name|
|
|
170
|
+
[module_path_of_repository?(mod_name, name) ? 0 : 1, archived_artifact?(name) ? 1 : 0, name]
|
|
171
|
+
end
|
|
172
|
+
owner == artifact_name ? base : "#{base}@#{artifact_name.delete_prefix("Repo:")}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Whether the module path names the repository: the repository is the module itself or a module below it.
|
|
176
|
+
# `github.com/acme/library` and `github.com/acme/library/v2/client` belong to `Repo:acme:library` (and to
|
|
177
|
+
# `Repo:library`, an artifact named after its directory), not to `Repo:acme:app-poc`. The segments the artifact name
|
|
178
|
+
# and the module path both have must agree, so `github.com/alice/shop` does not name `Repo:acme:shop`.
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
def module_path_of_repository?(mod_name, artifact_name)
|
|
181
|
+
repo = artifact_name.delete_prefix("Repo:").split(":").reject(&:empty?)
|
|
182
|
+
path = module_path_segments(mod_name)
|
|
183
|
+
return false if repo.empty? || path.empty?
|
|
184
|
+
|
|
185
|
+
path.each_index.any? do |i|
|
|
186
|
+
next false if i.zero? && path.length > 1 # the first segment is the owner, never the repository
|
|
187
|
+
|
|
188
|
+
overlap = [repo.length, i + 1].min
|
|
189
|
+
repo.last(overlap) == path[(i + 1 - overlap)..i]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A module in a subdirectory of `artifact_name` that is a copy of another repository's module: its path does not
|
|
194
|
+
# name this repository, but it names another repository of the database. It gets no component; the repository it
|
|
195
|
+
# was copied from has it.
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
def copied_module?(rel_dir, mod_name, artifact_name)
|
|
198
|
+
return false if rel_dir == "." || artifact_name.nil? || module_path_of_repository?(mod_name, artifact_name)
|
|
199
|
+
|
|
200
|
+
artifacts = database&.instances_by_kind("TechnologyArtifact") || {}
|
|
201
|
+
artifacts.keys.any? { |name| name.start_with?("Repo:") && name != artifact_name && module_path_of_repository?(mod_name, name) }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A nested module below a test, tool, example or benchmark directory (`test/e2e`, `build/tools`,
|
|
205
|
+
# `internal/systemtest`). It gets no component, since nothing realizes it or depends on it; its packages stay in
|
|
206
|
+
# the module graph of the repository. `cmd/<name>` and `internal/` hold runtime code and are not matched.
|
|
207
|
+
# @return [Boolean]
|
|
208
|
+
def non_runtime_module?(rel_dir)
|
|
209
|
+
return false if rel_dir == "."
|
|
210
|
+
|
|
211
|
+
rel_dir.split("/").intersect?(NON_RUNTIME_MODULE_DIRS)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Path segments of a module without the host and without a major version suffix (`/v2`)
|
|
215
|
+
def module_path_segments(mod_name)
|
|
216
|
+
mod_name.split("/").drop(1).grep_v(/\Av\d+\z/)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Repository artifacts other than `artifact_name` that a generated component of this name is realized through
|
|
220
|
+
def colliding_artifacts(base, artifact_name)
|
|
221
|
+
existing = database&.instances_by_kind("ApplicationComponent")&.[](base)
|
|
222
|
+
return [] unless existing&.annotations&.key?("generated/script")
|
|
223
|
+
|
|
224
|
+
artifacts = Array(existing.raw.dig("spec", "realizedThrough", "technologyArtifacts"))
|
|
225
|
+
artifacts.select { |name| name.start_with?("Repo:") && name != artifact_name }.uniq
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def archived_artifact?(name)
|
|
229
|
+
artifact = database&.instances_by_kind("TechnologyArtifact")&.[](name)
|
|
230
|
+
artifact&.annotations&.[]("activity/status") == "archived"
|
|
231
|
+
end
|
|
144
232
|
end
|
|
@@ -35,6 +35,11 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
35
35
|
[]
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Namespaces of the machine tags (namespace:value) that importers write into architecture/tags.
|
|
39
|
+
# `entrypoint` is no longer written (see component/entrypoints) but stays listed so a re-import
|
|
40
|
+
# removes the tags of earlier runs.
|
|
41
|
+
MACHINE_TAG_NAMESPACES = %w[ecosystem packaging entrypoint linkage].freeze
|
|
42
|
+
|
|
38
43
|
PALETTE = [
|
|
39
44
|
{ fill: "#ddeeff", edge: "#2266cc" },
|
|
40
45
|
{ fill: "#ddffd8", edge: "#2a8a1e" },
|
|
@@ -51,7 +56,13 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
51
56
|
def execute
|
|
52
57
|
@path = config_path("path")
|
|
53
58
|
raise "Missing required config: path" unless @path
|
|
54
|
-
|
|
59
|
+
|
|
60
|
+
unless File.directory?(@path)
|
|
61
|
+
# The clone is the repository import's job (it reports its own failure);
|
|
62
|
+
# no marker is written, so this runs again once the clone exists.
|
|
63
|
+
progress.warn("Skipping, repository not cloned: #{@path}")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
55
66
|
|
|
56
67
|
# Consolidate all grapher output into a single shared file unless an
|
|
57
68
|
# explicit output path is already set (e.g. via grapherOutputPath on the
|
|
@@ -123,6 +134,34 @@ class Archsight::Import::Handlers::Grapher < Archsight::Import::Handler
|
|
|
123
134
|
raise NotImplementedError, "#{self.class}#collect_packages must be implemented"
|
|
124
135
|
end
|
|
125
136
|
|
|
137
|
+
# Hook: subclasses that emit ApplicationComponents describe each module here.
|
|
138
|
+
# @param _repo_root [String] Absolute path to the repository root
|
|
139
|
+
# @param _rel_dir [String] Directory of the module relative to the root ("." for the root)
|
|
140
|
+
# @param _mod_name [String] Module name from discover_modules
|
|
141
|
+
# @return [Hash] { type: "executable|library|module|...", tags: ["ecosystem:go", ...],
|
|
142
|
+
# entrypoints: ["cmd/api", ...] }, empty when unknown
|
|
143
|
+
def component_facts(_repo_root, _rel_dir, _mod_name)
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# component/type, component/entrypoints and the machine tags (namespace:value) in architecture/tags for the
|
|
148
|
+
# facts of component_facts.
|
|
149
|
+
# The tags a person set stay; machine tags of the importer's namespaces from an earlier run are replaced.
|
|
150
|
+
def component_annotations(facts, existing_tags: nil)
|
|
151
|
+
annotations = {}
|
|
152
|
+
annotations["component/type"] = facts[:type] if facts[:type]
|
|
153
|
+
annotations["component/entrypoints"] = facts[:entrypoints].join(",") if facts[:entrypoints]&.any?
|
|
154
|
+
annotations["architecture/tags"] = merge_machine_tags(existing_tags, facts[:tags]).join(",") if facts[:tags]&.any?
|
|
155
|
+
annotations
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def merge_machine_tags(existing_tags, machine_tags)
|
|
159
|
+
kept = existing_tags.to_s.split(",").map(&:strip).reject(&:empty?).reject do |tag|
|
|
160
|
+
MACHINE_TAG_NAMESPACES.include?(tag.split(":", 2).first) && tag.include?(":")
|
|
161
|
+
end
|
|
162
|
+
(kept + Array(machine_tags)).uniq
|
|
163
|
+
end
|
|
164
|
+
|
|
126
165
|
# ── Module coloring ───────────────────────────────────────────────────────
|
|
127
166
|
|
|
128
167
|
def build_module_colors(modules)
|
|
@@ -22,6 +22,9 @@ require_relative "../team_matcher"
|
|
|
22
22
|
# import/config/corporateAffixes - Optional comma-separated corporate username affixes for team matching (e.g., "ionos,1and1")
|
|
23
23
|
# import/config/grapherOutputPath - Optional output path for language-grapher child imports
|
|
24
24
|
class Archsight::Import::Handlers::Repository < Archsight::Import::Handler
|
|
25
|
+
# generates child imports (see Handler.generates_imports?)
|
|
26
|
+
def self.generates_imports? = true
|
|
27
|
+
|
|
25
28
|
def execute
|
|
26
29
|
@path = config_path("path")
|
|
27
30
|
@git_url = config("gitUrl")
|
|
@@ -43,6 +43,9 @@ require_relative "../registry"
|
|
|
43
43
|
# ]
|
|
44
44
|
# }
|
|
45
45
|
class Archsight::Import::Handlers::RestApiIndex < Archsight::Import::Handler
|
|
46
|
+
# generates child imports (see Handler.generates_imports?)
|
|
47
|
+
def self.generates_imports? = true
|
|
48
|
+
|
|
46
49
|
VALID_GATES = %w[General-Availability Early-Access Development GA EA DEV].freeze
|
|
47
50
|
GATE_TOKEN_RE = /\b(ga|ea|dev)\b/i
|
|
48
51
|
|
|
@@ -8,13 +8,25 @@ require "archsight/import"
|
|
|
8
8
|
# The executor looks up handlers by name to instantiate and execute them.
|
|
9
9
|
module Archsight::Import::Registry
|
|
10
10
|
@handlers = {}
|
|
11
|
+
@builtin = {}
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
14
|
# Register a handler class with a name
|
|
14
15
|
# @param name [String, Symbol] Handler name (matches import/handler annotation)
|
|
15
16
|
# @param handler_class [Class] Handler class that extends Handler
|
|
17
|
+
# @raise [DuplicateHandlerError] if another class already registered the name
|
|
16
18
|
def register(name, handler_class)
|
|
17
|
-
|
|
19
|
+
name = name.to_s
|
|
20
|
+
existing = @handlers[name] || @builtin[name]
|
|
21
|
+
raise Archsight::Import::DuplicateHandlerError, "Import handler '#{name}' is registered twice (#{existing} and #{handler_class})" if existing && existing != handler_class
|
|
22
|
+
|
|
23
|
+
@handlers[name] = handler_class
|
|
24
|
+
@builtin[name] = handler_class if builtin_caller?(caller_locations(1, 1)&.first)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Register the built-in handlers again after `clear!` (they are only required once per process)
|
|
28
|
+
def restore_builtin!
|
|
29
|
+
@handlers = @builtin.merge(@handlers)
|
|
18
30
|
end
|
|
19
31
|
|
|
20
32
|
# Look up a handler class by name
|
|
@@ -70,8 +82,17 @@ module Archsight::Import::Registry
|
|
|
70
82
|
def clear!
|
|
71
83
|
@handlers = {}
|
|
72
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def builtin_caller?(location)
|
|
89
|
+
location && File.expand_path(location.path).start_with?("#{File.expand_path("handlers", __dir__)}/")
|
|
90
|
+
end
|
|
73
91
|
end
|
|
74
92
|
end
|
|
75
93
|
|
|
76
94
|
# Error raised when an unknown handler is requested
|
|
77
95
|
class Archsight::Import::UnknownHandlerError < StandardError; end
|
|
96
|
+
|
|
97
|
+
# Error raised when two handler classes register the same name
|
|
98
|
+
class Archsight::Import::DuplicateHandlerError < StandardError; end
|
data/lib/archsight/import.rb
CHANGED
|
@@ -5,10 +5,73 @@ module Archsight
|
|
|
5
5
|
module Import
|
|
6
6
|
module Handlers
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
# Where the git-based handlers clone repositories: the handler's own `cacheRoot` setting, else `import.cache_root`
|
|
10
|
+
# of the user configuration (or ARCHSIGHT_IMPORT_CACHE_ROOT), else ~/.cache/archsight/git
|
|
11
|
+
# @param override [String, nil] a handler's own setting
|
|
12
|
+
# @return [String] the directory as configured (a `~` stays, so generated child imports keep the portable form;
|
|
13
|
+
# expand it before use)
|
|
14
|
+
def self.cache_root(override = nil)
|
|
15
|
+
require "archsight/user_config"
|
|
16
|
+
override || Archsight::UserConfig.setting("import", "cache_root")&.to_s ||
|
|
17
|
+
File.join(Dir.home, ".cache", "archsight", "git")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The entries of `import/dependsOn`: import names and glob patterns
|
|
21
|
+
# @return [Array<String>]
|
|
22
|
+
def self.declared_dependencies(import)
|
|
23
|
+
import.annotations["import/dependsOn"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Absolute path of a file below the resources directory, nil when the path leaves it
|
|
27
|
+
def self.resources_path(resources_dir, rel_path)
|
|
28
|
+
root = File.expand_path(resources_dir)
|
|
29
|
+
path = File.expand_path(rel_path.to_s, root)
|
|
30
|
+
path.start_with?("#{root}/") ? path : nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The file an import writes its resources to, relative to the resources directory: `import/outputPath`, else
|
|
34
|
+
# generated/<name>.yaml (see Handler#write_yaml)
|
|
35
|
+
def self.output_path(import)
|
|
36
|
+
import.annotations["import/outputPath"] || File.join("generated", "#{import.name.gsub(/[^a-zA-Z0-9_-]/, "_")}.yaml")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The files an import wrote, relative to the resources directory: its output path and the `generated/outputs`
|
|
40
|
+
# of its last run. Tells which import a resource of a generated file comes from.
|
|
41
|
+
def self.output_files(import)
|
|
42
|
+
[output_path(import), *import.annotations["generated/outputs"].to_s.split(",").map(&:strip)].reject(&:empty?).uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Hash of the files `import/config/inputs` matches (glob patterns relative to the resources directory, comma
|
|
46
|
+
# separated): their paths, sizes and modification times. nil when the import declares no inputs.
|
|
47
|
+
# @return [String, nil]
|
|
48
|
+
def self.inputs_hash(import, resources_dir)
|
|
49
|
+
patterns = import.annotations["import/config/inputs"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
50
|
+
return nil if patterns.empty?
|
|
51
|
+
|
|
52
|
+
root = File.expand_path(resources_dir)
|
|
53
|
+
files = patterns.flat_map { |pattern| Dir.glob(pattern, base: root) }.uniq.sort
|
|
54
|
+
stats = files.filter_map do |file|
|
|
55
|
+
stat = File.stat(File.join(root, file))
|
|
56
|
+
"#{file}:#{stat.size}:#{stat.mtime.to_f}" if stat.file?
|
|
57
|
+
end
|
|
58
|
+
require "digest"
|
|
59
|
+
Digest::SHA256.hexdigest(stats.join("\n"))[0, 16]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Whether a `import/dependsOn` entry is a glob pattern (`Import:Repo:*`)
|
|
63
|
+
def self.pattern?(entry)
|
|
64
|
+
entry.match?(/[*?\[]/)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Whether an import name matches a `import/dependsOn` entry
|
|
68
|
+
def self.dependency_match?(entry, name)
|
|
69
|
+
pattern?(entry) ? File.fnmatch?(entry, name, File::FNM_EXTGLOB) : entry == name
|
|
70
|
+
end
|
|
8
71
|
end
|
|
9
72
|
end
|
|
10
73
|
|
|
11
74
|
require_relative "import/registry"
|
|
12
75
|
require_relative "import/handler"
|
|
13
76
|
require_relative "import/executor"
|
|
14
|
-
|
|
77
|
+
require_relative "import/handler_loader"
|