archsight 0.4.0 → 0.4.2

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/docs/configuration.md +1 -1
  3. data/docs/modeling.md +39 -10
  4. data/docs/pages.md +2 -2
  5. data/lib/archsight/analysis/typst_renderer.rb +2 -3
  6. data/lib/archsight/export/confluence/analysis_output.rb +3 -4
  7. data/lib/archsight/export/confluence/exporter.rb +3 -1
  8. data/lib/archsight/export/confluence/page_properties.rb +2 -3
  9. data/lib/archsight/export/confluence/storage.rb +25 -12
  10. data/lib/archsight/export/confluence/tables.rb +2 -3
  11. data/lib/archsight/git_source.rb +81 -0
  12. data/lib/archsight/helpers/asset_images.rb +3 -4
  13. data/lib/archsight/import/executor.rb +1 -0
  14. data/lib/archsight/import.rb +27 -2
  15. data/lib/archsight/linter/structure.rb +4 -1
  16. data/lib/archsight/linter.rb +2 -2
  17. data/lib/archsight/markdown.rb +48 -0
  18. data/lib/archsight/openapi.rb +169 -0
  19. data/lib/archsight/page_tree.rb +2 -3
  20. data/lib/archsight/resources/application_component.rb +8 -0
  21. data/lib/archsight/resources/application_interface.rb +12 -0
  22. data/lib/archsight/resources/technology_communication_network.rb +2 -0
  23. data/lib/archsight/resources/technology_device.rb +56 -4
  24. data/lib/archsight/resources/technology_service.rb +7 -0
  25. data/lib/archsight/version.rb +1 -1
  26. data/lib/archsight/web/api/docs.rb +2 -1
  27. data/lib/archsight/web/api/openapi/spec.yaml +136 -27
  28. data/lib/archsight/web/api/routes.rb +2 -2
  29. data/lib/archsight/web/application.rb +2 -2
  30. data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
  31. data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
  32. data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
  33. data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
  34. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
  35. data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
  36. data/lib/archsight/web/public/vue/{DocPage-D52OwaKc.js → DocPage-C8Ysway-.js} +1 -1
  37. data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +35 -0
  38. data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +2 -0
  39. data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +1 -0
  40. data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +2 -0
  41. data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +1 -0
  42. data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-Ca_C8izS.css} +1 -1
  43. data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +1 -0
  44. data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +1 -0
  45. data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +1 -0
  46. data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +2 -0
  47. data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +1 -0
  48. data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +3 -0
  49. data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
  50. data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
  51. data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
  52. data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
  53. data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
  54. data/lib/archsight/web/public/vue/{index-eUHmJ7K2.css → index-BLSE5DSv.css} +1 -1
  55. data/lib/archsight/web/public/vue/index-yiZbbvAs.js +3 -0
  56. data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
  57. data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
  58. data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
  59. data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
  60. data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
  61. data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
  62. data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
  63. data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
  64. data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
  65. data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
  66. data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
  67. data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
  68. data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
  69. data/lib/archsight/web/public/vue/{useGraphviz-4evusK9i.js → useGraphviz-Dnm2pkon.js} +2 -2
  70. data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
  71. data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
  72. data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
  73. data/lib/archsight/web/public/vue.html +4 -4
  74. metadata +47 -40
  75. data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +0 -1
  76. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
  77. data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +0 -35
  78. data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +0 -2
  79. data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +0 -1
  80. data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +0 -2
  81. data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +0 -1
  82. data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +0 -1
  83. data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +0 -1
  84. data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +0 -1
  85. data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +0 -2
  86. data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +0 -1
  87. data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +0 -3
  88. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +0 -1
  89. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +0 -1
  90. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +0 -1
  91. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +0 -1
  92. data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
  93. data/lib/archsight/web/public/vue/index-BGlYCbi7.js +0 -3
  94. data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +0 -1
  95. data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
  96. data/lib/archsight/web/public/vue/mermaid-DjsXQ_XN.js +0 -3334
  97. data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +0 -1
  98. data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +0 -1
  99. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +0 -1
  100. data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +0 -1
  101. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +0 -1
  102. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +0 -1
  103. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +0 -1
  104. data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +0 -1
  105. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +0 -1
  106. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +0 -1
  107. data/lib/archsight/web/public/vue/useHighlight-DPguaxfm.js +0 -10
  108. data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +0 -12
  109. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +0 -1
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require_relative "resources"
6
+
7
+ module Archsight
8
+ # The OpenAPI document of the REST API: the hand-written base (`web/api/openapi/spec.yaml`: the fixed endpoints such
9
+ # as pages, assets, search and the editor, with their prose, examples and error shapes) plus what the model knows and
10
+ # nobody should type twice: one schema per kind with its annotations (type, enum, description, default), its `spec`
11
+ # relations and its relations, the list of kinds, and the instance detail schema that is one of them. A new kind, a
12
+ # changed enum or another relation shows up in the documentation without touching the YAML.
13
+ #
14
+ # Archsight::OpenAPI.document # => Hash, `.to_yaml` is what GET /api/v1/openapi.yaml serves
15
+ module OpenAPI
16
+ BASE = File.join(__dir__, "web", "api", "openapi", "spec.yaml")
17
+
18
+ # The operations that answer with the detail of one instance, whose kind the path names
19
+ DETAIL_PATHS = ["/api/v1/kinds/{kind}/instances/{name}", "/kinds/{kind}/instances/{name}.json"].freeze
20
+
21
+ # What an annotation of a type is in the JSON. The value is the one of the YAML file, so a number may also be its
22
+ # string ("100"); only a computed annotation is always a number. Everything else is a string.
23
+ TYPES = {
24
+ Integer => { "type" => %w[integer string] },
25
+ Float => { "type" => %w[number string] }
26
+ }.freeze
27
+
28
+ NAMES = { "type" => "array", "items" => { "type" => "string" } }.freeze
29
+
30
+ class << self
31
+ # @return [Hash] the complete document; built once, the model does not change while the server runs
32
+ def document
33
+ @document ||= build
34
+ end
35
+
36
+ # Forget the built document (the model was changed, in a test)
37
+ def reset!
38
+ @document = nil
39
+ end
40
+
41
+ # The name of the schema of a kind
42
+ def schema_name(kind) = "#{kind}Instance"
43
+
44
+ # The schema of one kind: the detail of an instance as GET /api/v1/kinds/{kind}/instances/{name} returns it
45
+ # @param kind [String] name of a kind
46
+ # @return [Hash]
47
+ def kind_schema(kind)
48
+ klass = Archsight::Resources[kind]
49
+ schema = {
50
+ "type" => "object",
51
+ "required" => %w[kind name metadata spec relations references],
52
+ "x-layer" => klass.layer,
53
+ "properties" => {
54
+ "kind" => { "type" => "string", "const" => kind },
55
+ "name" => { "type" => "string", "description" => "Name of the instance" },
56
+ "metadata" => { "type" => "object", "properties" => { "annotations" => annotations_schema(klass) } },
57
+ "annotation_formats" => { "type" => "object", "additionalProperties" => { "type" => "string" },
58
+ "description" => "How the UI shows each annotation that is set (markdown, link, tag_list, ...)" },
59
+ "annotation_links" => { "type" => "object", "additionalProperties" => true,
60
+ "description" => "Annotations whose value names resources, with the resources they resolve to" },
61
+ "spec" => spec_schema(klass),
62
+ "relations" => relations_schema(klass),
63
+ "references" => references_schema,
64
+ "diagram" => { "type" => %w[string null], "description" => "The `architecture/diagram` annotation as inline SVG" }
65
+ }
66
+ }
67
+ schema["description"] = klass.description.strip if klass.description
68
+ schema
69
+ end
70
+
71
+ private
72
+
73
+ def build
74
+ document = YAML.safe_load_file(BASE, permitted_classes: [], aliases: true)
75
+ kinds = Archsight::Resources.resource_classes.keys.sort
76
+ schemas = document.fetch("components").fetch("schemas")
77
+ add_schemas(schemas, kinds)
78
+ document.dig("components", "parameters", "kind")["schema"] = { "$ref" => "#/components/schemas/KindName" }
79
+ DETAIL_PATHS.each { |path| use_detail_schema(document.fetch("paths").fetch(path)) }
80
+ # the schemas share small hashes: a copy of each keeps YAML from writing anchors and aliases for them
81
+ JSON.parse(JSON.generate(document))
82
+ end
83
+
84
+ def add_schemas(schemas, kinds)
85
+ kinds.each do |kind|
86
+ name = schema_name(kind)
87
+ raise ArgumentError, "OpenAPI: the schema #{name} of #{kind} clashes with a schema of the base spec" if schemas.key?(name)
88
+
89
+ schemas[name] = kind_schema(kind)
90
+ end
91
+ schemas["KindName"] = {
92
+ "type" => "string", "enum" => kinds,
93
+ "description" => "Name of a resource kind. A kind that was renamed is still accepted under its old name."
94
+ }
95
+ schemas["InstanceDetail"] = {
96
+ "description" => "An instance with its relations and references: the schema depends on the kind",
97
+ "oneOf" => kinds.map { |kind| { "$ref" => "#/components/schemas/#{schema_name(kind)}" } },
98
+ "discriminator" => { "propertyName" => "kind", "mapping" => kinds.to_h { |kind| [kind, "#/components/schemas/#{schema_name(kind)}"] } }
99
+ }
100
+ end
101
+
102
+ def use_detail_schema(path_item)
103
+ path_item.dig("get", "responses", "200", "content", "application/json")["schema"] = { "$ref" => "#/components/schemas/InstanceDetail" }
104
+ end
105
+
106
+ # The annotations of the kind that are set; every annotation is optional. Other annotations are allowed.
107
+ def annotations_schema(klass)
108
+ fixed, patterns = klass.annotations.partition { |annotation| !annotation.pattern? }
109
+ schema = {
110
+ "type" => "object", "additionalProperties" => true,
111
+ "properties" => fixed.to_h { |annotation| [annotation.key, annotation_schema(annotation)] }
112
+ }
113
+ schema["patternProperties"] = patterns.to_h { |annotation| [pattern_of(annotation.key), annotation_schema(annotation)] } unless patterns.empty?
114
+ schema
115
+ end
116
+
117
+ def annotation_schema(annotation)
118
+ schema = (TYPES[annotation.type] || { "type" => "string" }).dup
119
+ description = annotation.description.to_s.strip
120
+ description = "#{description}\n\nRendered to HTML in the response." if annotation.format == :markdown && !description.empty?
121
+ schema["description"] = description unless description.empty?
122
+ if annotation.enum && annotation.list?
123
+ # a list is one string of comma separated values, each of them one of the enum
124
+ schema["description"] = "#{description}\n\nComma separated list of: #{annotation.enum.join(", ")}.".strip
125
+ schema["x-items-enum"] = annotation.enum
126
+ elsif annotation.enum
127
+ schema["enum"] = annotation.enum
128
+ end
129
+ schema["default"] = annotation.default unless annotation.default.nil?
130
+ schema["deprecated"] = true if annotation.deprecated
131
+ schema
132
+ end
133
+
134
+ # `link/*` -> a regular expression for the keys it matches
135
+ def pattern_of(key) = "^#{Regexp.escape(key).gsub('\*', ".+")}$"
136
+
137
+ # What the file says: spec.<verb>.<key>: [names]
138
+ def spec_schema(klass)
139
+ properties = {}
140
+ klass.declared_relations.each do |verb, key, target|
141
+ verb_schema = (properties[verb.to_s] ||= { "type" => "object", "properties" => {} })
142
+ verb_schema["properties"][key.to_s] = NAMES.merge("description" => "Names of #{target} resources")
143
+ end
144
+ { "type" => "object", "additionalProperties" => true, "properties" => properties,
145
+ "description" => "The relations as written in the resource: verb, then the kind's collection, then the names" }
146
+ end
147
+
148
+ # The outgoing relations that exist: verb, then the kind of the target, then the names
149
+ def relations_schema(klass)
150
+ properties = {}
151
+ klass.relations.each do |verb, _key, target|
152
+ verb_schema = (properties[verb.to_s] ||= { "type" => "object" })
153
+ if Archsight::Resources::DERIVED_VERBS.include?(verb)
154
+ verb_schema["additionalProperties"] = NAMES # targets of any kind, grouped by the kind they are
155
+ else
156
+ (verb_schema["properties"] ||= {})[target.to_s] = NAMES
157
+ end
158
+ end
159
+ { "type" => "object", "additionalProperties" => false, "properties" => properties,
160
+ "description" => "Outgoing relations that have targets, grouped by verb and the kind of the target" }
161
+ end
162
+
163
+ def references_schema
164
+ { "type" => "object", "additionalProperties" => { "type" => "object", "additionalProperties" => NAMES },
165
+ "description" => "Incoming relations grouped by the kind of the source and the verb" }
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "kramdown"
4
- require "kramdown-parser-gfm"
3
+ require_relative "markdown"
5
4
 
6
5
  module Archsight
7
6
  # PageTree builds the sidebar tree, breadcrumbs and tables of contents for Page resources
@@ -63,7 +62,7 @@ module Archsight
63
62
 
64
63
  # Flat table of contents [{level:, id:, text:}] of a markdown body
65
64
  def self.toc(markdown)
66
- doc = Kramdown::Document.new(markdown.to_s, input: "GFM")
65
+ doc = Archsight::Markdown.document(markdown)
67
66
  entries = [] #: Array[Hash[String, untyped]]
68
67
  collect_toc(doc.to_toc, entries)
69
68
  entries
@@ -62,6 +62,13 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
62
62
  importers write the service per component, so a sidecar or agent deployed into many clusters shows where it runs
63
63
  without making its own service look like it runs in all of them.
64
64
 
65
+ A component that creates technology services is `provisions` them: a controller, operator, pipeline or job that
66
+ builds and reconciles the clusters, database clusters or VM pools of a product (Crossplane, Cluster API, a
67
+ Terraform controller). It is written on the component, which points down to what it creates, like `servedBy`; the
68
+ service page lists the component as incoming "provisions". `servedBy` says where the component itself runs,
69
+ `provisions` what it builds, and a service that is built elsewhere keeps running while its provisioner is down,
70
+ only changes stop. The impact of a provisioner is what it reaches: its outgoing relations.
71
+
65
72
  The computed `component/dependents` counts the components that depend on this one, which tells shared libraries
66
73
  and modules from components nobody consumes.
67
74
 
@@ -418,6 +425,7 @@ class Archsight::Resources::ApplicationComponent < Archsight::Resources::Base
418
425
  relation :realizedBy, :technologyComponents, :TechnologyNode
419
426
  relation :servedBy, :technologyComponents, :TechnologySystemSoftware
420
427
  relation :servedBy, :technologyServices, :TechnologyService
428
+ relation :provisions, :technologyServices, :TechnologyService
421
429
  relation :exposes, :applicationInterfaces, :ApplicationInterface
422
430
  relation :dependsOn, :applicationInterfaces, :ApplicationInterface
423
431
  relation :dependsOn, :applicationComponents, :ApplicationComponent
@@ -25,6 +25,17 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
25
25
  - gRPC services
26
26
  - Message queues (producer/consumer interfaces)
27
27
  - WebSocket endpoints
28
+
29
+ ## Reachability
30
+
31
+ An interface is `servedBy` the `TechnologyInterface` that carries it (a protocol, an authentication method) and
32
+ by the `TechnologyCommunicationNetwork`s on which it can be reached, just as a `TechnologyNode` attaches to the
33
+ networks it is on. Together with `api/authenticationMethod` this decides the exposure of the interface: an
34
+ interface without authentication on an internal network is a different risk than on a network that
35
+ internet-facing components attach to. The network page lists the interfaces as incoming "served by".
36
+
37
+ Write the networks only when they are known. An interface without a network makes no claim about where it can
38
+ be reached.
28
39
  MD
29
40
 
30
41
  icon "usb"
@@ -47,6 +58,7 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
47
58
  enum: ["none", "hard coded", "pbac", "abac", "rbac"]
48
59
 
49
60
  relation :servedBy, :technologyComponents, :TechnologyInterface
61
+ relation :servedBy, :technologyCommunicationNetworks, :TechnologyCommunicationNetwork
50
62
  relation :realizes, :motivationConstraints, :MotivationConstraint
51
63
  relation :serves, :dataObjects, :DataObject
52
64
  end
@@ -50,6 +50,8 @@ class Archsight::Resources::TechnologyCommunicationNetwork < Archsight::Resource
50
50
  - An overlay that software builds is `realizedBy` that `ApplicationComponent` or `ApplicationService` (an SDN agent,
51
51
  a network operator, a load-balancer controller), also when the software runs on the nodes the network serves
52
52
  - A `TechnologyNode` is `servedBy` the networks it is attached to; the network page lists them
53
+ - An `ApplicationInterface` is `servedBy` the networks on which it can be reached (only when that is known); the
54
+ network page lists the interfaces, which shows what a network exposes
53
55
  - A network is `realizedBy` the `TechnologyDevice`s that build it (its switches and routers) and `connects` the
54
56
  devices attached to it (servers, firewalls, appliances). Both are written on the network, so a switch that is
55
57
  also attached to the network it builds closes no cycle
@@ -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,13 @@ 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
+ ## Provisioning
54
+
55
+ A service that software creates (clusters built by an infrastructure-as-code operator, a database cluster
56
+ reconciled by a controller) is `provisions`-ed by that `ApplicationComponent`. The relation is written on the
57
+ component and points down to the service; the service page lists it as incoming "provisions". It says who builds
58
+ the service, `suppliedBy` says what the service is made of (the system software).
59
+
53
60
  ## Concept and implementation
54
61
 
55
62
  A service can `specialize` another service (ArchiMate specialization): the general service is the concept, the
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.4.0"
7
+ VERSION = "0.4.2"
8
8
  end
@@ -11,8 +11,9 @@ module Archsight::Web::API; end
11
11
  module Archsight::Web::API::Docs
12
12
  extend Sinatra::Extension
13
13
 
14
+ # A page of its own, not the single page app: ReDoc must not get the app's CSS (Pico restyles every <code>)
14
15
  get "/docs/api" do
15
- serve_vue
16
+ send_file File.join(settings.public_folder, "vendor", "redoc", "api.html"), type: :html
16
17
  end
17
18
 
18
19
  # GET /api/v1/docs/resources/:filename — auto-generated resource docs
@@ -623,12 +623,10 @@ paths:
623
623
  type: object
624
624
  properties:
625
625
  html:
626
- type: string
627
- nullable: true
626
+ type: [string, "null"]
628
627
  description: '`<figure class="asd-diagram">` with the SVG'
629
628
  error:
630
- type: string
631
- nullable: true
629
+ type: [string, "null"]
632
630
  '400':
633
631
  description: Body is not JSON with a `source` string
634
632
  content:
@@ -708,9 +706,11 @@ paths:
708
706
  summary: Requirements of a selection of resources
709
707
  description: |
710
708
  The requirements that the resources selected by `of` realize (`implemented`), partially realize
711
- (`partial`) or plan (`planned`), merged over all of them: one entry per requirement with the best status and
712
- the resources it comes from (`by`). Sorted by priority (must, should, may, none), then name. `story` is
713
- rendered HTML.
709
+ (`partial`) or plan (`planned`), merged over all of them: one entry per requirement, with the best status
710
+ any of the resources gives it and the resources it comes from (`by`, each with its own status). Sorted by
711
+ priority (must, should, may, requirements without a priority last), then name. `story` is rendered HTML.
712
+ `priority` and `status` filter the merged entries, so `status=partial` lists requirements whose best
713
+ status is partial.
714
714
  operationId: getRequirements
715
715
  tags:
716
716
  - Search
@@ -725,14 +725,14 @@ paths:
725
725
  - name: priority
726
726
  in: query
727
727
  required: false
728
- description: Comma separated list, only requirements with one of these priorities
728
+ description: Comma separated list, only requirements with one of these priorities (must, should, may)
729
729
  schema:
730
730
  type: string
731
731
  example: must,should
732
732
  - name: status
733
733
  in: query
734
734
  required: false
735
- description: Comma separated list, only requirements with one of these statuses
735
+ description: Comma separated list, only requirements with one of these statuses (implemented, partial, planned)
736
736
  schema:
737
737
  type: string
738
738
  example: implemented,partial
@@ -743,33 +743,42 @@ paths:
743
743
  application/json:
744
744
  schema:
745
745
  type: object
746
+ required: [query, total, query_time_ms, requirements]
746
747
  properties:
747
748
  query:
748
749
  type: string
750
+ description: The `of` query as given
749
751
  total:
750
752
  type: integer
753
+ description: Number of requirements returned
751
754
  query_time_ms:
752
755
  type: number
753
756
  requirements:
754
757
  type: array
755
758
  items:
756
759
  type: object
760
+ required: [name, status, priority, story, by]
757
761
  properties:
758
762
  name:
759
763
  type: string
764
+ description: Name of the MotivationRequirement
760
765
  status:
761
766
  type: string
762
767
  enum: [implemented, partial, planned]
768
+ description: The best status among the resources in `by`
763
769
  priority:
764
- type: string
765
- nullable: true
770
+ type: [string, "null"]
771
+ enum: [must, should, may, null]
772
+ description: Priority of the requirement, null if it has none
766
773
  story:
767
- type: string
768
- nullable: true
774
+ type: [string, "null"]
775
+ description: The requirement's story as rendered HTML, null if it has none
769
776
  by:
770
777
  type: array
778
+ description: The selected resources that realize, partially realize or plan the requirement, best status first
771
779
  items:
772
780
  type: object
781
+ required: [kind, name, status]
773
782
  properties:
774
783
  kind:
775
784
  type: string
@@ -778,12 +787,118 @@ paths:
778
787
  status:
779
788
  type: string
780
789
  enum: [implemented, partial, planned]
790
+ description: The status this resource gives the requirement
791
+ example:
792
+ query: 'ApplicationService: name =~ "Backup"'
793
+ total: 1
794
+ query_time_ms: 1.4
795
+ requirements:
796
+ - name: "Requirement:Restore"
797
+ status: partial
798
+ priority: must
799
+ story: "<p>As an operator I can restore a backup.</p>"
800
+ by:
801
+ - kind: ApplicationService
802
+ name: Backup
803
+ status: partial
781
804
  '400':
782
- description: Invalid query, or an unknown priority or status
805
+ description: |
806
+ `of` is missing or `priority` or `status` holds an unknown value (`BadRequest`), or `of` is not a valid
807
+ query (`QueryError`, which also returns the query)
783
808
  content:
784
809
  application/json:
785
810
  schema:
786
- $ref: '#/components/schemas/QueryError'
811
+ oneOf:
812
+ - $ref: '#/components/schemas/Error'
813
+ - $ref: '#/components/schemas/QueryError'
814
+
815
+ /api/v1/openapi.yaml:
816
+ get:
817
+ summary: This OpenAPI document
818
+ description: |
819
+ The OpenAPI 3.1 description of this API. The fixed endpoints are written by hand; the schema of every kind
820
+ (its annotations with type, enum and description, its `spec` and its relations), the list of kinds and the
821
+ instance detail are generated from the model, so they always describe the kinds this server has.
822
+ operationId: getOpenApi
823
+ tags:
824
+ - Operations
825
+ responses:
826
+ '200':
827
+ description: The OpenAPI document
828
+ content:
829
+ text/yaml:
830
+ schema:
831
+ type: string
832
+
833
+ /maintenance/restart:
834
+ post:
835
+ summary: Restart the server
836
+ description: |
837
+ Stops the server so that its supervisor (for example a Kubernetes Deployment) starts it again with fresh
838
+ data. Only available when the server was started with `--enable-restart` (or
839
+ `ARCHSIGHT_RESTART_ENABLED=true`); otherwise it answers 404. When a restart token is configured it must be
840
+ sent in the `X-Restart-Token` header.
841
+ operationId: restart
842
+ tags:
843
+ - Operations
844
+ parameters:
845
+ - name: X-Restart-Token
846
+ in: header
847
+ required: false
848
+ description: The configured restart token (required when the server has one)
849
+ schema:
850
+ type: string
851
+ responses:
852
+ '200':
853
+ description: The server shuts down
854
+ content:
855
+ application/json:
856
+ schema:
857
+ type: object
858
+ required: [ok, message]
859
+ properties:
860
+ ok:
861
+ type: boolean
862
+ message:
863
+ type: string
864
+ example:
865
+ ok: true
866
+ message: Server shutting down
867
+ '401':
868
+ description: The restart token is missing or wrong
869
+ content:
870
+ text/html:
871
+ schema:
872
+ type: string
873
+ '404':
874
+ description: The restart endpoint is not enabled
875
+ content:
876
+ text/html:
877
+ schema:
878
+ type: string
879
+
880
+ /kinds/{kind}/dot:
881
+ get:
882
+ summary: Get the graph of a kind as DOT
883
+ description: The GraphViz DOT graph of the resources of a kind and the relations between them.
884
+ operationId: getKindDot
885
+ tags:
886
+ - Graph
887
+ parameters:
888
+ - $ref: '#/components/parameters/kind'
889
+ responses:
890
+ '200':
891
+ description: GraphViz DOT source
892
+ content:
893
+ text/plain:
894
+ schema:
895
+ type: string
896
+ '404':
897
+ description: Unknown kind
898
+ content:
899
+ text/plain:
900
+ schema:
901
+ type: string
787
902
 
788
903
  /dot:
789
904
  get:
@@ -1343,13 +1458,11 @@ components:
1343
1458
  items:
1344
1459
  type: string
1345
1460
  format:
1346
- type: string
1347
- nullable: true
1461
+ type: [string, "null"]
1348
1462
  description: Rendering hint of the annotation (tag_word, tag_list, markdown, link, ...)
1349
1463
  example: tag_word
1350
1464
  type:
1351
- type: string
1352
- nullable: true
1465
+ type: [string, "null"]
1353
1466
  description: Short name of the value type when the annotation declares one (Integer, Float, Time, Person, ...)
1354
1467
  example: Integer
1355
1468
 
@@ -1684,26 +1797,22 @@ components:
1684
1797
  type: object
1685
1798
  properties:
1686
1799
  yaml:
1687
- type: string
1688
- nullable: true
1800
+ type: [string, "null"]
1689
1801
  description: Generated source text, YAML or (for pages) markdown with frontmatter (null if validation failed)
1690
1802
  format:
1691
1803
  type: string
1692
1804
  enum: [yaml, markdown]
1693
1805
  description: Format of `yaml`. Pages (kind Page) are stored as markdown files and replaced as a whole file
1694
1806
  errors:
1695
- type: array
1696
- nullable: true
1807
+ type: [array, "null"]
1697
1808
  items:
1698
1809
  type: string
1699
1810
  description: Validation errors (null if valid)
1700
1811
  path_ref:
1701
- type: string
1702
- nullable: true
1812
+ type: [string, "null"]
1703
1813
  description: "File path reference (edit mode only, e.g., path/file.yaml:5; just the file for markdown pages)"
1704
1814
  content_hash:
1705
- type: string
1706
- nullable: true
1815
+ type: [string, "null"]
1707
1816
  description: Content hash (edit mode only, for optimistic locking)
1708
1817
 
1709
1818
  SaveRequest:
@@ -7,6 +7,7 @@ require_relative "page_helpers"
7
7
  require_relative "requirements_helpers"
8
8
  require_relative "analysis_helpers"
9
9
  require_relative "../../assets"
10
+ require_relative "../../openapi"
10
11
 
11
12
  module Archsight; end
12
13
  module Archsight::Web; end
@@ -220,8 +221,7 @@ module Archsight::Web::API::Routes
220
221
  # GET /api/v1/openapi.yaml - OpenAPI specification
221
222
  get "/api/v1/openapi.yaml" do
222
223
  content_type "text/yaml"
223
- spec_path = File.join(__dir__, "openapi", "spec.yaml")
224
- File.read(spec_path)
224
+ Archsight::OpenAPI.document.to_yaml
225
225
  end
226
226
 
227
227
  # Convenience aliases with .json suffix