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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/docs/diagram.md +64 -0
  3. data/docs/modeling.md +33 -10
  4. data/lib/archsight/cli.rb +2 -1
  5. data/lib/archsight/database.rb +4 -3
  6. data/lib/archsight/diagram/cli.rb +11 -1
  7. data/lib/archsight/diagram/errors.rb +1 -0
  8. data/lib/archsight/diagram/includes.rb +168 -0
  9. data/lib/archsight/diagram/parser/ast.rb +5 -0
  10. data/lib/archsight/diagram/parser/table_layout.rb +2 -0
  11. data/lib/archsight/diagram/parser.rb +58 -16
  12. data/lib/archsight/diagram.rb +20 -4
  13. data/lib/archsight/export/confluence/storage.rb +6 -1
  14. data/lib/archsight/helpers/diagram_blocks.rb +34 -19
  15. data/lib/archsight/helpers/diagram_includes.rb +67 -0
  16. data/lib/archsight/helpers.rb +1 -0
  17. data/lib/archsight/linter/structure.rb +4 -1
  18. data/lib/archsight/linter.rb +10 -2
  19. data/lib/archsight/references.rb +4 -3
  20. data/lib/archsight/resources/application_component.rb +18 -0
  21. data/lib/archsight/resources/application_service.rb +9 -0
  22. data/lib/archsight/resources/technology_device.rb +56 -4
  23. data/lib/archsight/resources/technology_service.rb +16 -0
  24. data/lib/archsight/version.rb +1 -1
  25. data/lib/archsight/web/api/json_helpers.rb +3 -2
  26. data/lib/archsight/web/api/openapi/spec.yaml +9 -2
  27. data/lib/archsight/web/api/page_helpers.rb +1 -1
  28. data/lib/archsight/web/api/routes.rb +4 -3
  29. data/lib/archsight/web/application.rb +2 -1
  30. data/lib/archsight/web/public/vue/{DocPage-C2ubTXH7.js → DocPage-L0GzDFiV.js} +1 -1
  31. data/lib/archsight/web/public/vue/{EditorPage-Dso6PLMn.js → EditorPage-B3iNbh1u.js} +1 -1
  32. data/lib/archsight/web/public/vue/{ErrorPage-DnZ6YJLi.js → ErrorPage-Lro5vwhy.js} +1 -1
  33. data/lib/archsight/web/public/vue/{GraphView-CTl03Yre.js → GraphView-BRKwxa--.js} +1 -1
  34. data/lib/archsight/web/public/vue/{HomePage-C339nihQ.js → HomePage-DKgI1DXr.js} +2 -2
  35. data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-CJSkGdBG.css} +1 -1
  36. data/lib/archsight/web/public/vue/InstanceRouter-CX6qxVTm.js +1 -0
  37. data/lib/archsight/web/public/vue/{KindList-C0oaM3CF.js → KindList-C556gf23.js} +1 -1
  38. data/lib/archsight/web/public/vue/{PageView-OZB6G9Wu.js → PageView-DAqHjL1x.js} +1 -1
  39. data/lib/archsight/web/public/vue/{QueryError-C2xLkCEm.js → QueryError-DvspMrsh.js} +1 -1
  40. data/lib/archsight/web/public/vue/{ResourceList-DooWFwQP.js → ResourceList-BZOYa68X.js} +1 -1
  41. data/lib/archsight/web/public/vue/{SearchResults-BK-HjFnv.js → SearchResults-DaaOPG3t.js} +1 -1
  42. data/lib/archsight/web/public/vue/{WikiPage-B2FYmeHL.js → WikiPage-Bfx92Iig.js} +1 -1
  43. data/lib/archsight/web/public/vue/index-B_YGrWD8.css +1 -0
  44. data/lib/archsight/web/public/vue/{index-CLuLTE5I.js → index-DMhBow5t.js} +2 -2
  45. data/lib/archsight/web/public/vue.html +2 -2
  46. metadata +18 -16
  47. data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +0 -1
  48. data/lib/archsight/web/public/vue/index-BLSE5DSv.css +0 -1
@@ -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) { Parser.parse(source) }
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(Parser.parse(source)).nodes_by_id.values.filter_map { |node| node.attrs["resource"] }.uniq
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
- svg = Archsight::Diagram.render(source, id_prefix: "asd#{key}", resolver: resolver)
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
- def fingerprint(source, resolver: nil)
95
- ::Digest::SHA256.hexdigest([source, resolutions(source, resolver)].inspect)[0, 12]
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
- def resolutions(source, resolver)
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.uniq.map { |reference| [reference, resolver.call(reference)] }
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
@@ -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")
@@ -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
- Archsight::Diagram.render(source, resolver: Helpers::ResourceResolver.new(@database), unresolved: unresolved)
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
@@ -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 appliance
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 or an appliance.
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
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.4.1"
7
+ VERSION = "0.4.3"
8
8
  end
@@ -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: string
1586
- description: Available values for the filter
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.sort.map { |value, count| { value: value, count: count } } }
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
- etag Archsight::Helpers::DiagramBlocks.fingerprint(source, resolver: resolver)
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-CLuLTE5I.js";import{t as c}from"./WikiPage-B2FYmeHL.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};
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};