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.
- checksums.yaml +4 -4
- data/docs/configuration.md +1 -1
- data/docs/modeling.md +39 -10
- data/docs/pages.md +2 -2
- data/lib/archsight/analysis/typst_renderer.rb +2 -3
- data/lib/archsight/export/confluence/analysis_output.rb +3 -4
- data/lib/archsight/export/confluence/exporter.rb +3 -1
- data/lib/archsight/export/confluence/page_properties.rb +2 -3
- data/lib/archsight/export/confluence/storage.rb +25 -12
- data/lib/archsight/export/confluence/tables.rb +2 -3
- data/lib/archsight/git_source.rb +81 -0
- data/lib/archsight/helpers/asset_images.rb +3 -4
- data/lib/archsight/import/executor.rb +1 -0
- data/lib/archsight/import.rb +27 -2
- data/lib/archsight/linter/structure.rb +4 -1
- data/lib/archsight/linter.rb +2 -2
- data/lib/archsight/markdown.rb +48 -0
- data/lib/archsight/openapi.rb +169 -0
- data/lib/archsight/page_tree.rb +2 -3
- data/lib/archsight/resources/application_component.rb +8 -0
- data/lib/archsight/resources/application_interface.rb +12 -0
- data/lib/archsight/resources/technology_communication_network.rb +2 -0
- data/lib/archsight/resources/technology_device.rb +56 -4
- data/lib/archsight/resources/technology_service.rb +7 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +2 -1
- data/lib/archsight/web/api/openapi/spec.yaml +136 -27
- data/lib/archsight/web/api/routes.rb +2 -2
- data/lib/archsight/web/application.rb +2 -2
- data/lib/archsight/web/public/vendor/redoc/LICENSE +22 -0
- data/lib/archsight/web/public/vendor/redoc/VERSION +1 -0
- data/lib/archsight/web/public/vendor/redoc/api.html +27 -0
- data/lib/archsight/web/public/vendor/redoc/api.js +19 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js +1851 -0
- data/lib/archsight/web/public/vendor/redoc/redoc.standalone.js.LICENSE.txt +102 -0
- data/lib/archsight/web/public/vue/{DocPage-D52OwaKc.js → DocPage-C8Ysway-.js} +1 -1
- data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +1 -0
- data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-Ca_C8izS.css} +1 -1
- data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +1 -0
- data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +3 -0
- data/lib/archsight/web/public/vue/architecture-WOLXFQ4H-BGN-TV5r.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-EF2NZ3EQ-CZd6g_tq.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-K75KTNOO-CYnbEx8O.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-VSP46ZUC-DialfQSk.js +1 -0
- data/lib/archsight/web/public/vue/graphviz-1lWBhpEX.js +3 -0
- data/lib/archsight/web/public/vue/{index-eUHmJ7K2.css → index-BLSE5DSv.css} +1 -1
- data/lib/archsight/web/public/vue/index-yiZbbvAs.js +3 -0
- data/lib/archsight/web/public/vue/info-OHQRW6UA-BuhOW7ss.js +1 -0
- data/lib/archsight/web/public/vue/katex-B_JcIwqj.js +257 -0
- data/lib/archsight/web/public/vue/mermaid-C3bOamfn.js +3999 -0
- data/lib/archsight/web/public/vue/packet-JDAUHWVQ-CUeRz7w4.js +1 -0
- data/lib/archsight/web/public/vue/pie-XZMESJXO-DnuewJI6.js +1 -0
- data/lib/archsight/web/public/vue/radar-ABXABTNO-Bi9l8bcv.js +1 -0
- data/lib/archsight/web/public/vue/railroad-I3PHUGI6-DSlxFbxk.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-LNEFI6M7-D6Kqu8wu.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-DWYD2UWJ-MR7uCYT0.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-JR7BVNR7-BL7aKIc-.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-COiEfhsA.js +1 -0
- data/lib/archsight/web/public/vue/treeView-D4JQ5SDB-CamtB04D.js +1 -0
- data/lib/archsight/web/public/vue/treemap-SAJKECNS-DeQxu_HM.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-4evusK9i.js → useGraphviz-Dnm2pkon.js} +2 -2
- data/lib/archsight/web/public/vue/useHighlight-BH32C7yt.js +10 -0
- data/lib/archsight/web/public/vue/usePanZoom-HxgEsqq2.js +12 -0
- data/lib/archsight/web/public/vue/wardley-7MLQ67FV-BvKIB6Tv.js +1 -0
- data/lib/archsight/web/public/vue.html +4 -4
- metadata +47 -40
- data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +0 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +0 -1
- data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +0 -1
- data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +0 -1
- data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +0 -3
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +0 -1
- data/lib/archsight/web/public/vue/graphviz-SXWTgZH5.js +0 -3
- data/lib/archsight/web/public/vue/index-BGlYCbi7.js +0 -3
- data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +0 -1
- data/lib/archsight/web/public/vue/katex-CXMH3UgJ.js +0 -257
- data/lib/archsight/web/public/vue/mermaid-DjsXQ_XN.js +0 -3334
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +0 -1
- data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +0 -1
- data/lib/archsight/web/public/vue/useHighlight-DPguaxfm.js +0 -10
- data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +0 -12
- 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
|
data/lib/archsight/page_tree.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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 =
|
|
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
|
|
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,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
|
data/lib/archsight/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
712
|
-
the resources it comes from (`by
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
File.read(spec_path)
|
|
224
|
+
Archsight::OpenAPI.document.to_yaml
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
# Convenience aliases with .json suffix
|