archsight 0.4.1 → 0.4.3
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/docs/diagram.md +64 -0
- data/docs/modeling.md +33 -10
- data/lib/archsight/cli.rb +2 -1
- data/lib/archsight/database.rb +4 -3
- data/lib/archsight/diagram/cli.rb +11 -1
- data/lib/archsight/diagram/errors.rb +1 -0
- data/lib/archsight/diagram/includes.rb +168 -0
- data/lib/archsight/diagram/parser/ast.rb +5 -0
- data/lib/archsight/diagram/parser/table_layout.rb +2 -0
- data/lib/archsight/diagram/parser.rb +58 -16
- data/lib/archsight/diagram.rb +20 -4
- data/lib/archsight/export/confluence/storage.rb +6 -1
- data/lib/archsight/helpers/diagram_blocks.rb +34 -19
- data/lib/archsight/helpers/diagram_includes.rb +67 -0
- data/lib/archsight/helpers.rb +1 -0
- data/lib/archsight/linter/structure.rb +4 -1
- data/lib/archsight/linter.rb +10 -2
- data/lib/archsight/references.rb +4 -3
- data/lib/archsight/resources/application_component.rb +18 -0
- data/lib/archsight/resources/application_service.rb +9 -0
- data/lib/archsight/resources/technology_device.rb +56 -4
- data/lib/archsight/resources/technology_service.rb +16 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -2
- data/lib/archsight/web/api/openapi/spec.yaml +9 -2
- data/lib/archsight/web/api/page_helpers.rb +1 -1
- data/lib/archsight/web/api/routes.rb +4 -3
- data/lib/archsight/web/application.rb +2 -1
- data/lib/archsight/web/public/vue/{DocPage-C2ubTXH7.js → DocPage-L0GzDFiV.js} +1 -1
- data/lib/archsight/web/public/vue/{EditorPage-Dso6PLMn.js → EditorPage-B3iNbh1u.js} +1 -1
- data/lib/archsight/web/public/vue/{ErrorPage-DnZ6YJLi.js → ErrorPage-Lro5vwhy.js} +1 -1
- data/lib/archsight/web/public/vue/{GraphView-CTl03Yre.js → GraphView-BRKwxa--.js} +1 -1
- data/lib/archsight/web/public/vue/{HomePage-C339nihQ.js → HomePage-DKgI1DXr.js} +2 -2
- data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-CJSkGdBG.css} +1 -1
- data/lib/archsight/web/public/vue/InstanceRouter-CX6qxVTm.js +1 -0
- data/lib/archsight/web/public/vue/{KindList-C0oaM3CF.js → KindList-C556gf23.js} +1 -1
- data/lib/archsight/web/public/vue/{PageView-OZB6G9Wu.js → PageView-DAqHjL1x.js} +1 -1
- data/lib/archsight/web/public/vue/{QueryError-C2xLkCEm.js → QueryError-DvspMrsh.js} +1 -1
- data/lib/archsight/web/public/vue/{ResourceList-DooWFwQP.js → ResourceList-BZOYa68X.js} +1 -1
- data/lib/archsight/web/public/vue/{SearchResults-BK-HjFnv.js → SearchResults-DaaOPG3t.js} +1 -1
- data/lib/archsight/web/public/vue/{WikiPage-B2FYmeHL.js → WikiPage-Bfx92Iig.js} +1 -1
- data/lib/archsight/web/public/vue/index-B_YGrWD8.css +1 -0
- data/lib/archsight/web/public/vue/{index-CLuLTE5I.js → index-DMhBow5t.js} +2 -2
- data/lib/archsight/web/public/vue.html +2 -2
- metadata +18 -16
- data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +0 -1
- data/lib/archsight/web/public/vue/index-BLSE5DSv.css +0 -1
data/lib/archsight/diagram.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "diagram/style/theme"
|
|
|
5
5
|
require_relative "diagram/parser/lexer"
|
|
6
6
|
require_relative "diagram/parser"
|
|
7
7
|
require_relative "diagram/parser/ast"
|
|
8
|
+
require_relative "diagram/includes"
|
|
8
9
|
require_relative "diagram/graph"
|
|
9
10
|
require_relative "diagram/resource_links"
|
|
10
11
|
require_relative "diagram/layout"
|
|
@@ -33,9 +34,14 @@ module Archsight
|
|
|
33
34
|
# `resolver` turns a node's `resource "..."` reference into a link (see
|
|
34
35
|
# `ResourceLinks`); `unresolved`, if an Array, collects the references it
|
|
35
36
|
# couldn't resolve.
|
|
37
|
+
# `includes` loads the diagrams that `include "<path>"` statements name (see `Includes`); without it
|
|
38
|
+
# a source that includes anything does not render. `warnings`, if an Array, collects what the includes left
|
|
39
|
+
# out (see `Includes`).
|
|
36
40
|
def self.render(source, relation_filter: Relations::DEFAULT_FILTER, profile: nil, style: nil, theme: nil, legend: nil, id_prefix: nil,
|
|
37
|
-
resolver: nil, unresolved: nil)
|
|
38
|
-
statements = time(profile, :parse)
|
|
41
|
+
resolver: nil, unresolved: nil, includes: nil, warnings: nil)
|
|
42
|
+
statements = time(profile, :parse) do
|
|
43
|
+
expand(source, includes: includes).tap { |expanded| warnings&.concat(expanded.warnings) }.statements
|
|
44
|
+
end
|
|
39
45
|
graph = time(profile, :graph) do
|
|
40
46
|
Graph.build(statements).tap { |g| ResourceLinks.apply(g, resolver, unresolved) }
|
|
41
47
|
end
|
|
@@ -47,8 +53,18 @@ module Archsight
|
|
|
47
53
|
# The `resource "..."` references of the nodes of a diagram source, in order and without duplicates. Parses and
|
|
48
54
|
# builds the graph but does not lay it out, so it is cheap enough to run over every diagram of a model.
|
|
49
55
|
# @raise [Archsight::Diagram::Error] if the source is not a valid diagram
|
|
50
|
-
def self.resource_references(source)
|
|
51
|
-
Graph.build(
|
|
56
|
+
def self.resource_references(source, includes: nil)
|
|
57
|
+
Graph.build(expand(source, includes: includes).statements).nodes_by_id.values.filter_map { |node| node.attrs["resource"] }.uniq
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Parses `source` and splices in the diagrams it includes.
|
|
61
|
+
# @return [Includes::Result] the statements, and the sources that were included (`{ path => source }`)
|
|
62
|
+
# @raise [Archsight::Diagram::Error] if the source or an included one is not a valid diagram
|
|
63
|
+
def self.expand(source, includes: nil)
|
|
64
|
+
statements = Parser.parse(source)
|
|
65
|
+
return Includes::Result.new(statements: statements, sources: {}, warnings: []) unless Includes.includes?(statements)
|
|
66
|
+
|
|
67
|
+
Includes.expand(statements, includes)
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def self.time(profile, key)
|
|
@@ -146,12 +146,17 @@ module Archsight
|
|
|
146
146
|
# A diagram from asd source (fenced block, .asd file) as block XML
|
|
147
147
|
def diagram(source, stem)
|
|
148
148
|
stem ||= "#{@page_name}-diagram-#{@diagrams += 1}"
|
|
149
|
-
figure(Archsight::Diagram.render(source, resolver: @diagram_links), stem)
|
|
149
|
+
figure(Archsight::Diagram.render(source, resolver: @diagram_links, includes: diagram_includes), stem)
|
|
150
150
|
rescue Archsight::Diagram::Error => e
|
|
151
151
|
problem("diagram #{stem}: #{e.message}")
|
|
152
152
|
""
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# `include "file://..."` / `archsight://...` in a diagram: the page's database, else only files of the resources directory
|
|
156
|
+
def diagram_includes
|
|
157
|
+
Helpers::DiagramIncludes.new(@database, resources_dir: @resources_dir)
|
|
158
|
+
end
|
|
159
|
+
|
|
155
160
|
# An image that must not sit inside a paragraph (a diagram), nil for ordinary images
|
|
156
161
|
def block_image(attr)
|
|
157
162
|
path = asset_path(attr["src"].to_s)
|
|
@@ -18,6 +18,7 @@ module Archsight
|
|
|
18
18
|
module DiagramBlocks
|
|
19
19
|
BLOCK = %r{<pre><code class="language-asd">(.*?)</code></pre>}m
|
|
20
20
|
CACHE_LIMIT = 256
|
|
21
|
+
INCLUDE = /\binclude\s+"/
|
|
21
22
|
|
|
22
23
|
@cache = {}
|
|
23
24
|
@cache_lock = Mutex.new
|
|
@@ -26,11 +27,11 @@ module Archsight
|
|
|
26
27
|
|
|
27
28
|
# @return [Array(String, Hash{String => String})] HTML with a placeholder per
|
|
28
29
|
# asd block, and the rendered replacement for each placeholder
|
|
29
|
-
def extract(html, resolver: nil)
|
|
30
|
+
def extract(html, resolver: nil, includes: nil)
|
|
30
31
|
diagrams = {}
|
|
31
32
|
replaced = html.gsub(BLOCK) do |block|
|
|
32
33
|
placeholder = "<!--asd-diagram:#{diagrams.size}-->"
|
|
33
|
-
diagrams[placeholder] = render_block(::CGI.unescapeHTML(Regexp.last_match(1)), block, resolver: resolver)
|
|
34
|
+
diagrams[placeholder] = render_block(::CGI.unescapeHTML(Regexp.last_match(1)), block, resolver: resolver, includes: includes)
|
|
34
35
|
placeholder
|
|
35
36
|
end
|
|
36
37
|
[replaced, diagrams]
|
|
@@ -44,9 +45,9 @@ module Archsight
|
|
|
44
45
|
|
|
45
46
|
# A whole diagram definition (the `architecture/diagram` annotation) as
|
|
46
47
|
# inline SVG, or the error box plus the source if it doesn't render.
|
|
47
|
-
def render_diagram(source, resolver: nil)
|
|
48
|
+
def render_diagram(source, resolver: nil, includes: nil)
|
|
48
49
|
escaped = ::Rack::Utils.escape_html(source)
|
|
49
|
-
render_block(source, %(<pre><code class="language-asd">#{escaped}</code></pre>), resolver: resolver)
|
|
50
|
+
render_block(source, %(<pre><code class="language-asd">#{escaped}</code></pre>), resolver: resolver, includes: includes)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
# A diagram for the editor preview: the same SVG (and cache) as a rendered page, but a diagram that
|
|
@@ -54,8 +55,8 @@ module Archsight
|
|
|
54
55
|
# that repeats the source, which the editor already shows.
|
|
55
56
|
#
|
|
56
57
|
# @return [Hash] `{ html: "<figure ...>", error: nil }` or `{ html: nil, error: "message" }`
|
|
57
|
-
def preview(source, resolver: nil)
|
|
58
|
-
{ html: render_svg(source, resolver: resolver), error: nil }
|
|
58
|
+
def preview(source, resolver: nil, includes: nil)
|
|
59
|
+
{ html: render_svg(source, resolver: resolver, includes: includes), error: nil }
|
|
59
60
|
rescue Archsight::Diagram::Error => e
|
|
60
61
|
{ html: nil, error: e.message }
|
|
61
62
|
end
|
|
@@ -73,40 +74,54 @@ module Archsight
|
|
|
73
74
|
# `resource "..."` references resolve against the database, which can
|
|
74
75
|
# change under a cached render (a reload, an edit), so the outcome of
|
|
75
76
|
# each reference is part of the key.
|
|
76
|
-
def render_block(source, original, resolver: nil)
|
|
77
|
-
render_svg(source, resolver: resolver)
|
|
77
|
+
def render_block(source, original, resolver: nil, includes: nil)
|
|
78
|
+
render_svg(source, resolver: resolver, includes: includes)
|
|
78
79
|
rescue Archsight::Diagram::Error => e
|
|
79
80
|
message = ::Rack::Utils.escape_html(e.message)
|
|
80
81
|
%(<div class="asd-diagram-error"><p><strong>Diagram error:</strong> #{message}</p>#{original}</div>)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# @raise [Archsight::Diagram::Error] if the source does not render
|
|
84
|
-
def render_svg(source, resolver: nil)
|
|
85
|
-
key = fingerprint(source, resolver: resolver)
|
|
85
|
+
def render_svg(source, resolver: nil, includes: nil)
|
|
86
|
+
key = fingerprint(source, resolver: resolver, includes: includes)
|
|
86
87
|
cached(key) do
|
|
87
|
-
|
|
88
|
+
warnings = [] #: Array[String]
|
|
89
|
+
svg = Archsight::Diagram.render(source, id_prefix: "asd#{key}", resolver: resolver, includes: includes, warnings: warnings)
|
|
90
|
+
warnings.each { |message| warn "Diagram: #{message}" }
|
|
88
91
|
%(<figure class="asd-diagram">#{svg.sub(/\A<\?xml[^>]*\?>\s*/, "")}</figure>)
|
|
89
92
|
end
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
# The cache key of a diagram: its source plus how each `resource` reference resolves right now. It
|
|
93
|
-
# changes exactly when the rendered diagram would, so it is also a good ETag.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
# changes exactly when the rendered diagram would, so it is also a good ETag. The diagrams it includes
|
|
97
|
+
# are part of it: their sources, and how their `resource` references resolve.
|
|
98
|
+
def fingerprint(source, resolver: nil, includes: nil)
|
|
99
|
+
included = included_sources(source, includes)
|
|
100
|
+
::Digest::SHA256.hexdigest([source, included, resolutions([source, *included.values], resolver)].inspect)[0, 12]
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
# A diagram as a standalone SVG document (what the assets API serves for an .asd file), cached by
|
|
99
104
|
# fingerprint like the inline diagrams.
|
|
100
105
|
# @raise [Archsight::Diagram::Error] if the source does not render
|
|
101
|
-
def standalone_svg(source, resolver: nil)
|
|
102
|
-
key = fingerprint(source, resolver: resolver)
|
|
103
|
-
cached("svg-#{key}") { Archsight::Diagram.render(source, id_prefix: "asd#{key}", resolver: resolver) }
|
|
106
|
+
def standalone_svg(source, resolver: nil, includes: nil)
|
|
107
|
+
key = fingerprint(source, resolver: resolver, includes: includes)
|
|
108
|
+
cached("svg-#{key}") { Archsight::Diagram.render(source, id_prefix: "asd#{key}", resolver: resolver, includes: includes) }
|
|
104
109
|
end
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
# `{ path => source }` of the diagrams `source` includes, recursively; empty for a source that includes
|
|
112
|
+
# none (checked on the text first: most diagrams include nothing and need not be parsed twice)
|
|
113
|
+
# @raise [Archsight::Diagram::Error] if the source or an included one is not a valid diagram
|
|
114
|
+
def included_sources(source, includes)
|
|
115
|
+
return {} unless includes && source.match?(INCLUDE)
|
|
116
|
+
|
|
117
|
+
Archsight::Diagram.expand(source, includes: includes).sources
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resolutions(sources, resolver)
|
|
107
121
|
return [] unless resolver
|
|
108
122
|
|
|
109
|
-
source.scan(/\bresource\s+"([^"]*)"/).flatten
|
|
123
|
+
references = sources.flat_map { |source| source.scan(/\bresource\s+"([^"]*)"/).flatten }
|
|
124
|
+
references.uniq.map { |reference| [reference, resolver.call(reference)] }
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
def cached(key)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../assets"
|
|
4
|
+
|
|
5
|
+
module Archsight
|
|
6
|
+
module Helpers
|
|
7
|
+
# Loads the diagrams that `include "<path>"` names (see Archsight::Diagram::Includes):
|
|
8
|
+
#
|
|
9
|
+
# archsight://Kind/name the `architecture/diagram` of that resource
|
|
10
|
+
# file://path/to/x.asd an .asd file, relative to the resources directory (never outside it)
|
|
11
|
+
#
|
|
12
|
+
# #call returns the diagram source, or `:missing` / `:forbidden` / `:invalid`. Without a database only
|
|
13
|
+
# `file://` paths work (the standalone `archsight diagram`).
|
|
14
|
+
class DiagramIncludes
|
|
15
|
+
ARCHSIGHT = "archsight://"
|
|
16
|
+
FILE = "file://"
|
|
17
|
+
ANNOTATION = "architecture/diagram"
|
|
18
|
+
MAX_BYTES = 1_000_000
|
|
19
|
+
|
|
20
|
+
# The loader for a database, whose path is the resources directory
|
|
21
|
+
def self.for(database)
|
|
22
|
+
new(database, resources_dir: (database.path if database.respond_to?(:path)))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(database, resources_dir:)
|
|
26
|
+
@database = database
|
|
27
|
+
@resources_dir = resources_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(path)
|
|
31
|
+
if path.start_with?(ARCHSIGHT)
|
|
32
|
+
resource(path.delete_prefix(ARCHSIGHT))
|
|
33
|
+
elsif path.start_with?(FILE)
|
|
34
|
+
file(path.delete_prefix(FILE))
|
|
35
|
+
else
|
|
36
|
+
:invalid
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# `Kind/name`; names contain `:` but a kind never contains `/`, so the first `/` separates them
|
|
43
|
+
def resource(reference)
|
|
44
|
+
return :forbidden unless @database
|
|
45
|
+
|
|
46
|
+
kind, name = reference.split("/", 2)
|
|
47
|
+
return :invalid if kind.to_s.empty? || name.to_s.empty? || !Archsight::Resources[kind]
|
|
48
|
+
|
|
49
|
+
source = @database.instance_by_kind(kind, name)&.annotations&.fetch(ANNOTATION, nil)
|
|
50
|
+
source.is_a?(String) && !source.strip.empty? ? source : :missing
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def file(path)
|
|
54
|
+
return :invalid if @resources_dir.nil? || !Archsight::Assets.asd?(path)
|
|
55
|
+
return :forbidden if path.start_with?("/") # file:///etc/x.asd; relative to the resources directory only
|
|
56
|
+
|
|
57
|
+
real = Archsight::Assets.file_for(path, resources_dir: @resources_dir)
|
|
58
|
+
return :missing unless real
|
|
59
|
+
return :forbidden if File.size(real) > MAX_BYTES
|
|
60
|
+
|
|
61
|
+
File.read(real, encoding: "UTF-8")
|
|
62
|
+
rescue SystemCallError
|
|
63
|
+
:missing
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/archsight/helpers.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "helpers/fenced_blocks"
|
|
|
7
7
|
require_relative "helpers/view_blocks"
|
|
8
8
|
require_relative "helpers/requirements_blocks"
|
|
9
9
|
require_relative "helpers/resource_resolver"
|
|
10
|
+
require_relative "helpers/diagram_includes"
|
|
10
11
|
require_relative "helpers/embeds"
|
|
11
12
|
require_relative "helpers/wiki_links"
|
|
12
13
|
require_relative "helpers/asset_images"
|
|
@@ -9,6 +9,8 @@ module Archsight
|
|
|
9
9
|
# Network types that run over another network and need one that serves them
|
|
10
10
|
OVERLAY_TYPES = %w[overlay vpn].freeze
|
|
11
11
|
NETWORK_DEVICE_TYPES = %w[switch router].freeze
|
|
12
|
+
# Client machines move: they need no room
|
|
13
|
+
CLIENT_DEVICE_TYPES = %w[laptop desktop workstation thin-client tablet mobile].freeze
|
|
12
14
|
|
|
13
15
|
def initialize(database)
|
|
14
16
|
@database = database
|
|
@@ -185,12 +187,13 @@ module Archsight
|
|
|
185
187
|
end
|
|
186
188
|
|
|
187
189
|
# A device that is part of equipment (a rack) or of another device (a virtual chassis) is placed through its
|
|
188
|
-
# whole, and a chassis through its members
|
|
190
|
+
# whole, and a chassis through its members. A client device (laptop, workstation, phone) is not placed.
|
|
189
191
|
def device_without_facility
|
|
190
192
|
parts = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
|
|
191
193
|
racked = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
|
|
192
194
|
instances("TechnologyDevice").filter_map do |device|
|
|
193
195
|
next if parts.key?(device) || racked.key?(device) || targets(device, :composedOf, :technologyDevices).any?
|
|
196
|
+
next if CLIENT_DEVICE_TYPES.include?(device.annotations["device/type"])
|
|
194
197
|
next if targets(device, :locatedAt, :technologyFacilities).any?
|
|
195
198
|
|
|
196
199
|
warning(device, "is located at no TechnologyFacility")
|
data/lib/archsight/linter.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Archsight
|
|
|
24
24
|
def initialize(database)
|
|
25
25
|
@database = database
|
|
26
26
|
@errors = []
|
|
27
|
+
@diagram_warnings = []
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def validate
|
|
@@ -47,7 +48,7 @@ module Archsight
|
|
|
47
48
|
# annotations their kind marks `deprecated:`. They are not errors, so they do not make `archsight lint` fail.
|
|
48
49
|
def warnings
|
|
49
50
|
renamed = @database.respond_to?(:deprecations) ? @database.deprecations.map(&:to_s) : []
|
|
50
|
-
renamed + deprecated_annotations
|
|
51
|
+
renamed + deprecated_annotations + @diagram_warnings
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
# Structural modeling checks (rules of "Choosing the Kind", see Linter::Structure). Warnings, never errors.
|
|
@@ -402,7 +403,10 @@ module Archsight
|
|
|
402
403
|
# Renders with the same resolver the web UI uses, so a `resource` reference that would show as a broken link there is reported here
|
|
403
404
|
def render_diagram_source(instance, where, source)
|
|
404
405
|
unresolved = []
|
|
405
|
-
|
|
406
|
+
warnings = []
|
|
407
|
+
Archsight::Diagram.render(source, resolver: Helpers::ResourceResolver.new(@database), unresolved: unresolved,
|
|
408
|
+
includes: diagram_includes, warnings: warnings)
|
|
409
|
+
warnings.each { |message| @diagram_warnings << "#{instance.path_ref}: Diagram in #{where}: #{message}" }
|
|
406
410
|
unresolved.each do |u|
|
|
407
411
|
@errors << "#{instance.path_ref}: Diagram resource link #{u[:reference].inspect} #{u[:reason]} in #{where} (node #{u[:node].inspect}, line #{u[:line]})"
|
|
408
412
|
end
|
|
@@ -410,6 +414,10 @@ module Archsight
|
|
|
410
414
|
@errors << "#{instance.path_ref}: Diagram error in #{where}: #{e.message}"
|
|
411
415
|
end
|
|
412
416
|
|
|
417
|
+
def diagram_includes
|
|
418
|
+
@diagram_includes ||= Helpers::DiagramIncludes.for(@database)
|
|
419
|
+
end
|
|
420
|
+
|
|
413
421
|
def validate_view_fields(instance)
|
|
414
422
|
check_view_components(instance, instance.annotations["view/fields"].to_s.split(",").map(&:strip))
|
|
415
423
|
end
|
data/lib/archsight/references.rb
CHANGED
|
@@ -26,9 +26,10 @@ module Archsight
|
|
|
26
26
|
# Adds the derived relations to every resource of the database.
|
|
27
27
|
def derive!(database)
|
|
28
28
|
finder = Finder.new(database)
|
|
29
|
+
includes = Helpers::DiagramIncludes.for(database)
|
|
29
30
|
database.instances.values.flat_map(&:values).each do |inst|
|
|
30
31
|
text_sources(inst).each { |markdown| mentions(markdown, finder).each { |target| link(inst, :mentions, target) } }
|
|
31
|
-
diagram_sources(inst, database).each { |source| depictions(source, finder).each { |target| link(inst, :depicts, target) } }
|
|
32
|
+
diagram_sources(inst, database).each { |source| depictions(source, finder, includes).each { |target| link(inst, :depicts, target) } }
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -44,8 +45,8 @@ module Archsight
|
|
|
44
45
|
# The resources the nodes of a diagram source name with `resource "..."`; a source that is not a valid diagram
|
|
45
46
|
# names none.
|
|
46
47
|
# @return [Array<Base>]
|
|
47
|
-
def depictions(source, finder)
|
|
48
|
-
Diagram.resource_references(source).filter_map { |reference| finder.reference(reference) }
|
|
48
|
+
def depictions(source, finder, includes = nil)
|
|
49
|
+
Diagram.resource_references(source, includes: includes).filter_map { |reference| finder.reference(reference) }
|
|
49
50
|
rescue Diagram::Error
|
|
50
51
|
[]
|
|
51
52
|
end
|
|
@@ -55,6 +55,13 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
55
55
|
works in queries like a tag but is not a sidebar filter, as there are too many distinct values. Deployable forms (container image, chart, package) are `TechnologyArtifact`s, not
|
|
56
56
|
component types.
|
|
57
57
|
|
|
58
|
+
## Teams
|
|
59
|
+
|
|
60
|
+
A component is `maintainedBy` the team (`BusinessActor`) that builds and changes it and `operatedBy` the team
|
|
61
|
+
(`BusinessActor` or `BusinessRole`) that runs it in production. Build and run are often different teams, which is
|
|
62
|
+
the point of writing both. Both are optional; without a direct `maintainedBy`, the build team is the one that
|
|
63
|
+
maintains the component's `TechnologyArtifact`s.
|
|
64
|
+
|
|
58
65
|
## Where it runs
|
|
59
66
|
|
|
60
67
|
A component is `servedBy` the `TechnologyService` it is deployed on (a product's cluster, a managed platform) and
|
|
@@ -62,6 +69,13 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
62
69
|
importers write the service per component, so a sidecar or agent deployed into many clusters shows where it runs
|
|
63
70
|
without making its own service look like it runs in all of them.
|
|
64
71
|
|
|
72
|
+
A component that creates technology services is `provisions` them: a controller, operator, pipeline or job that
|
|
73
|
+
builds and reconciles the clusters, database clusters or VM pools of a product (Crossplane, Cluster API, a
|
|
74
|
+
Terraform controller). It is written on the component, which points down to what it creates, like `servedBy`; the
|
|
75
|
+
service page lists the component as incoming "provisions". `servedBy` says where the component itself runs,
|
|
76
|
+
`provisions` what it builds, and a service that is built elsewhere keeps running while its provisioner is down,
|
|
77
|
+
only changes stop. The impact of a provisioner is what it reaches: its outgoing relations.
|
|
78
|
+
|
|
65
79
|
The computed `component/dependents` counts the components that depend on this one, which tells shared libraries
|
|
66
80
|
and modules from components nobody consumes.
|
|
67
81
|
|
|
@@ -418,6 +432,10 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
|
|
|
418
432
|
relation :realizedBy, :technologyComponents, :TechnologyNode
|
|
419
433
|
relation :servedBy, :technologyComponents, :TechnologySystemSoftware
|
|
420
434
|
relation :servedBy, :technologyServices, :TechnologyService
|
|
435
|
+
relation :provisions, :technologyServices, :TechnologyService
|
|
436
|
+
relation :maintainedBy, :businessActors, :BusinessActor
|
|
437
|
+
relation :operatedBy, :businessActors, :BusinessActor
|
|
438
|
+
relation :operatedBy, :businessRoles, :BusinessRole
|
|
421
439
|
relation :exposes, :applicationInterfaces, :ApplicationInterface
|
|
422
440
|
relation :dependsOn, :applicationInterfaces, :ApplicationInterface
|
|
423
441
|
relation :dependsOn, :applicationComponents, :ApplicationComponent
|
|
@@ -35,6 +35,12 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
|
|
|
35
35
|
the other service's components listed again; a service that many others use is modeled once. A service whose only component has the same name is a smell. See
|
|
36
36
|
the modeling guide, "Choosing the Kind".
|
|
37
37
|
|
|
38
|
+
## Teams
|
|
39
|
+
|
|
40
|
+
A service is `maintainedBy` the team (`BusinessActor`) that builds and changes it and `operatedBy` the team
|
|
41
|
+
(`BusinessActor` or `BusinessRole`) that runs it in production. Build and run are often different teams, which is
|
|
42
|
+
the point of writing both. `servedBy` stays "who uses it". Both are optional.
|
|
43
|
+
|
|
38
44
|
## Security and risk modelling
|
|
39
45
|
|
|
40
46
|
A service is an asset at risk (`asset/*` profile, protection needs) that loss events `affects`, a trigger target
|
|
@@ -230,6 +236,9 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
|
|
|
230
236
|
relation :dependsOn, :applicationServices, :ApplicationService
|
|
231
237
|
relation :servedBy, :businessActors, :BusinessActor
|
|
232
238
|
relation :servedBy, :technologyServices, :TechnologyService
|
|
239
|
+
relation :maintainedBy, :businessActors, :BusinessActor
|
|
240
|
+
relation :operatedBy, :businessActors, :BusinessActor
|
|
241
|
+
relation :operatedBy, :businessRoles, :BusinessRole
|
|
233
242
|
relation :realizes, :motivationConstraints, :MotivationConstraint
|
|
234
243
|
relation :realizes, :motivationRequirements, :MotivationRequirement
|
|
235
244
|
relation :partiallyRealizes, :motivationRequirements, :MotivationRequirement
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# TechnologyDevice represents one physical machine: a server, switch, router, firewall or
|
|
3
|
+
# TechnologyDevice represents one physical machine: a server, switch, router, firewall, appliance or client machine
|
|
4
4
|
class Archsight::Resources::TechnologyDevice < Archsight::Resources::Base
|
|
5
5
|
include_annotations :git, :architecture, :asset, :risk
|
|
6
6
|
|
|
7
7
|
description <<~MD
|
|
8
|
-
Represents one physical machine: a server, a switch, a router, a firewall, a terminal server
|
|
8
|
+
Represents one physical machine: a server, a switch, a router, a firewall, a terminal server, an appliance, or a
|
|
9
|
+
client machine (a laptop, a workstation, a phone).
|
|
9
10
|
|
|
10
11
|
## ArchiMate Definition
|
|
11
12
|
|
|
@@ -18,13 +19,34 @@ class Archsight::Resources::TechnologyDevice < Archsight::Resources::Base
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
20
21
|
Use TechnologyDevice when the individual machine matters: which room it stands in, which switch it is cabled to,
|
|
21
|
-
which machines a failure domain loses.
|
|
22
|
+
which machines a failure domain loses, which operating system and owner a workstation has.
|
|
22
23
|
|
|
23
24
|
- One element per physical machine (name it after the host: "Device:Frankfurt:Server01")
|
|
24
25
|
- A group of machines with one role ("the hypervisors of the platform") stays a logical `TechnologyNode` that is
|
|
25
26
|
`realizedBy` its devices
|
|
26
27
|
- A virtual chassis, a stack or a cluster pair is a device `composedOf` its member devices
|
|
27
28
|
|
|
29
|
+
## Client devices
|
|
30
|
+
|
|
31
|
+
A developer workstation, a laptop, a thin client, a tablet or a phone is a device of the matching `device/type`;
|
|
32
|
+
`device/os`, `device/osVersion` and `device/arch` say what it runs on, `device/ownership` whether the company
|
|
33
|
+
owns it, `device/managed` whether it is centrally managed (MDM) and `device/encryption` whether the disk is
|
|
34
|
+
encrypted. The vendor and product go into `device/model`, also for a hardened workstation that runs the user's
|
|
35
|
+
operating system virtualized next to other isolated sessions: it is a `workstation`. The end of support is
|
|
36
|
+
`lifecycle/endOfLifeAt`, a group of machines is a tag (`architecture/tags`).
|
|
37
|
+
|
|
38
|
+
- Model the individual machine when it is inventoried (an MDM import can write them), otherwise one
|
|
39
|
+
`TechnologyNode` per client platform ("Node:Workstation:macOS") `realizedBy` the devices
|
|
40
|
+
- The operating system is `TechnologySystemSoftware` `realizedBy` that node; the image or packages it is
|
|
41
|
+
installed from are `TechnologyArtifact`s the node is `assignedTo`. Each isolated session of a hardened
|
|
42
|
+
workstation can be a node of its own, `realizedBy` the device
|
|
43
|
+
- Network access: the platform node is `servedBy` the networks it uses (the office, the VPN), a single named
|
|
44
|
+
machine is `connects`-ed by its network (the VPN it tunnels through, an isolated segment). A machine that
|
|
45
|
+
reaches no network (air-gapped) has neither relation. The device itself does not point to networks: a network
|
|
46
|
+
is `realizedBy` its switches, so the reverse arrow would close a cycle
|
|
47
|
+
- Who uses it is the team or role (`servedBy`), not a person; a client device needs no room, `locatedAt` an
|
|
48
|
+
office is optional
|
|
49
|
+
|
|
28
50
|
## Not this kind
|
|
29
51
|
|
|
30
52
|
- A VM, a Kubernetes node or a host role is a `TechnologyNode`
|
|
@@ -57,7 +79,7 @@ class Archsight::Resources::TechnologyDevice < Archsight::Resources::Base
|
|
|
57
79
|
annotation "device/type",
|
|
58
80
|
description: "Kind of device",
|
|
59
81
|
title: "Device Type",
|
|
60
|
-
enum: %w[server switch router firewall terminal-server appliance other],
|
|
82
|
+
enum: %w[server switch router firewall terminal-server appliance laptop desktop workstation thin-client tablet mobile other],
|
|
61
83
|
filter: :word,
|
|
62
84
|
summary: true
|
|
63
85
|
annotation "device/role",
|
|
@@ -79,6 +101,36 @@ class Archsight::Resources::TechnologyDevice < Archsight::Resources::Base
|
|
|
79
101
|
title: "Purpose",
|
|
80
102
|
filter: :word
|
|
81
103
|
|
|
104
|
+
annotation "device/os",
|
|
105
|
+
description: "Operating system family of a client device",
|
|
106
|
+
title: "Operating System",
|
|
107
|
+
enum: %w[windows macos linux freebsd openbsd netbsd android ios chromeos other],
|
|
108
|
+
filter: :word,
|
|
109
|
+
summary: true
|
|
110
|
+
annotation "device/osVersion",
|
|
111
|
+
description: "Operating system version, e.g. 15.1 or 24.04 LTS",
|
|
112
|
+
title: "OS Version"
|
|
113
|
+
annotation "device/arch",
|
|
114
|
+
description: "CPU architecture",
|
|
115
|
+
title: "Architecture",
|
|
116
|
+
enum: %w[x86_64 arm64 riscv64 other],
|
|
117
|
+
filter: :word
|
|
118
|
+
annotation "device/ownership",
|
|
119
|
+
description: "Who owns the device",
|
|
120
|
+
title: "Ownership",
|
|
121
|
+
enum: %w[company-owned leased byod],
|
|
122
|
+
filter: :word
|
|
123
|
+
annotation "device/managed",
|
|
124
|
+
description: "Whether the device is managed centrally (MDM, configuration management)",
|
|
125
|
+
title: "Managed",
|
|
126
|
+
enum: %w[managed unmanaged],
|
|
127
|
+
filter: :word
|
|
128
|
+
annotation "device/encryption",
|
|
129
|
+
description: "Whether the disk is encrypted",
|
|
130
|
+
title: "Disk Encryption",
|
|
131
|
+
enum: %w[enabled disabled],
|
|
132
|
+
filter: :word
|
|
133
|
+
|
|
82
134
|
relation :locatedAt, :technologyFacilities, :TechnologyFacility
|
|
83
135
|
relation :connectedTo, :technologyDevices, :TechnologyDevice
|
|
84
136
|
relation :composedOf, :technologyDevices, :TechnologyDevice
|
|
@@ -50,6 +50,19 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
|
|
|
50
50
|
"servedBy" from ApplicationComponent on its page. An ApplicationService that is `servedBy` the service uses it as a
|
|
51
51
|
whole; list a cluster there only when the service's own components run in it.
|
|
52
52
|
|
|
53
|
+
## Teams
|
|
54
|
+
|
|
55
|
+
A service is `maintainedBy` the team (`BusinessActor`) that builds and changes it and `operatedBy` the team
|
|
56
|
+
(`BusinessActor` or `BusinessRole`) that runs it in production. Build and run are often different teams, which is
|
|
57
|
+
the point of writing both. `servedBy` stays "who uses it". Both are optional.
|
|
58
|
+
|
|
59
|
+
## Provisioning
|
|
60
|
+
|
|
61
|
+
A service that software creates (clusters built by an infrastructure-as-code operator, a database cluster
|
|
62
|
+
reconciled by a controller) is `provisions`-ed by that `ApplicationComponent`. The relation is written on the
|
|
63
|
+
component and points down to the service; the service page lists it as incoming "provisions". It says who builds
|
|
64
|
+
the service, `suppliedBy` says what the service is made of (the system software).
|
|
65
|
+
|
|
53
66
|
## Concept and implementation
|
|
54
67
|
|
|
55
68
|
A service can `specialize` another service (ArchiMate specialization): the general service is the concept, the
|
|
@@ -68,6 +81,9 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
|
|
|
68
81
|
relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
|
|
69
82
|
relation :specializes, :technologyServices, :TechnologyService
|
|
70
83
|
relation :servedBy, :businessActors, :BusinessActor
|
|
84
|
+
relation :maintainedBy, :businessActors, :BusinessActor
|
|
85
|
+
relation :operatedBy, :businessActors, :BusinessActor
|
|
86
|
+
relation :operatedBy, :businessRoles, :BusinessRole
|
|
71
87
|
relation :locatedAt, :technologyFacilities, :TechnologyFacility
|
|
72
88
|
relation :realizedBy, :technologyEquipment, :TechnologyEquipment
|
|
73
89
|
relation :realizes, :motivationRequirements, :MotivationRequirement
|
data/lib/archsight/version.rb
CHANGED
|
@@ -106,7 +106,8 @@ module Archsight::Web::API::JsonHelpers
|
|
|
106
106
|
source = annotations[DIAGRAM_ANNOTATION_KEY]
|
|
107
107
|
return unless source.is_a?(String) && !source.strip.empty?
|
|
108
108
|
|
|
109
|
-
Archsight::Helpers::DiagramBlocks.render_diagram(source, resolver: Archsight::Helpers::ResourceResolver.new(db)
|
|
109
|
+
Archsight::Helpers::DiagramBlocks.render_diagram(source, resolver: Archsight::Helpers::ResourceResolver.new(db),
|
|
110
|
+
includes: Archsight::Helpers::DiagramIncludes.for(db))
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
# The values of markdown annotations are rendered to HTML: architecture/description and every annotation the
|
|
@@ -190,7 +191,7 @@ module Archsight::Web::API::JsonHelpers
|
|
|
190
191
|
title: annotation.title,
|
|
191
192
|
description: annotation.description,
|
|
192
193
|
filter_type: annotation.filter.to_s,
|
|
193
|
-
values: values
|
|
194
|
+
values: values.map { |value, count| { value: value, count: count } }
|
|
194
195
|
}
|
|
195
196
|
end
|
|
196
197
|
end
|
|
@@ -1581,9 +1581,16 @@ components:
|
|
|
1581
1581
|
description: Filter type (e.g., "select", "boolean")
|
|
1582
1582
|
values:
|
|
1583
1583
|
type: array
|
|
1584
|
+
description: Available values with the number of instances using them, most used first
|
|
1584
1585
|
items:
|
|
1585
|
-
type:
|
|
1586
|
-
|
|
1586
|
+
type: object
|
|
1587
|
+
required: [value, count]
|
|
1588
|
+
properties:
|
|
1589
|
+
value:
|
|
1590
|
+
type: string
|
|
1591
|
+
count:
|
|
1592
|
+
type: integer
|
|
1593
|
+
description: Number of instances with this value
|
|
1587
1594
|
|
|
1588
1595
|
AnalysisResult:
|
|
1589
1596
|
type: object
|
|
@@ -24,7 +24,7 @@ module Archsight::Web::API::PageHelpers
|
|
|
24
24
|
next if counts.empty?
|
|
25
25
|
|
|
26
26
|
{ key: annotation.key, title: annotation.title, filter_type: annotation.filter.to_s,
|
|
27
|
-
values: counts.
|
|
27
|
+
values: counts.sort_by { |value, count| [-count, value.to_s] }.map { |value, count| { value: value, count: count } } }
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -179,9 +179,10 @@ module Archsight::Web::API::Routes
|
|
|
179
179
|
resolver = Archsight::Helpers::ResourceResolver.new(db)
|
|
180
180
|
headers "Content-Security-Policy" => "default-src 'none'; style-src 'unsafe-inline'; sandbox"
|
|
181
181
|
cache_control :public, :must_revalidate, max_age: 0
|
|
182
|
-
|
|
182
|
+
includes = Archsight::Helpers::DiagramIncludes.for(db)
|
|
183
|
+
etag Archsight::Helpers::DiagramBlocks.fingerprint(source, resolver: resolver, includes: includes)
|
|
183
184
|
content_type "image/svg+xml"
|
|
184
|
-
Archsight::Helpers::DiagramBlocks.standalone_svg(source, resolver: resolver)
|
|
185
|
+
Archsight::Helpers::DiagramBlocks.standalone_svg(source, resolver: resolver, includes: includes)
|
|
185
186
|
rescue Archsight::Diagram::Error => e
|
|
186
187
|
json_error("Diagram does not render: #{e.message}", status: 422, error_type: "DiagramError")
|
|
187
188
|
end
|
|
@@ -201,7 +202,7 @@ module Archsight::Web::API::Routes
|
|
|
201
202
|
json_error("A JSON body with a 'source' string is required", status: 400, error_type: "BadRequest") unless source.is_a?(String)
|
|
202
203
|
|
|
203
204
|
resolver = Archsight::Helpers::ResourceResolver.new(db)
|
|
204
|
-
json_response(Archsight::Helpers::DiagramBlocks.preview(source, resolver: resolver))
|
|
205
|
+
json_response(Archsight::Helpers::DiagramBlocks.preview(source, resolver: resolver, includes: Archsight::Helpers::DiagramIncludes.for(db)))
|
|
205
206
|
end
|
|
206
207
|
|
|
207
208
|
# POST /api/v1/kinds/Analysis/instances/:name/execute - Execute an analysis
|
|
@@ -161,7 +161,8 @@ class Archsight::Web::Application < Sinatra::Base
|
|
|
161
161
|
html = Archsight::Markdown.document(data).to_html
|
|
162
162
|
html = Archsight::Helpers::AssetImages.rewrite(html, base_dir: base, resources_dir: Archsight.resources_dir) if base
|
|
163
163
|
# ```asd blocks become placeholders until the text passes below are done (see DiagramBlocks)
|
|
164
|
-
html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db)
|
|
164
|
+
html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db),
|
|
165
|
+
includes: Archsight::Helpers::DiagramIncludes.for(db))
|
|
165
166
|
# so are ```view and ```requirements blocks (see ViewBlocks, RequirementsBlocks)
|
|
166
167
|
html, views = Archsight::Helpers::ViewBlocks.extract(html)
|
|
167
168
|
html, requirements = Archsight::Helpers::RequirementsBlocks.extract(html)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{F as e,L as t,P as n,Q as r,d as i,q as a,t as o,tt as s}from"./index-
|
|
1
|
+
import{F as e,L as t,P as n,Q as r,d as i,q as a,t as o,tt as s}from"./index-DMhBow5t.js";import{t as c}from"./WikiPage-Bfx92Iig.js";var l={key:1},u={__name:`DocPage`,props:{filename:String},setup(u){let d=u,f=s(null);function p(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function m(){let e=await i(d.filename);f.value=e?p(e,d.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},o(f.value.title)}return r(()=>d.filename,m,{immediate:!0}),(r,i)=>f.value?(a(),e(c,{key:0,title:f.value.title,html:f.value.html,breadcrumb:f.value.breadcrumb,toc:f.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(a(),t(`article`,l,[...i[0]||=[n(`p`,null,`Loading...`,-1)]]))}};export{u as default};
|