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,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
|
+
require "pathname"
|
|
6
7
|
require "yaml"
|
|
7
8
|
require "archsight/import"
|
|
8
9
|
require_relative "progress"
|
|
@@ -12,7 +13,14 @@ require_relative "progress"
|
|
|
12
13
|
# Subclasses must implement the #execute method to perform the actual import.
|
|
13
14
|
# Use the helper methods to read configuration, validate environment, and write output.
|
|
14
15
|
class Archsight::Import::Handler
|
|
15
|
-
attr_reader :import_resource, :database, :resources_dir, :progress, :shared_writer
|
|
16
|
+
attr_reader :import_resource, :database, :resources_dir, :progress, :shared_writer, :owned_outputs
|
|
17
|
+
|
|
18
|
+
# Whether the handler generates child imports (`import_yaml`). An import that depends on a pattern
|
|
19
|
+
# (`import/dependsOn: Import:Repo:*`) waits for such a handler on its first run, before it has a `generates` record
|
|
20
|
+
# that tells which imports it creates. Override in handlers that generate imports.
|
|
21
|
+
def self.generates_imports?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
16
24
|
|
|
17
25
|
# @param import_resource [Archsight::Resources::Import] The import resource to execute
|
|
18
26
|
# @param database [Archsight::Database] The database instance
|
|
@@ -26,6 +34,15 @@ class Archsight::Import::Handler
|
|
|
26
34
|
@progress = progress || Archsight::Import::Progress.new
|
|
27
35
|
@shared_writer = shared_writer
|
|
28
36
|
@tracked_resources = []
|
|
37
|
+
@owned_outputs = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The directory the git-based handlers clone repositories into (`import/config/cacheRoot`, else `import.cache_root`
|
|
41
|
+
# of the user configuration or ARCHSIGHT_IMPORT_CACHE_ROOT, else ~/.cache/archsight/git), so an importer of the
|
|
42
|
+
# resources repository finds the same clones
|
|
43
|
+
# @return [String] absolute path
|
|
44
|
+
def cache_root
|
|
45
|
+
File.expand_path(Archsight::Import.cache_root(config("cacheRoot")))
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
# Execute the import. Must be implemented by subclasses.
|
|
@@ -74,23 +91,55 @@ class Archsight::Import::Handler
|
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
# Generate a marker Import for this handler with generated/at timestamp and config hash
|
|
77
|
-
# Used for caching - call at end of execute() to persist the execution timestamp
|
|
94
|
+
# Used for caching - call at end of execute() to persist the execution timestamp. It also records the hash of the
|
|
95
|
+
# files in `import/config/inputs` and the files written with #write_file, so call it after writing them.
|
|
78
96
|
# @return [Hash] Import resource hash ready for YAML serialization
|
|
79
97
|
def self_marker
|
|
98
|
+
annotations = {
|
|
99
|
+
"generated/at" => Time.now.utc.iso8601,
|
|
100
|
+
"generated/configHash" => compute_config_hash
|
|
101
|
+
}
|
|
102
|
+
inputs_hash = Archsight::Import.inputs_hash(import_resource, resources_dir)
|
|
103
|
+
annotations["generated/inputsHash"] = inputs_hash if inputs_hash
|
|
104
|
+
annotations["generated/outputs"] = owned_outputs.uniq.sort.join(",") if owned_outputs.any?
|
|
80
105
|
{
|
|
81
106
|
"apiVersion" => "architecture/v1alpha1",
|
|
82
107
|
"kind" => "Import",
|
|
83
108
|
"metadata" => {
|
|
84
109
|
"name" => import_resource.name,
|
|
85
|
-
"annotations" =>
|
|
86
|
-
"generated/at" => Time.now.utc.iso8601,
|
|
87
|
-
"generated/configHash" => compute_config_hash
|
|
88
|
-
}
|
|
110
|
+
"annotations" => annotations
|
|
89
111
|
},
|
|
90
112
|
"spec" => {}
|
|
91
113
|
}
|
|
92
114
|
end
|
|
93
115
|
|
|
116
|
+
# Write a whole file that belongs to this import alone, such as a page (Markdown) next to the generated YAML.
|
|
117
|
+
# The file is recorded in the self marker (`generated/outputs`): the cache is invalid while it is missing, and a
|
|
118
|
+
# file of an earlier run that is not written again is deleted.
|
|
119
|
+
# @param rel_path [String] path relative to the resources directory
|
|
120
|
+
# @param content [String]
|
|
121
|
+
# @return [String] the absolute path
|
|
122
|
+
# @raise [ArgumentError] for a path outside the resources directory
|
|
123
|
+
def write_file(rel_path, content)
|
|
124
|
+
full_path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
125
|
+
raise ArgumentError, "#{rel_path} is outside the resources directory" unless full_path
|
|
126
|
+
|
|
127
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
128
|
+
File.write(full_path, content)
|
|
129
|
+
record_output(rel_path)
|
|
130
|
+
full_path
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Record a file this import wrote by other means (a command, a library) as its own output (see #write_file)
|
|
134
|
+
# @param rel_path [String] path relative to the resources directory
|
|
135
|
+
# @raise [ArgumentError] for a path outside the resources directory
|
|
136
|
+
def record_output(rel_path)
|
|
137
|
+
full_path = Archsight::Import.resources_path(resources_dir, rel_path)
|
|
138
|
+
raise ArgumentError, "#{rel_path} is outside the resources directory" unless full_path
|
|
139
|
+
|
|
140
|
+
@owned_outputs << Pathname.new(full_path).relative_path_from(File.expand_path(resources_dir)).to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
94
143
|
# Write YAML content to the output path
|
|
95
144
|
# @param content [String] YAML content to write
|
|
96
145
|
# @param filename [String, nil] Output filename (overrides import/outputPath filename)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "archsight/import"
|
|
4
|
+
|
|
5
|
+
# Loads import handlers: the built-in ones shipped with the gem, and the handlers of the resources repository that an
|
|
6
|
+
# Import declares with `import/handlerPath` (a Ruby file relative to the resources directory, like
|
|
7
|
+
# `import/outputPath`). Used by `archsight import`, `lint` and `web`, so every command knows the same handler names.
|
|
8
|
+
module Archsight::Import::HandlerLoader
|
|
9
|
+
BUILTIN_DIR = File.expand_path("handlers", __dir__)
|
|
10
|
+
|
|
11
|
+
# A problem with a local handler file, reported by the linter and by `archsight import`
|
|
12
|
+
Problem = Struct.new(:import, :message) do
|
|
13
|
+
def to_s
|
|
14
|
+
"#{import.path_ref}: Import '#{import.name}' #{message}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Require the built-in handlers. Idempotent; registers them again when a test cleared the registry.
|
|
20
|
+
def load_builtin!
|
|
21
|
+
Dir.glob(File.join(BUILTIN_DIR, "*.rb")).each { |file| require file }
|
|
22
|
+
Archsight::Import::Registry.restore_builtin!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Load the built-in handlers and every handler file the Imports of the database declare
|
|
26
|
+
# @param database [Archsight::Database]
|
|
27
|
+
# @param resources_dir [String] directory `import/handlerPath` is relative to
|
|
28
|
+
# @return [Array<Problem>] files that are missing, outside the resources directory, fail to load or do not register
|
|
29
|
+
# the handler named in `import/handler`
|
|
30
|
+
def load(database, resources_dir: database.path)
|
|
31
|
+
load_builtin!
|
|
32
|
+
imports = database.instances_by_kind("Import")&.values || []
|
|
33
|
+
imports.select { |imp| imp.annotations["import/handlerPath"] }.flat_map { |imp| load_for(imp, resources_dir) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Absolute path of the handler file of an Import, nil when it points outside the resources directory
|
|
37
|
+
def handler_file(import, resources_dir)
|
|
38
|
+
Archsight::Import.resources_path(resources_dir, import.annotations["import/handlerPath"])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def load_for(import, resources_dir)
|
|
44
|
+
file = handler_file(import, resources_dir)
|
|
45
|
+
return [Problem.new(import, "has import/handlerPath '#{import.annotations["import/handlerPath"]}' outside the resources directory")] unless file
|
|
46
|
+
return [Problem.new(import, "has import/handlerPath '#{import.annotations["import/handlerPath"]}' that does not exist")] unless File.file?(file)
|
|
47
|
+
|
|
48
|
+
problem = require_file(import, file)
|
|
49
|
+
return [problem] if problem
|
|
50
|
+
|
|
51
|
+
name = import.annotations["import/handler"]
|
|
52
|
+
return [] if name.nil? || Archsight::Import::Registry[name]
|
|
53
|
+
|
|
54
|
+
[Problem.new(import, "loads #{import.annotations["import/handlerPath"]}, which does not register the handler '#{name}'")]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def require_file(import, file)
|
|
58
|
+
require file
|
|
59
|
+
nil
|
|
60
|
+
rescue ScriptError, StandardError => e
|
|
61
|
+
Problem.new(import, "cannot load #{import.annotations["import/handlerPath"]}: #{e.class}: #{e.message.lines.first&.strip}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -6,18 +6,41 @@
|
|
|
6
6
|
# the current API listing, so a renamed, moved, or deleted repo doesn't
|
|
7
7
|
# leave a stale clone behind that a later importer could still resolve
|
|
8
8
|
# against under its old name.
|
|
9
|
+
#
|
|
10
|
+
# A listing can be incomplete (throttled or empty API pages) and clones may be
|
|
11
|
+
# in progress, so pruning is conservative: entries touched within MIN_AGE are
|
|
12
|
+
# kept, and nothing is removed when it would wipe out most of the cache.
|
|
9
13
|
module Archsight::Import::Handlers::CachePruner
|
|
14
|
+
MIN_AGE = 3600 # seconds; younger directories may be a clone in progress
|
|
15
|
+
MAX_STALE_ABSOLUTE = 10 # always allowed, whatever the cache size
|
|
16
|
+
MAX_STALE_FRACTION = 0.2 # larger share of the cache is treated as a bad listing
|
|
17
|
+
|
|
10
18
|
def prune_stale_cache_entries(target_dir, current_names)
|
|
11
19
|
return unless File.directory?(target_dir)
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
next unless File.directory?(stale_path)
|
|
21
|
+
known = current_names.to_set
|
|
22
|
+
dirs = Dir.children(target_dir).select { |entry| File.directory?(File.join(target_dir, entry)) }
|
|
23
|
+
stale = dirs.reject { |entry| known.include?(entry) }
|
|
24
|
+
return if stale.empty?
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
limit = [MAX_STALE_ABSOLUTE, (dirs.size * MAX_STALE_FRACTION).floor].max
|
|
27
|
+
if stale.size > limit
|
|
28
|
+
progress.warn("Not pruning #{stale.size} of #{dirs.size} cache entries in #{target_dir}: " \
|
|
29
|
+
"the project listing looks incomplete")
|
|
30
|
+
return
|
|
21
31
|
end
|
|
32
|
+
|
|
33
|
+
stale.each { |entry| prune_cache_entry(File.join(target_dir, entry), entry) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def prune_cache_entry(path, name)
|
|
39
|
+
return if Time.now - File.mtime(path) < MIN_AGE
|
|
40
|
+
|
|
41
|
+
progress.warn("Pruning stale cache entry: #{name} (repo renamed, moved, or removed)")
|
|
42
|
+
FileUtils.rm_rf(path)
|
|
43
|
+
rescue Errno::ENOENT
|
|
44
|
+
nil
|
|
22
45
|
end
|
|
23
46
|
end
|
|
@@ -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
|