├── .github └── workflows │ └── docs-pipeline.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── helm └── inventory-service │ ├── templates │ └── prometheus-rules.yaml │ └── values.yml ├── settings.gradle.kts └── src ├── docs ├── kotlin │ ├── GenerateDiagramsAndDocs.kt │ └── model │ │ ├── Components.kt │ │ ├── Containers.kt │ │ ├── Deployment.kt │ │ ├── ExternalTeams.kt │ │ ├── InventoryWorkspace.kt │ │ ├── Systems.kt │ │ └── utils │ │ ├── HelmAlertUtils.kt │ │ ├── HelmValuesUtils.kt │ │ └── KafkaTopicsUtils.kt └── resources │ ├── _01_introduction_and_goals.adoc │ ├── _02_architecture_constraints.adoc │ ├── _03_system_scope_and_context.adoc │ ├── _04_solution_strategy.adoc │ ├── _05_building_block_view.adoc │ ├── _06_runtime_view.adoc │ ├── _07_deployment_view.adoc │ ├── _08_concepts.adoc │ ├── _09_architecture_decisions.adoc │ ├── _10_quality_requirements.adoc │ ├── _11_technical_risks.adoc │ ├── _12_glossary.adoc │ └── index.adoc └── main ├── kotlin └── example │ └── webshop │ └── inventory │ └── Components.kt └── resources └── application.yml /.github/workflows/docs-pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Docs pipeline 2 | 3 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 4 | permissions: 5 | contents: read 6 | pages: write 7 | id-token: write 8 | 9 | # Allow one concurrent deployment 10 | concurrency: 11 | group: "docs_pages" 12 | cancel-in-progress: true 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | 18 | jobs: 19 | docs_pages: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout Repo 26 | uses: actions/checkout@v3 27 | # We need to install GraphViz to convert PlantUml files to images 28 | - name: Setup Graphviz 29 | uses: ts-graphviz/setup-graphviz@v1 30 | # Run Asciidoctor 31 | - name: Asciidoctor 32 | uses: gradle/gradle-build-action@v2 33 | with: 34 | arguments: asciidoctor 35 | # Upload the HTML docs generated with Asciidoctor 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v1 38 | with: 39 | path: ./build/docs 40 | # Deploy the uploaded docs using GitHub Pages action 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v1 44 | docs_confluence: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout Repo 48 | uses: actions/checkout@v3 49 | # We need to install GraphViz to convert PlantUml files to images 50 | - name: Setup Graphviz 51 | uses: ts-graphviz/setup-graphviz@v1 52 | # Run publishToConfluence task which runs Asciidoctor and publishes the resulting file to confluence 53 | - name: Publish To Confluence 54 | uses: gradle/gradle-build-action@v2 55 | # Use confluence token provided as project secret 56 | env: 57 | CONFLUENCE_TOKEN: ${{secrets.CONFLUENCE_TOKEN}} 58 | with: 59 | arguments: publishToConfluence -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .idea 3 | .gradle 4 | /gradle.properties 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christoph Knauf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arch-docs-as-code-example 2 | 3 | Example Project demonstrating how to do docs as code for architecture documents using Asciidoctor and Structurizr. The documentation created with this repository can be found on [GitHub Pages](https://chriskn.github.io/arch-docs-as-code-example) 4 | 5 | For more information see my blog article series about the topic: 6 | 7 | * [Part 1: Workflow and tooling](https://blog.codecentric.de/architecture-documentation-docs-as-code-structurizr-asciidoctor) 8 | * [Part 2: Asciidoctor](https://blog.codecentric.de/architecture-documentation-as-code-with-structurizr-and-asciidoctor-part-2-asciidoctor) 9 | * [Part 3: Structurizr](https://blog.codecentric.de/architecture-documentation-as-code-with-structurizr-and-asciidoctor-part-3-structurizr) 10 | * [Part 4: Publishing](https://blog.codecentric.de/architecture-documentation-as-code-with-structurizr-and-asciidoctor-part4-publishing) 11 | * [Part 5: Generating documentation](https://blog.codecentric.de/architecture-docs-as-code-structurizr-asciidoctor-part-5-generating-documentation) 12 | 13 | ## Build documentation 14 | Make sure you have Graphviz and Kotlin installed. 15 | 16 | Afterwards, clone the project and run 17 | 18 | `gradlew asciidoctor` 19 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import ch.nomisp.confluence.publisher.PublishToConfluenceTask 2 | import org.asciidoctor.gradle.editorconfig.AsciidoctorEditorConfigGenerator 3 | import org.asciidoctor.gradle.jvm.AsciidoctorTask 4 | import org.springframework.boot.gradle.tasks.bundling.BootJar 5 | import java.text.SimpleDateFormat 6 | import java.util.Date 7 | 8 | plugins { 9 | kotlin("jvm") version "1.7.10" 10 | id("org.springframework.boot") version "2.7.5" 11 | id("io.spring.dependency-management") version "1.0.15.RELEASE" 12 | 13 | id("org.asciidoctor.jvm.convert") version "4.0.0" 14 | id("org.asciidoctor.editorconfig") version "4.0.0" 15 | id("ch.nomisp.confluence.publisher") version "0.2.0" 16 | } 17 | 18 | group = "com.github.chriskn" 19 | version = "1.0-SNAPSHOT" 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | tasks.getByName("bootJar") { 26 | enabled = false 27 | } 28 | 29 | sourceSets { 30 | create("docs") { 31 | kotlin { 32 | compileClasspath += main.get().output 33 | runtimeClasspath += output + compileClasspath 34 | } 35 | } 36 | val docs by getting { 37 | dependencies { 38 | "docsImplementation"("io.github.chriskn:structurizr-c4puml-extension:0.8.0") 39 | "docsImplementation"("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.1") 40 | "docsImplementation"("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1") 41 | "docsImplementation"("com.structurizr:structurizr-analysis:1.3.5") 42 | "docsImplementation"("com.structurizr:structurizr-spring:1.3.5") 43 | // make spring annotations available to use it with AnnotationTypeMatcher 44 | "docsImplementation"("org.springframework.boot:spring-boot-starter") 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation("org.springframework.boot:spring-boot-starter") 51 | implementation("com.structurizr:structurizr-annotations:1.3.5") 52 | } 53 | 54 | tasks { 55 | compileKotlin { 56 | kotlinOptions.jvmTarget = "11" 57 | } 58 | compileTestKotlin { 59 | kotlinOptions.jvmTarget = "11" 60 | } 61 | } 62 | 63 | // Asciidoctor 64 | 65 | val asciiAttributes = mapOf( 66 | "imagesdir" to ".", 67 | "plantUmlDir" to "./plantuml", 68 | "toc" to "left", 69 | "toclevels" to 3, 70 | "max-width" to "100%", 71 | "projectName" to rootProject.name, 72 | "dateTime" to SimpleDateFormat("dd-MM-yyyy HH:mm:ssZ").format(Date()) 73 | ) 74 | 75 | tasks.withType(AsciidoctorTask::class) { 76 | setSourceDir(file("./src/docs/resources")) 77 | setBaseDir(file("./src/docs/resources")) 78 | setOutputDir(file("build/docs")) 79 | attributes(asciiAttributes) 80 | options(mapOf("doctype" to "book")) 81 | isLogDocuments = true 82 | dependsOn("generateDiagramsAndDocs") 83 | } 84 | 85 | tasks.withType(AsciidoctorEditorConfigGenerator::class) { 86 | setAttributes(asciiAttributes) 87 | setDestinationDir("./src/docs/resources") 88 | group = "documentation" 89 | } 90 | 91 | tasks.named("processDocsResources") { 92 | dependsOn("asciidoctorEditorConfig") 93 | } 94 | 95 | // Diagrams 96 | 97 | tasks.register("generateDiagramsAndDocs", JavaExec::class) { 98 | classpath += sourceSets["docs"].runtimeClasspath 99 | mainClass.set("docsascode.GenerateDiagramsAndDocsKt") 100 | group = "documentation" 101 | } 102 | 103 | asciidoctorj { 104 | modules { 105 | diagram.use() 106 | diagram.setVersion("2.2.1") 107 | } 108 | } 109 | 110 | // Confluence publisher 111 | 112 | confluencePublisher { 113 | asciiDocRootFolder.set(tasks.asciidoctor.get().sourceDir) 114 | setAttributes(tasks.asciidoctor.get().attributes) 115 | rootConfluenceUrl.set("YOUR_CONFLUENCE_URL") 116 | spaceKey.set("YOUR_SPACE_KEY") 117 | ancestorId.set("ID_OF_PARENT_PAGE") 118 | // set username and password 119 | username.set("") 120 | password.set(System.getenv("CONFLUENCE_TOKEN")) 121 | notifyWatchers.set(false) 122 | } 123 | 124 | tasks.withType(PublishToConfluenceTask::class) { 125 | dependsOn("generateDiagramsAndDocs") 126 | } 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriskn/arch-docs-as-code-example/25902d95d313cb68446d55a6ff1f56151ff2e3a1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /helm/inventory-service/templates/prometheus-rules.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: PrometheusRule 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | prometheus: example 8 | spec: 9 | groups: 10 | - name: example 11 | rules: 12 | - alert: InstanceDown 13 | expr: up == 0 14 | for: 5m 15 | labels: 16 | severity: critical 17 | annotations: 18 | summary: "Instance {{ $labels.instance }} down" 19 | description: | 20 | {{ $labels.instance }} of job {{ $labels.job }} 21 | has been down for more than 5 minutes. 22 | - alert: APIHighRequestLatency 23 | expr: api_http_request_latencies_second{quantile="0.5"} > 1 24 | for: 10m 25 | labels: 26 | severity: critical 27 | annotations: 28 | summary: "High request latency on {{ $labels.instance }}" 29 | description: | 30 | {{ $labels.instance }} has a median request latency 31 | above 1s (current value: {{ $value }}s) -------------------------------------------------------------------------------- /helm/inventory-service/values.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | requests_cpu: 2000m 3 | requests_memory: 1500Mi 4 | limits_cpu: 3000m 5 | limits_memory: 1500Mi 6 | autoscaling: 7 | min_replicas: 5 8 | max_replicas: 10 9 | port: 8080 10 | imagePullPolicy: IfNotPresent -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Inventory Service" 2 | 3 | -------------------------------------------------------------------------------- /src/docs/kotlin/GenerateDiagramsAndDocs.kt: -------------------------------------------------------------------------------- 1 | package docsascode 2 | 3 | import com.github.chriskn.structurizrextension.writeDiagrams 4 | import docsascode.model.Components 5 | import docsascode.model.Containers 6 | import docsascode.model.Deployment 7 | import docsascode.model.InventoryWorkspace.workspace 8 | import docsascode.model.Systems 9 | import docsascode.model.utils.createAsciiAlertTable 10 | import java.io.File 11 | 12 | private val outputFolder = File("src/docs/resources/plantuml/") 13 | private val docsGeneratedFolder = File("src/docs/resources/docs_generated/") 14 | 15 | fun main() { 16 | generateDiagrams() 17 | generateDocs() 18 | } 19 | 20 | private fun generateDiagrams() { 21 | Systems.createContextView() 22 | Containers.createContainerView() 23 | Components.createComponentView() 24 | Deployment.createDeploymentView() 25 | 26 | workspace.writeDiagrams(outputFolder) 27 | } 28 | 29 | private fun generateDocs() { 30 | val alertTable = createAsciiAlertTable() 31 | docsGeneratedFolder.mkdirs() 32 | File(docsGeneratedFolder, "alerts.adoc") 33 | .bufferedWriter() 34 | .use { out -> out.write(alertTable) } 35 | } 36 | -------------------------------------------------------------------------------- /src/docs/kotlin/model/Components.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.github.chriskn.structurizrextension.plantuml.C4PlantUmlLayout 4 | import com.github.chriskn.structurizrextension.plantuml.DependencyConfiguration 5 | import com.github.chriskn.structurizrextension.plantuml.Direction 6 | import com.github.chriskn.structurizrextension.view.componentView 7 | import com.structurizr.analysis.AbstractComponentFinderStrategy 8 | import com.structurizr.analysis.AnnotationTypeMatcher 9 | import com.structurizr.analysis.ComponentFinder 10 | import com.structurizr.analysis.ExtendsClassTypeMatcher 11 | import com.structurizr.analysis.IgnoreDuplicateComponentStrategy 12 | import com.structurizr.analysis.ImplementsInterfaceTypeMatcher 13 | import com.structurizr.analysis.NameSuffixTypeMatcher 14 | import com.structurizr.analysis.RegexTypeMatcher 15 | import com.structurizr.analysis.SpringComponentFinderStrategy 16 | import com.structurizr.analysis.StructurizrAnnotationsComponentFinderStrategy 17 | import com.structurizr.analysis.TypeMatcherComponentFinderStrategy 18 | import com.structurizr.model.Component 19 | import docsascode.model.Containers.inventoryProvider 20 | import docsascode.model.Containers.subGraphInventory 21 | import example.webshop.inventory.AbstractQuery 22 | import example.webshop.inventory.Consumer 23 | import org.reflections.util.ClasspathHelper 24 | import org.springframework.stereotype.Repository 25 | import java.net.URLClassLoader 26 | 27 | 28 | object Components { 29 | 30 | init { 31 | val strategy = 32 | createStructurizrAnnotationsComponentFinderStrategy() 33 | // createSpringComponentFinderStrategy() 34 | // createTypeMatcherComponentFinderStrategy() 35 | strategy.duplicateComponentStrategy = IgnoreDuplicateComponentStrategy() 36 | 37 | val componentFinder = ComponentFinder( 38 | inventoryProvider, 39 | "example.webshop.inventory", 40 | strategy, 41 | ) 42 | // Workaround for issue https://github.com/ronmamo/reflections/issues/373 occurring when running from jar 43 | componentFinder.urlClassLoader = URLClassLoader( 44 | ClasspathHelper 45 | .forPackage("example.webshop.inventory") 46 | .toTypedArray() 47 | ) 48 | // componentFinder.exclude(".*\\.Abstract.*", ".*\\.Consumer") 49 | componentFinder.findComponents() 50 | } 51 | 52 | private fun createTypeMatcherComponentFinderStrategy(): AbstractComponentFinderStrategy = 53 | TypeMatcherComponentFinderStrategy( 54 | NameSuffixTypeMatcher( 55 | "WriteService", 56 | "Writes data to database and triggers orders if goods run out of stock", 57 | "Spring Service" 58 | ), 59 | NameSuffixTypeMatcher( 60 | "ReadService", 61 | "Reads data from database", 62 | "Spring Service" 63 | ), 64 | AnnotationTypeMatcher( 65 | Repository::class.java, 66 | "Spring JDBC Repository", 67 | "Spring JDBC" 68 | ), 69 | ImplementsInterfaceTypeMatcher( 70 | Consumer::class.java, 71 | "Consumes Kafka topic", 72 | "Spring Cloud Stream Binder Kafka" 73 | ), 74 | ExtendsClassTypeMatcher( 75 | AbstractQuery::class.java, 76 | "GraphQL query", 77 | "Spring Controller" 78 | ), 79 | RegexTypeMatcher( 80 | ".*\\.OrderClient", 81 | "Sends orders via POST to order service", 82 | "Spring RestTemplate" 83 | ) 84 | ) 85 | 86 | 87 | private fun createSpringComponentFinderStrategy(): AbstractComponentFinderStrategy = SpringComponentFinderStrategy() 88 | 89 | private fun createStructurizrAnnotationsComponentFinderStrategy(): AbstractComponentFinderStrategy = 90 | StructurizrAnnotationsComponentFinderStrategy() 91 | 92 | fun createComponentView() { 93 | val componentView = InventoryWorkspace.views.componentView( 94 | container = inventoryProvider, 95 | key = "inventory_components", 96 | description = "Component diagram for the ${inventoryProvider.name} service", 97 | layout = C4PlantUmlLayout( 98 | dependencyConfigurations = listOf( 99 | DependencyConfiguration( 100 | filter = { it.destination.parent == Systems.warehouse || it.destination == subGraphInventory }, 101 | direction = Direction.Up 102 | ) 103 | ) 104 | ) 105 | ) 106 | // inventoryProvider.components 107 | // .filter { 108 | // it.technology.equals(SpringComponentFinderStrategy.SPRING_REPOSITORY) 109 | // }.map { 110 | // it.description = "Spring JDBC Repository" 111 | // } 112 | componentView.addAllComponents() 113 | inventoryProvider.components.forEach { 114 | componentView.addNearestNeighbours(it) 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/Containers.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.github.chriskn.structurizrextension.model.C4Type 4 | import com.github.chriskn.structurizrextension.model.Dependency 5 | import com.github.chriskn.structurizrextension.model.container 6 | import com.github.chriskn.structurizrextension.plantuml.C4PlantUmlLayout 7 | import com.github.chriskn.structurizrextension.plantuml.DependencyConfiguration 8 | import com.github.chriskn.structurizrextension.plantuml.Direction 9 | import com.github.chriskn.structurizrextension.view.containerView 10 | import com.structurizr.model.Container 11 | import com.structurizr.model.InteractionStyle.Asynchronous 12 | import com.structurizr.model.Location 13 | import com.structurizr.model.SoftwareSystem 14 | import docsascode.model.Systems.graphQlFederation 15 | import docsascode.model.Systems.inventoryService 16 | import docsascode.model.Systems.orderService 17 | import docsascode.model.Systems.warehouse 18 | import docsascode.model.utils.parseTopicDestinations 19 | 20 | object Containers { 21 | 22 | val subGraphInventory = graphQlFederation.container( 23 | name = "subgraph-inventory", 24 | description = "Provides inventory data", 25 | location = Location.Internal, 26 | technology = "GraphQL", 27 | icon = "graphql" 28 | ) 29 | 30 | private val topicConsumedByInventoryService = 31 | parseTopicDestinations().map { topicName -> 32 | warehouse.kafkaTopic(topicName, resolveDescriptionByTopicName(topicName)) 33 | } 34 | 35 | private fun SoftwareSystem.kafkaTopic( 36 | name: String, 37 | description: String 38 | ): Container = this.container( 39 | name = name, 40 | description = description, 41 | c4Type = C4Type.QUEUE, 42 | technology = "Kafka", 43 | icon = "kafka", 44 | link = "https://examplecompany.akhq.org/$name" 45 | ) 46 | 47 | private fun resolveDescriptionByTopicName(topicName: String): String = with(topicName) { 48 | when { 49 | contains("goods") -> "Contains metadata regarding goods" 50 | contains("stock") -> "Contains data regarding the amount of goods in stock" 51 | else -> "" 52 | } 53 | } 54 | 55 | val database = inventoryService.container( 56 | name = "Inventory Database", 57 | description = "Stores inventory items", 58 | c4Type = C4Type.DATABASE, 59 | technology = "PostgreSQL", 60 | icon = "postgresql" 61 | ) 62 | 63 | val inventoryProvider = inventoryService.container( 64 | name = "Inventory Provider", 65 | description = "Reads inventory data and provides it to clients via subgraph-inventory. " + 66 | "Orders new goods if they run out of stock", 67 | technology = "SpringBoot, Spring Data JDBC, Kafka Streams", 68 | icon = "springboot", 69 | link = "#component-view", 70 | uses = topicConsumedByInventoryService.map { topic -> 71 | Dependency( 72 | destination = topic, 73 | description = "Reads", 74 | technology = "Kafka", 75 | interactionStyle = Asynchronous 76 | ) 77 | }.plus( 78 | listOf( 79 | Dependency(destination = database, description = "Reads and writes inventory data to/from"), 80 | Dependency(destination = subGraphInventory, description = "contributes to federated graph"), 81 | Dependency( 82 | destination = orderService, 83 | description = "Triggers order if goods run out of stock", 84 | technology = "REST" 85 | ) 86 | ) 87 | ) 88 | ) 89 | 90 | fun createContainerView() { 91 | val containerView = InventoryWorkspace.views.containerView( 92 | system = inventoryService, 93 | key = "inventory_container", 94 | description = "Container diagram for the inventory domain", 95 | layout = C4PlantUmlLayout( 96 | dependencyConfigurations = listOf( 97 | DependencyConfiguration( 98 | filter = { it.destination == database }, 99 | direction = Direction.Left 100 | ), 101 | DependencyConfiguration( 102 | filter = { it.destination == subGraphInventory }, 103 | direction = Direction.Up 104 | ), 105 | DependencyConfiguration( 106 | filter = { it.destination == orderService }, 107 | direction = Direction.Right 108 | ) 109 | ), 110 | nodeSep = 80, 111 | rankSep = 80, 112 | ) 113 | ) 114 | containerView.addNearestNeighbours(inventoryProvider) 115 | containerView.externalSoftwareSystemBoundariesVisible = true 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/Deployment.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.github.chriskn.structurizrextension.model.C4Properties 4 | import com.github.chriskn.structurizrextension.model.Dependency 5 | import com.github.chriskn.structurizrextension.model.deploymentNode 6 | import com.github.chriskn.structurizrextension.model.infrastructureNode 7 | import com.github.chriskn.structurizrextension.plantuml.C4PlantUmlLayout 8 | import com.github.chriskn.structurizrextension.plantuml.DependencyConfiguration 9 | import com.github.chriskn.structurizrextension.plantuml.Layout 10 | import com.github.chriskn.structurizrextension.plantuml.Mode 11 | import com.github.chriskn.structurizrextension.view.deploymentView 12 | import docsascode.model.utils.resolveHelmValues 13 | 14 | object Deployment { 15 | 16 | private val aws = InventoryWorkspace.model.deploymentNode( 17 | name = "AWS", 18 | icon = "aws", 19 | properties = C4Properties( 20 | values = listOf( 21 | listOf("accountId", "123456"), 22 | listOf("region", "eu-central-1") 23 | ) 24 | ) 25 | ) 26 | 27 | private val rds = aws.deploymentNode( 28 | name = "Relational Database Service (RDS)", 29 | icon = "awsrds", 30 | hostsContainers = listOf(Containers.database) 31 | ) 32 | 33 | private val eks = aws.deploymentNode( 34 | name = "Elastic Kubernetes Service (EKS)", 35 | icon = "awsekscloud" 36 | ) 37 | 38 | 39 | private val inventoryPod = eks.deploymentNode( 40 | name = "Inventory POD", 41 | properties = C4Properties( 42 | values = resolveHelmValues() 43 | ) 44 | ) 45 | 46 | private val inventoryDocker = inventoryPod.deploymentNode( 47 | name = "Docker Container", 48 | icon = "docker", 49 | hostsContainers = listOf(Containers.inventoryProvider) 50 | ) 51 | 52 | private val ingress = eks.infrastructureNode( 53 | name = "Ingress", 54 | description = "Used for load balancing and SSL termination", 55 | icon = "nginx", 56 | technology = "NGINX", 57 | uses = listOf( 58 | Dependency( 59 | destination = inventoryPod, 60 | description = "Forwards requests to" 61 | ) 62 | ) 63 | ) 64 | 65 | private val apollo = InventoryWorkspace.model.deploymentNode( 66 | name = "Apollo Studio", 67 | hostsSystems = listOf(Systems.graphQlFederation), 68 | uses = listOf( 69 | Dependency( 70 | destination = ingress, 71 | description = "Forwards ${Containers.subGraphInventory.name} queries to" 72 | ) 73 | ) 74 | ) 75 | 76 | fun createDeploymentView() { 77 | val deploymentView = InventoryWorkspace.views.deploymentView( 78 | system = Systems.inventoryService, 79 | key = "inventory_deployment", 80 | description = "Deployment diagram for the Inventory service", 81 | layout = C4PlantUmlLayout( 82 | dependencyConfigurations = listOf( 83 | DependencyConfiguration( 84 | filter = { it.source == ingress }, 85 | mode = Mode.Neighbor 86 | ), 87 | ), 88 | layout = Layout.LeftToRight 89 | ) 90 | ) 91 | deploymentView.addAllDeploymentNodes() 92 | } 93 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/ExternalTeams.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.github.chriskn.structurizrextension.model.Dependency 4 | import com.github.chriskn.structurizrextension.model.person 5 | import com.structurizr.model.Location 6 | import com.structurizr.model.SoftwareSystem 7 | import docsascode.model.Systems.orderService 8 | import docsascode.model.Systems.warehouse 9 | import docsascode.model.InventoryWorkspace.model 10 | 11 | object ExternalTeams { 12 | 13 | fun SoftwareSystem.maintainedBy(teamName: String) = model.person( 14 | name = teamName, 15 | description = "Maintains and operates the ${this.name}", 16 | location = this.location, 17 | uses = listOf(Dependency(this, "maintains and operates")), 18 | link = "https://yourconfluence.com/teams/${teamName.replace(" ", "+")}" 19 | ) 20 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/InventoryWorkspace.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.structurizr.Workspace 4 | import com.structurizr.model.Model 5 | import com.structurizr.view.ViewSet 6 | 7 | 8 | object InventoryWorkspace { 9 | val workspace: Workspace = Workspace( 10 | "Inventory Service", 11 | "Inventory Service example" 12 | ) 13 | val model: Model = workspace.model 14 | val views: ViewSet = workspace.views 15 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/Systems.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model 2 | 3 | import com.github.chriskn.structurizrextension.model.Dependency 4 | import com.github.chriskn.structurizrextension.model.softwareSystem 5 | import com.github.chriskn.structurizrextension.plantuml.C4PlantUmlLayout 6 | import com.github.chriskn.structurizrextension.plantuml.DependencyConfiguration 7 | import com.github.chriskn.structurizrextension.plantuml.Direction 8 | import com.github.chriskn.structurizrextension.view.systemContextView 9 | import com.structurizr.model.InteractionStyle 10 | import com.structurizr.model.Location 11 | import com.structurizr.model.Person 12 | import docsascode.model.ExternalTeams.maintainedBy 13 | import docsascode.model.InventoryWorkspace.model 14 | 15 | object Systems { 16 | 17 | val orderService = model.softwareSystem( 18 | name = "Order service", 19 | description = "Orders new goods", 20 | location = Location.External 21 | ) 22 | val warehouse = model.softwareSystem( 23 | name = "Warehouse", 24 | description = "Provides inventory data via Kafka", 25 | location = Location.External 26 | ) 27 | 28 | val graphQlFederation = model.softwareSystem( 29 | name = "GraphQL Federation", 30 | description = "Provides a federated Graph to clients", 31 | location = Location.External, 32 | ) 33 | 34 | val inventoryService = model.softwareSystem( 35 | name = "Inventory service", 36 | description = "Reads inventory data and provides it to clients via subgraph-inventory. " + 37 | "Orders new goods if they run out of stock", 38 | uses = listOf( 39 | Dependency( 40 | destination = graphQlFederation, 41 | description = "subgraph-inventory", 42 | link = "http://your-federated-graph/subgraph-inventory" 43 | ), 44 | Dependency( 45 | destination = warehouse, 46 | description = "Reads inventory data from", technology = "Kafka", 47 | interactionStyle = InteractionStyle.Asynchronous 48 | ), 49 | Dependency( 50 | destination = orderService, 51 | description = "Triggers order if goods run out of stock", 52 | technology = "REST" 53 | ) 54 | ), 55 | link = "#container-view" 56 | ) 57 | 58 | init { 59 | warehouse.maintainedBy(teamName = "Team A") 60 | orderService.maintainedBy(teamName = "Team B") 61 | } 62 | 63 | fun createContextView(){ 64 | val contextView = InventoryWorkspace.views.systemContextView( 65 | softwareSystem = inventoryService, 66 | key = "inventory_context", 67 | description = "Context diagram for the web shop inventory system", 68 | layout = C4PlantUmlLayout( 69 | dependencyConfigurations = listOf( 70 | DependencyConfiguration( 71 | filter = {it.destination == graphQlFederation}, 72 | direction = Direction.Up 73 | ), 74 | DependencyConfiguration( 75 | filter = {it.destination == orderService}, 76 | direction = Direction.Right 77 | ), 78 | DependencyConfiguration( 79 | filter = {it.source is Person}, 80 | direction = Direction.Up 81 | ) 82 | ) 83 | ) 84 | ) 85 | contextView.addAllElements() 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/docs/kotlin/model/utils/HelmAlertUtils.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model.utils 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.DeserializationFeature 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper 7 | import com.fasterxml.jackson.module.kotlin.KotlinModule 8 | import java.io.File 9 | 10 | private val yamlParser: ObjectMapper = YAMLMapper() 11 | .registerModule(KotlinModule.Builder().build()) 12 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 13 | 14 | // matches variables and stores reference $variablename in group $1 15 | private val variablesRegex = """\{\{\s*(.*?)\s*\}\}""".toRegex() 16 | 17 | fun createAsciiAlertTable(): String { 18 | val alertTableRows = parseAlertingRules() 19 | .joinToString(separator = "\n\n") { it.toAsciiTable() } 20 | return """ 21 | ||=== 22 | |${Alert.ASCII_TABLE_HEADER} 23 | 24 | $alertTableRows 25 | 26 | ||=== 27 | """.trimMargin() 28 | } 29 | 30 | 31 | private fun parseAlertingRules(): List { 32 | val rulesYaml = File( 33 | "./helm/inventory-service/templates/prometheus-rules.yaml" 34 | ).readText().replace(variablesRegex, "$1") 35 | val yamlAlerts = yamlParser.readTree(rulesYaml).findPath("rules") 36 | return yamlParser.readValue( 37 | yamlAlerts.toPrettyString(), 38 | object : TypeReference>() {} 39 | ) 40 | } 41 | 42 | data class Alert( 43 | val alert: String, 44 | val expr: String, 45 | val `for`: String, 46 | val annotations: Annotations, 47 | val labels: Labels 48 | ) { 49 | companion object { 50 | const val ASCII_TABLE_HEADER = 51 | "|Name |Severity |Summary |Description |Expression |Time range" 52 | } 53 | 54 | private val invalidSubstrings = listOf("\n", "|") 55 | 56 | fun toAsciiTable(): String { 57 | return """ 58 | || ${alert.sanitize()} 59 | || ${labels.severity.sanitize()} 60 | || ${annotations.summary.sanitize()} 61 | || ${annotations.description.sanitize()} 62 | ||`${expr.sanitize()}` 63 | || ${`for`.sanitize()} 64 | """.trimIndent() 65 | } 66 | 67 | private fun String?.sanitize(): String { 68 | var replaced = this 69 | invalidSubstrings.forEach { replaced = replaced?.replace(it, "") } 70 | return replaced ?: " " 71 | } 72 | 73 | data class Labels(val severity: String) 74 | 75 | data class Annotations(val summary: String, val description: String?) 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/docs/kotlin/model/utils/HelmValuesUtils.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model.utils 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper 6 | import java.io.File 7 | 8 | private val yamlParser: ObjectMapper = YAMLMapper() 9 | private val valuesYaml = File("./helm/inventory-service/values.yml").readText() 10 | 11 | fun resolveHelmValues(): List> { 12 | val test = yamlParser.readValue( 13 | valuesYaml, 14 | object : TypeReference>() {} 15 | ) 16 | val properties = mutableListOf>() 17 | flattenProperties(test, properties) 18 | return properties.sortedBy { it.first() } 19 | } 20 | 21 | private fun flattenProperties( 22 | propertyMap: Map, 23 | keys: MutableList>, 24 | parentPath: String = "" 25 | ) { 26 | propertyMap.entries 27 | .forEach { (propertyName, propertyValue): Map.Entry -> 28 | val canonicalName = if (parentPath.isNotBlank()) { 29 | "$parentPath.$propertyName" 30 | } else { 31 | propertyName 32 | } 33 | if (propertyValue is Map<*, *>) { 34 | flattenProperties( 35 | propertyValue as Map, 36 | keys, 37 | canonicalName 38 | ) 39 | } else { 40 | keys.add( 41 | listOf(canonicalName, propertyValue.toString()) 42 | ) 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/docs/kotlin/model/utils/KafkaTopicsUtils.kt: -------------------------------------------------------------------------------- 1 | package docsascode.model.utils 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper 5 | 6 | private val yamlParser: ObjectMapper = YAMLMapper() 7 | private val appYaml = object {}::class.java.classLoader.getResource("application.yml")?.readText() ?: "" 8 | fun parseTopicDestinations(): List = yamlParser 9 | .readTree(appYaml) 10 | .path("spring").path("cloud").path("stream").path("bindings") 11 | .fields() 12 | .asSequence() 13 | .map { it.value.get("destination").textValue() } 14 | .toList() 15 | 16 | -------------------------------------------------------------------------------- /src/docs/resources/_01_introduction_and_goals.adoc: -------------------------------------------------------------------------------- 1 | [[section-introduction-and-goals]] 2 | == Introduction and Goals 3 | 4 | 5 | 6 | === Requirements Overview 7 | 8 | 9 | === Quality Goals 10 | 11 | 12 | 13 | === Stakeholders 14 | -------------------------------------------------------------------------------- /src/docs/resources/_02_architecture_constraints.adoc: -------------------------------------------------------------------------------- 1 | [[section-architecture-constraints]] 2 | == Architecture Constraints 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/docs/resources/_03_system_scope_and_context.adoc: -------------------------------------------------------------------------------- 1 | [[section-system-scope-and-context]] 2 | == System Scope and Context 3 | 4 | plantuml::{plantUmlDir}/inventory_context.puml[format=svg,opts="inline",align="center"] 5 | 6 | === Business Context 7 | 8 | 9 | === Technical Context 10 | 11 | -------------------------------------------------------------------------------- /src/docs/resources/_04_solution_strategy.adoc: -------------------------------------------------------------------------------- 1 | [[section-solution-strategy]] 2 | == Solution Strategy 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/docs/resources/_05_building_block_view.adoc: -------------------------------------------------------------------------------- 1 | [[section-building-block-view]] 2 | 3 | 4 | == Building Block View 5 | 6 | 7 | 8 | === Whitebox Overall System 9 | 10 | [[container-view]] 11 | plantuml::{plantUmlDir}/inventory_container.puml[format=svg,opts="inline",align="center"] 12 | 13 | === Level 2 14 | 15 | [[component-view]] 16 | plantuml::{plantUmlDir}/inventory_components.puml[format=svg,opts="inline",align="center"] 17 | -------------------------------------------------------------------------------- /src/docs/resources/_06_runtime_view.adoc: -------------------------------------------------------------------------------- 1 | [[section-runtime-view]] 2 | == Runtime View 3 | 4 | 5 | 6 | 7 | === 8 | 9 | === 10 | 11 | === ... 12 | 13 | === 14 | -------------------------------------------------------------------------------- /src/docs/resources/_07_deployment_view.adoc: -------------------------------------------------------------------------------- 1 | [[section-deployment-view]] 2 | 3 | 4 | == Deployment View 5 | 6 | plantuml::{plantUmlDir}/inventory_deployment.puml[format=svg,opts="inline",align="center"] 7 | 8 | == Alerts 9 | 10 | include::docs_generated/alerts.adoc[] -------------------------------------------------------------------------------- /src/docs/resources/_08_concepts.adoc: -------------------------------------------------------------------------------- 1 | [[section-concepts]] 2 | == Cross-cutting Concepts 3 | -------------------------------------------------------------------------------- /src/docs/resources/_09_architecture_decisions.adoc: -------------------------------------------------------------------------------- 1 | [[section-design-decisions]] 2 | == Architecture Decisions 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/docs/resources/_10_quality_requirements.adoc: -------------------------------------------------------------------------------- 1 | [[section-quality-scenarios]] 2 | == Quality Requirements 3 | 4 | === Quality Tree 5 | 6 | === Quality Scenarios 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/docs/resources/_11_technical_risks.adoc: -------------------------------------------------------------------------------- 1 | [[section-technical-risks]] 2 | == Risks and Technical Debts 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/docs/resources/_12_glossary.adoc: -------------------------------------------------------------------------------- 1 | [[section-glossary]] 2 | == Glossary 3 | 4 | [cols="e,2e" options="header"] 5 | |=== 6 | |Term |Definition 7 | 8 | | 9 | | 10 | 11 | | 12 | | 13 | |=== 14 | -------------------------------------------------------------------------------- /src/docs/resources/index.adoc: -------------------------------------------------------------------------------- 1 | // header file for arc42-template, 2 | // including all help texts 3 | // 4 | // ==================================== 5 | 6 | = {projectName} architecture documentation 7 | // toc-title definition MUST follow document title without blank line! 8 | :toc-title: Table of Contents 9 | 10 | // numbering from here on 11 | :numbered: 12 | 13 | Last updated: {dateTime} 14 | 15 | <<<< 16 | // 1. Introduction and Goals 17 | include::_01_introduction_and_goals.adoc[] 18 | 19 | <<<< 20 | // 2. Architecture Constraints 21 | include::_02_architecture_constraints.adoc[] 22 | 23 | <<<< 24 | // 3. System Scope and Context 25 | include::_03_system_scope_and_context.adoc[] 26 | 27 | <<<< 28 | // 4. Solution Strategy 29 | include::_04_solution_strategy.adoc[] 30 | 31 | <<<< 32 | // 5. Building Block View 33 | include::_05_building_block_view.adoc[] 34 | 35 | <<<< 36 | // 6. Runtime View 37 | include::_06_runtime_view.adoc[] 38 | 39 | <<<< 40 | // 7. Deployment View 41 | include::_07_deployment_view.adoc[] 42 | 43 | <<<< 44 | // 8. Concepts 45 | include::_08_concepts.adoc[] 46 | 47 | <<<< 48 | // 9. Architecture Decisions 49 | include::_09_architecture_decisions.adoc[] 50 | 51 | <<<< 52 | // 10. Quality Requirements 53 | include::_10_quality_requirements.adoc[] 54 | 55 | <<<< 56 | // 11. Technical Risks 57 | include::_11_technical_risks.adoc[] 58 | 59 | <<<< 60 | // 12. Glossary 61 | include::_12_glossary.adoc[] 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/kotlin/example/webshop/inventory/Components.kt: -------------------------------------------------------------------------------- 1 | package example.webshop.inventory 2 | 3 | import com.structurizr.annotation.UsesComponent 4 | import com.structurizr.annotation.UsesContainer 5 | import com.structurizr.annotation.UsesSoftwareSystem 6 | import org.springframework.stereotype.Component 7 | import org.springframework.stereotype.Repository 8 | import org.springframework.stereotype.Service 9 | import com.structurizr.annotation.Component as StructurizrComponent 10 | 11 | abstract class AbstractQuery 12 | 13 | @Service 14 | @StructurizrComponent(description = "Query providing inventory data to clients", technology = "GraphQL") 15 | @UsesContainer(name = "Container://GraphQL Federation.subgraph-inventory", description = "Is part of") 16 | class InventoryQuery( 17 | @UsesComponent(description = "Reads inventory data using") 18 | private val inventoryReadService: InventoryReadService 19 | ) 20 | 21 | interface Consumer 22 | 23 | @Service 24 | @StructurizrComponent(description = "Consumes goods Kafka topic") 25 | @UsesContainer(name = "Container://Warehouse.warehousegoods", description = "Consumes", technology = "Kafka") 26 | class GoodsConsumer( 27 | @UsesComponent(description = "Writes received goods via") 28 | private val inventoryWriteService: InventoryWriteService 29 | ) : Consumer 30 | 31 | @Component 32 | @StructurizrComponent(description = "Consumes stock Kafka topic") 33 | @UsesContainer(name = "Container://Warehouse.warehousestock_v1", description = "Consumes", technology = "Kafka") 34 | class StockConsumer( 35 | @UsesComponent(description = "Writes received stock data") 36 | private val inventoryWriteService: InventoryWriteService 37 | ) : Consumer 38 | 39 | @Component 40 | @StructurizrComponent(description = "Sends orders via POST to Order service") 41 | @UsesSoftwareSystem(name = "Order service", description = "Sends orders via POST requests to", technology = "REST") 42 | class OrderClient 43 | 44 | @Service 45 | @StructurizrComponent(description = "Service reading inventory data") 46 | class InventoryReadService( 47 | @UsesComponent(description = "Reads inventory data using") 48 | private val inventoryRepository: InventoryRepository 49 | ) 50 | 51 | @Service 52 | @StructurizrComponent(description = "Writes Inventory data to database and triggers orders if goods run out of stock") 53 | class InventoryWriteService( 54 | @UsesComponent(description = "Writes Inventory data to") 55 | private val inventoryRepository: InventoryRepository, 56 | @UsesComponent(description = "Uses to order new goods") 57 | private val orderClient: OrderClient 58 | ) 59 | 60 | @Repository 61 | @StructurizrComponent(description = "JDBC repository reading/writing inventory data") 62 | @UsesContainer(name = "Inventory Database", description = "Reads/writes Inventory data", technology = "JDBC") 63 | class InventoryRepository 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # This configuration is just an example and incomplete. 2 | # Complete config can be found here: https://github.com/codecentric/spring-kafka-streams-example/blob/0515cad71fffb6f34465260c66fafd9f59f7cc79/kafka-samples-consumer/src/main/resources/application.yml 3 | spring: 4 | application: 5 | name: inventory-service 6 | cloud: 7 | stream: 8 | bindings: 9 | processStock-in-0: 10 | group: ${spring.application.name}-consumer 11 | destination: warehouse.stock_v1 12 | processGoods-in-0: 13 | group: ${spring.application.name}-consumer 14 | destination: warehouse.goods 15 | function: 16 | definition: processStock;processGoods --------------------------------------------------------------------------------