├── .gitignore ├── .travis.yml ├── .zappr.yaml ├── Dockerfile ├── LICENSE ├── MAINTAINERS ├── README.md ├── delivery.yaml ├── dev └── user.clj ├── java-dynamic-memory-opts ├── project.clj ├── resources ├── api │ └── kio-api.yaml └── db │ ├── applications.sql │ └── migration │ ├── V10__Nullable_criticality_level.sql │ ├── V11__Add_not_relevant_criticality_level.sql │ ├── V12__Add Support field.sql │ ├── V1__Basic schema.sql │ ├── V2__Add v_last_modified.sql │ ├── V3__Add required_approvers.sql │ ├── V4__Add specification_Type.sql │ ├── V5__Add auditing fields.sql │ ├── V6__ Add criticality fields.sql │ ├── V7__Add publicly_accessible field.sql │ ├── V8__Add_incident_contact_field.sql │ └── V9__Add_indexes.sql ├── src └── org │ └── zalando │ └── stups │ └── kio │ ├── api.clj │ ├── audit.clj │ ├── core.clj │ ├── metrics.clj │ └── sql.clj └── test └── org └── zalando └── stups └── kio ├── integration_test └── api_test.clj └── unit_test ├── api_test.clj ├── audit_test.clj └── db_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | # Docker build 2 | /scm-source.json 3 | 4 | # Leiningen 5 | pom.xml 6 | pom.xml.asc 7 | *jar 8 | /lib/ 9 | /classes/ 10 | /target/ 11 | /checkouts/ 12 | .lein-deps-sum 13 | .lein-repl-history 14 | .lein-plugins/ 15 | .lein-failures 16 | .nrepl-port 17 | 18 | # IntelliJ IDEA 19 | /.idea 20 | *.iml 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: 3 | - lein clean 4 | - lein test 5 | - lein uberjar 6 | dist: trusty # https://travis-ci.community/t/install-of-oracle-jdk-8-failing/3038/8 7 | jdk: 8 | - oraclejdk8 9 | -------------------------------------------------------------------------------- /.zappr.yaml: -------------------------------------------------------------------------------- 1 | approvals: 2 | groups: 3 | zalando: 4 | minimum: 2 5 | from: 6 | orgs: 7 | - "zalando" 8 | 9 | # Allow last committer / PR creator to approve as well. 10 | # This will probably become the default in zappr. 11 | ignore: none 12 | 13 | X-Zalando-Team: "automata" 14 | X-Zalando-Type: "code" 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM container-registry.zalando.net/library/eclipse-temurin-8-jdk:latest 2 | 3 | MAINTAINER Zalando SE 4 | 5 | COPY resources/api/kio-api.yaml /zalando-apis/ 6 | COPY target/kio.jar / 7 | COPY java-dynamic-memory-opts /usr/local/bin/java-dynamic-memory-opts 8 | RUN chmod +x /usr/local/bin/java-dynamic-memory-opts 9 | 10 | EXPOSE 8080 11 | ENV HTTP_PORT=8080 12 | 13 | CMD java $JAVA_OPTS $(java-dynamic-memory-opts 70) -jar /kio.jar 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Zalando SE 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Tronje Krop 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kio 2 | 3 | [![Build Status](https://travis-ci.org/zalando-stups/kio.svg?branch=main)](https://travis-ci.org/zalando-stups/kio) 4 | 5 | Kio is the application registry in the [STUPS ecosystem](http://zalando-stups.github.io). It manages the basic 6 | information about an organisation’s applications. 7 | 8 | ## Download 9 | 10 | Releases are pushed as Docker images in the [public Docker registry](https://registry.hub.docker.com/u/stups/kio/): 11 | 12 | * Image: [stups/kio](https://registry.hub.docker.com/u/stups/kio/tags/manage/) 13 | 14 | You can run Kio by starting it with Docker: 15 | 16 | $ docker run -it stups/kio 17 | 18 | ## Requirements 19 | 20 | * PostgreSQL 9.3+ 21 | 22 | ## Configuration 23 | 24 | Configuration is provided via environment variables during start. 25 | 26 | Variable | Default | Description 27 | ---------------- | ---------------------- | ----------- 28 | HTTP_PORT | `8080` | TCP port to provide the HTTP API. 29 | HTTP_CORS_ORIGIN | | Domain for cross-origin JavaScript requests. If set, the Access-Control headers will be set. 30 | DB_SUBNAME | `//localhost:5432/kio` | JDBC connection information of your database. 31 | DB_USER | `postgres` | Database user. 32 | DB_PASSWORD | `postgres` | Database password. 33 | 34 | Example: 35 | 36 | ``` 37 | $ docker run -it \ 38 | -e HTTP_CORS_ORIGIN="*.zalando.de" \ 39 | -e DB_USER=kio \ 40 | -e DB_PASSWORD=kio123 \ 41 | stups/kio 42 | ``` 43 | 44 | ## Building 45 | 46 | $ lein uberjar 47 | $ lein docker build 48 | 49 | ## Releasing 50 | 51 | $ lein release :minor 52 | 53 | ## Developing 54 | 55 | Kio embeds the [reloaded](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) workflow for interactive 56 | development: 57 | 58 | $ lein repl 59 | user=> (go) 60 | user=> (reset) 61 | 62 | ## License 63 | 64 | Copyright © 2015 Zalando SE 65 | 66 | Licensed under the Apache License, Version 2.0 (the "License"); 67 | you may not use this file except in compliance with the License. 68 | You may obtain a copy of the License at 69 | 70 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 71 | 72 | Unless required by applicable law or agreed to in writing, software 73 | distributed under the License is distributed on an "AS IS" BASIS, 74 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 75 | See the License for the specific language governing permissions and 76 | limitations under the License. 77 | -------------------------------------------------------------------------------- /delivery.yaml: -------------------------------------------------------------------------------- 1 | version: '2017-09-20' 2 | 3 | dependencies: 4 | - id: java 5 | type: docker 6 | ref: container-registry.zalando.net/library/eclipse-temurin-8-jdk 7 | 8 | pipeline: 9 | - id: build 10 | env: 11 | LEIN_ROOT: true 12 | type: script 13 | vm_config: 14 | type: linux 15 | image: "cdp-runtime/jdk8-clojure" 16 | artifacts: 17 | - type: docs 18 | name: automata-databases 19 | path: schema 20 | commands: 21 | - desc: Run unit tests 22 | cmd: | 23 | docker run -d -p 5432:5432 postgres:12.1 24 | lein test 25 | - desc: Build and push docker image 26 | cmd: | 27 | lein do clean, uberjar 28 | IMAGE="pierone.stups.zalan.do/automata/kio:${CDP_BUILD_VERSION}" 29 | docker build -t "$IMAGE" . 30 | if [ -z "$CDP_PULL_REQUEST_NUMBER" ]; then 31 | docker push "$IMAGE" 32 | fi 33 | TEST_IMAGE="pierone.stups.zalan.do/automata/kio-test:${CDP_BUILD_VERSION}" 34 | docker tag "$IMAGE" "$TEST_IMAGE" 35 | docker push "$TEST_IMAGE" 36 | - desc: Generate database docs 37 | cmd: | 38 | mkdir schema 39 | if [ -z "$CDP_PULL_REQUEST_NUMBER" ]; then 40 | DOCS_PATH=schema/kio 41 | 42 | wget https://github.com/schemaspy/schemaspy/releases/download/v6.0.0/schemaspy-6.0.0.jar -O schema.jar 43 | 44 | wget https://jdbc.postgresql.org/download/postgresql-42.2.4.jar 45 | 46 | java -jar schema.jar -t pgsql -s zk_data -db postgres -u postgres -host localhost -o $DOCS_PATH -dp postgresql-42.2.4.jar -noads 47 | fi 48 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | "Tools for interactive development with the REPL. This file should 3 | not be included in a production build of the application." 4 | (:require [clojure.java.javadoc :refer [javadoc]] 5 | [clojure.pprint :refer [pprint]] 6 | [clojure.reflect :refer [reflect]] 7 | [clojure.repl :refer [apropos dir doc find-doc pst source]] 8 | [clojure.tools.namespace.repl :refer [refresh refresh-all]] 9 | [com.stuartsierra.component :as component] 10 | [clojure.test :refer [run-all-tests]] 11 | [org.zalando.stups.kio.core :as core])) 12 | 13 | (def system 14 | "A Var containing an object representing the application under 15 | development." 16 | nil) 17 | 18 | (defn slurp-if-exists [file] 19 | (when (.exists (clojure.java.io/as-file file)) 20 | (slurp file))) 21 | 22 | (defn load-dev-config [file] 23 | (clojure.edn/read-string (slurp-if-exists file))) 24 | 25 | (defn start 26 | "Starts the system running, sets the Var #'system." 27 | [extra-config] 28 | (alter-var-root #'system (constantly (core/run (merge {:system-log-level "INFO"} 29 | extra-config 30 | (load-dev-config "./dev-config.edn")))))) 31 | 32 | (defn stop 33 | "Stops the system if it is currently running, updates the Var 34 | #'system." 35 | [] 36 | (alter-var-root #'system 37 | (fn [s] (when s (component/stop s))))) 38 | 39 | (defn go 40 | "Initializes and starts the system running." 41 | ([extra-config] 42 | (start extra-config) 43 | :ready) 44 | ([] 45 | (go {}))) 46 | 47 | (defn reset 48 | "Stops the system, reloads modified source files, and restarts it." 49 | [] 50 | (stop) 51 | (refresh :after 'user/go)) 52 | 53 | (defn run-tests [] 54 | (run-all-tests #"org.zalando.stups.kio.unit-test.*-test")) 55 | 56 | (defn run-integration-tests [] 57 | (run-all-tests #"org.zalando.stups.kio.integration-test.*-test")) 58 | 59 | (defn tests 60 | "Stops the system, reloads modified source files and runs tests" 61 | [] 62 | (stop) 63 | (refresh :after 'user/run-tests)) 64 | -------------------------------------------------------------------------------- /java-dynamic-memory-opts: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # example usage: 3 | # exec java $(java-dynamic-memory-opts 80) -jar myfatjar.jar 4 | 5 | # JVM uses only 1/4 of system memory by default 6 | DEFAULT_MEM_JAVA_PERCENT=80 7 | 8 | if [ -n "$1" ] 9 | then 10 | MEM_JAVA_PERCENT=$1 11 | else 12 | MEM_JAVA_PERCENT=$DEFAULT_MEM_JAVA_PERCENT 13 | fi 14 | 15 | MEM_TOTAL_KB=$(cat /proc/meminfo | grep MemTotal | awk '{print $2}') 16 | MEM_JAVA_KB=$(($MEM_TOTAL_KB * $MEM_JAVA_PERCENT / 100)) 17 | 18 | echo "-Xmx${MEM_JAVA_KB}k" 19 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.zalando.stups/kio "0.25.0-SNAPSHOT" 2 | :description "The application registry." 3 | :url "https://github.com/zalando-stups/kio" 4 | 5 | :license {:name "The Apache License, Version 2.0" 6 | :url "http://www.apache.org/licenses/LICENSE-2.0"} 7 | 8 | :min-lein-version "2.0.0" 9 | 10 | :dependencies [[org.clojure/clojure "1.10.0"] 11 | [org.apache.logging.log4j/log4j-api "2.17.0"] 12 | [org.apache.logging.log4j/log4j-core "2.17.0"] 13 | [org.apache.logging.log4j/log4j-slf4j-impl "2.17.0"] 14 | [org.apache.logging.log4j/log4j-jcl "2.17.0"] 15 | [org.apache.logging.log4j/log4j-1.2-api "2.17.0"] 16 | [org.apache.logging.log4j/log4j-jul "2.17.0"] 17 | [org.zalando.stups/friboo "1.13.0"] 18 | [clj-time "0.13.0"] 19 | [org.zalando.stups/tokens "0.11.0-beta-2"] 20 | [yesql "0.5.3"] 21 | [org.clojure/core.memoize "0.7.1"]] 22 | 23 | :managed-dependencies [[org.flatland/ordered "1.5.7"] 24 | [marick/suchwow "6.0.2"]] 25 | 26 | :main ^:skip-aot org.zalando.stups.kio.core 27 | :uberjar-name "kio.jar" 28 | 29 | :plugins [[io.sarnowski/lein-docker "1.1.0"] 30 | [org.zalando.stups/lein-scm-source "0.3.0"] 31 | [lein-cloverage "1.0.7-SNAPSHOT"]] 32 | 33 | :docker {:image-name #=(eval (str (some-> (System/getenv "DEFAULT_DOCKER_REGISTRY") 34 | (str "/")) 35 | "stups/kio"))} 36 | 37 | :release-tasks [["vcs" "assert-committed"] 38 | ["clean"] 39 | ["test"] 40 | ["change" "version" "leiningen.release/bump-version" "release"] 41 | ["vcs" "commit"] 42 | ["vcs" "tag"] 43 | ["uberjar"] 44 | ["scm-source"] 45 | ["docker" "build"] 46 | ["docker" "push"] 47 | ["change" "version" "leiningen.release/bump-version"] 48 | ["vcs" "commit"] 49 | ["vcs" "push"]] 50 | 51 | :pom-addition [:developers 52 | [:developer {:id "sarnowski"} 53 | [:name "Tobias Sarnowski"] 54 | [:email "tobias.sarnowski@zalando.de"] 55 | [:role "Maintainer"]]] 56 | 57 | :test-selectors {:default :unit 58 | :unit :unit 59 | :integration :integration} 60 | 61 | :profiles {:uberjar {:aot :all} 62 | :dev {:repl-options {:init-ns user} 63 | :source-paths ["dev"] 64 | :dependencies [[org.clojure/tools.namespace "0.2.10"] 65 | [midje "1.9.8"] 66 | [org.clojure/java.classpath "0.2.3"]]}}) 67 | -------------------------------------------------------------------------------- /resources/api/kio-api.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | 3 | # basic meta information 4 | info: 5 | title: Kio API 6 | version: '0.3' 7 | description: Kio is STUPS' application registry. 8 | 9 | externalDocs: 10 | description: STUPS overview 11 | url: http://stups.io 12 | 13 | # technical configuration 14 | basePath: / 15 | produces: 16 | - application/json 17 | consumes: 18 | - application/json 19 | 20 | security: 21 | - oauth2: [uid] 22 | 23 | paths: 24 | 25 | '/': 26 | get: 27 | summary: Application root 28 | operationId: org.zalando.stups.friboo.system.http/redirect-to-swagger-ui 29 | responses: 30 | default: 31 | description: "Redirects to /ui/" 32 | 33 | # applications 34 | 35 | '/apps': 36 | get: 37 | summary: list applications 38 | description: | 39 | Lists all registered applications. 40 | tags: 41 | - Applications 42 | operationId: 'org.zalando.stups.kio.api/read-applications' 43 | parameters: 44 | - name: search 45 | in: query 46 | description: "Search term for application filtering." 47 | type: string 48 | required: false 49 | - name: modified_before 50 | in: query 51 | description: "Only include apps that were modified before date" 52 | type: string 53 | format: date-time 54 | required: false 55 | - name: modified_after 56 | in: query 57 | description: "Only include apps that were modified after date" 58 | type: string 59 | format: date-time 60 | required: false 61 | - name: team_id 62 | in: query 63 | description: "Only include apps of this team" 64 | type: string 65 | required: false 66 | - name: incident_contact 67 | in: query 68 | description: "Only include apps with 24x7 support by the given contact/team" 69 | type: string 70 | required: false 71 | - name: active 72 | in: query 73 | description: "If true, include only active apps" 74 | type: boolean 75 | required: false 76 | responses: 77 | 200: 78 | description: List of all applications 79 | schema: 80 | type: array 81 | items: 82 | type: object 83 | properties: 84 | id: 85 | type: string 86 | description: Unique identifier of the application 87 | example: kio 88 | team_id: 89 | type: string 90 | description: ID of the team, responsible for this application 91 | example: stups 92 | incident_contact: 93 | type: string 94 | description: 24x7 contact, e.g. team ID of on-call support team 95 | example: sre 96 | active: 97 | type: boolean 98 | description: If this appliation is active, ie gets credentials 99 | example: true 100 | criticality_level: 101 | type: integer 102 | description: The criticality level (tier) of the application 103 | example: 2 104 | name: 105 | type: string 106 | description: A human-readable name of the application 107 | example: Kio 108 | subtitle: 109 | type: string 110 | description: Subtitle of the application 111 | example: Application Registry 112 | service_url: 113 | type: string 114 | description: URL of the service 115 | example: https://kio.example.com/ 116 | scm_url: 117 | type: string 118 | description: URL of SCM repository 119 | example: https://github.com/zalando-stups/kio.git 120 | documentation_url: 121 | type: string 122 | description: URL of documentation 123 | example: https://github.com/zalando-stups/kio 124 | specification_url: 125 | type: string 126 | description: URL of the specification tool 127 | example: https://github.com/zalando-stups/kio/issues 128 | last_modified: 129 | type: string 130 | format: date-time 131 | description: Point in time when the application was created or last modified. 132 | example: '2015-04-25T16:25:00.000Z' 133 | last_modified_by: 134 | type: string 135 | description: Who modified the application last 136 | example: npiccolotto 137 | matched_rank: 138 | type: number 139 | description: Search result rank for ordering 140 | matched_description: 141 | type: string 142 | description: Text fragments of the search result 143 | support_url: 144 | type: string 145 | description: URL Where to contact for support 146 | description: 147 | type: string 148 | description: Purpose of this application 149 | example: Kio manages all application base information. 150 | specification_type: 151 | type: string 152 | description: Where tickets for the application are managed 153 | example: Github 154 | created: 155 | type: string 156 | format: date-time 157 | description: Point in time when the application was created. 158 | example: '2015-04-25T16:25:00.000Z' 159 | created_by: 160 | type: string 161 | description: Who created the application 162 | example: npiccolotto 163 | publicly_accessible: 164 | type: boolean 165 | description: | 166 | Marks an app as "publicly available" (on the internet). A public app usually has a landing or login 167 | page and does not expose confidential data to unauthorized users. Examples are: Blogs, Job Portals, 168 | Shops, and so on. Secured (REST)-APIs and micro services are no public endpoints, even if they are 169 | available over the internet. 170 | example: true 171 | required: 172 | - id 173 | - team_id 174 | - name 175 | default: 176 | $ref: '#/responses/Error' 177 | 178 | '/apps/{application_id}': 179 | get: 180 | summary: read application 181 | description: | 182 | Returns details about one application 183 | tags: 184 | - Applications 185 | operationId: 'org.zalando.stups.kio.api/read-application' 186 | parameters: 187 | - $ref: '#/parameters/ApplicationID' 188 | responses: 189 | 200: 190 | description: Details of one application 191 | schema: 192 | type: object 193 | properties: 194 | id: 195 | type: string 196 | description: Unique identifier of the application 197 | example: kio 198 | team_id: 199 | type: string 200 | description: ID of the team, responsible for this application 201 | example: stups 202 | incident_contact: 203 | type: string 204 | description: 24x7 contact, e.g. team ID of on-call support team 205 | example: sre 206 | active: 207 | type: boolean 208 | description: If the application is active 209 | example: true 210 | name: 211 | type: string 212 | description: A human-readable name of the application 213 | example: Kio 214 | subtitle: 215 | type: string 216 | description: An additional title for the application 217 | example: Application Registry 218 | description: 219 | type: string 220 | description: Purpose of this application 221 | example: Kio manages all application base information. 222 | service_url: 223 | type: string 224 | description: URL of the application 225 | example: https://kio.example.com/ 226 | scm_url: 227 | type: string 228 | description: URL of SCM repository 229 | example: https://github.com/zalando-stups/kio.git 230 | documentation_url: 231 | type: string 232 | description: URL of documentation 233 | example: https://github.com/zalando-stups/kio 234 | specification_url: 235 | type: string 236 | description: URL of the specification tool 237 | example: https://github.com/zalando-stups/kio/issues 238 | required_approvers: 239 | type: integer 240 | description: Minimum number of approvers needed for a version. Used by fullstop. Deprecated, will be removed in the future. 241 | example: 2 242 | specification_type: 243 | type: string 244 | description: Where tickets for the application are managed 245 | example: Github 246 | criticality_level: 247 | type: integer 248 | description: The criticality level (tier) of the application 249 | example: 2 250 | last_modified: 251 | type: string 252 | format: date-time 253 | description: Point in time when the application was created or last modified. 254 | example: '2015-04-25T16:25:00.000Z' 255 | last_modified_by: 256 | type: string 257 | description: Who modified the application last 258 | example: npiccolotto 259 | created: 260 | type: string 261 | format: date-time 262 | description: Point in time when the application was created. 263 | example: '2015-04-25T16:25:00.000Z' 264 | created_by: 265 | type: string 266 | description: Who created the application 267 | example: npiccolotto 268 | publicly_accessible: 269 | type: boolean 270 | description: | 271 | Marks an app as "publicly available" (on the internet). A public app usually has a landing or login 272 | page and does not expose confidential data to unauthorized users. Examples are: Blogs, Job Portals, 273 | Shops, and so on. Secured (REST)-APIs and micro services are no public endpoints, even if they are 274 | available over the internet. 275 | example: true 276 | 404: 277 | description: Not found 278 | default: 279 | $ref: '#/responses/Error' 280 | 281 | put: 282 | summary: create or update application 283 | description: | 284 | Creates or updates an application. 285 | tags: 286 | - Applications 287 | operationId: "org.zalando.stups.kio.api/create-or-update-application!" 288 | parameters: 289 | - $ref: '#/parameters/ApplicationID' 290 | - name: application 291 | in: body 292 | description: Application details that will be saved. 293 | schema: 294 | '$ref': '#/definitions/StoreApplication' 295 | responses: 296 | 200: 297 | description: Application was saved. 298 | default: 299 | $ref: '#/responses/Error' 300 | 301 | # approval types 302 | '/apps/{application_id}/approvals': 303 | get: 304 | summary: list used approval types 305 | deprecated: true 306 | description: | 307 | Returns all used approval types for an application. Deprecated, will be removed in the future. Will always return an empty array. 308 | tags: 309 | - Applications 310 | operationId: 'org.zalando.stups.kio.api/read-application-approvals' 311 | parameters: 312 | - $ref: '#/parameters/ApplicationID' 313 | responses: 314 | 200: 315 | description: List of approvals 316 | schema: 317 | type: array 318 | items: 319 | type: string 320 | 404: 321 | description: Not found 322 | default: 323 | $ref: '#/responses/Error' 324 | 325 | # versions 326 | 327 | '/apps/{application_id}/versions': 328 | get: 329 | summary: list versions 330 | deprecated: true 331 | description: | 332 | Returns a list of all versions of an application. Deprecated, will be removed in the future. Will always return an empty array. 333 | tags: 334 | - Versions 335 | operationId: 'org.zalando.stups.kio.api/read-versions-by-application' 336 | parameters: 337 | - $ref: '#/parameters/ApplicationID' 338 | responses: 339 | 200: 340 | description: List of versions 341 | schema: 342 | type: array 343 | items: 344 | type: object 345 | properties: 346 | id: 347 | type: string 348 | description: Unique identifier of the version 349 | example: "1.0" 350 | application_id: 351 | type: string 352 | description: ID of the version's application 353 | example: kio 354 | last_modified: 355 | type: string 356 | format: date-time 357 | description: Point in time when the version was created or last modified. 358 | example: '2015-04-25T16:25:00.000Z' 359 | artifact: 360 | type: string 361 | description: Software artifact reference of this version 362 | example: docker://stups/kio:1.0 363 | 404: 364 | description: Not found 365 | default: 366 | $ref: '#/responses/Error' 367 | 368 | '/apps/{application_id}/versions/{version_id}': 369 | get: 370 | summary: read version 371 | deprecated: true 372 | description: | 373 | Returns a list of all versions of an application. Deprecated, will be removed in the future. Will always return an empty array. 374 | tags: 375 | - Versions 376 | operationId: 'org.zalando.stups.kio.api/read-version-by-application' 377 | parameters: 378 | - $ref: '#/parameters/ApplicationID' 379 | - $ref: '#/parameters/VersionID' 380 | responses: 381 | 200: 382 | description: Returns detailed information 383 | schema: 384 | type: object 385 | properties: 386 | id: 387 | type: string 388 | description: Unique identifier of the version 389 | example: 1.0 390 | application_id: 391 | type: string 392 | description: ID of the version's application 393 | example: kio 394 | last_modified: 395 | type: string 396 | format: date-time 397 | description: Point in time when the version was created or last modified. 398 | example: '2015-04-25T16:25:00.000Z' 399 | last_modified_by: 400 | type: string 401 | description: Who modified the version last 402 | example: npiccolotto 403 | created: 404 | type: string 405 | format: date-time 406 | description: Point in time when the version was created. 407 | example: '2015-04-25T16:25:00.000Z' 408 | created_by: 409 | type: string 410 | description: Who created the version 411 | example: npiccolotto 412 | artifact: 413 | type: string 414 | description: Software artifact reference of this version 415 | example: docker://stups/kio:1.0 416 | notes: 417 | type: string 418 | description: Release notes in Markdown format 419 | example: | 420 | **Release 1.0** 421 | 422 | * initial commit 423 | * bugfixes 424 | 404: 425 | description: Not found 426 | default: 427 | $ref: '#/responses/Error' 428 | 429 | put: 430 | summary: Create or update version. Deprecated, will be removed in the future. Will not actually save the version in the DB. 431 | deprecated: true 432 | description: | 433 | Creates or updates a version. 434 | tags: 435 | - Versions 436 | operationId: "org.zalando.stups.kio.api/create-or-update-version!" 437 | parameters: 438 | - $ref: '#/parameters/ApplicationID' 439 | - $ref: '#/parameters/VersionID' 440 | - name: version 441 | in: body 442 | description: Version details that will be saved. 443 | schema: 444 | '$ref': '#/definitions/StoreVersion' 445 | responses: 446 | 200: 447 | description: Version was saved. 448 | default: 449 | $ref: '#/responses/Error' 450 | 451 | # approvals 452 | 453 | '/apps/{application_id}/versions/{version_id}/approvals': 454 | get: 455 | summary: List approvals. Deprecated, will be removed in the future. Will always return an empty array. 456 | deprecated: true 457 | description: | 458 | Returns a list of all approvals of a version. 459 | tags: 460 | - Approvals 461 | operationId: 'org.zalando.stups.kio.api/read-approvals-by-version' 462 | parameters: 463 | - $ref: '#/parameters/ApplicationID' 464 | - $ref: '#/parameters/VersionID' 465 | responses: 466 | 200: 467 | description: List of approvals 468 | schema: 469 | type: array 470 | items: 471 | type: object 472 | properties: 473 | application_id: 474 | type: string 475 | description: ID of the application 476 | example: kio 477 | version_id: 478 | type: string 479 | description: ID of the application's version 480 | example: 1.0 481 | approval_type: 482 | type: string 483 | description: Kind of approval like 'TESTED' or 'REVIEWED'. 484 | example: TESTED 485 | user_id: 486 | type: string 487 | description: ID of the user who approved the version 488 | example: tobi 489 | approved_at: 490 | type: string 491 | format: date-time 492 | description: Point in time when the version was approved 493 | example: '2015-04-25T16:25:00.000Z' 494 | notes: 495 | type: string 496 | description: Some hints on what was approved 497 | example: | 498 | I tested this kio version carefully. 499 | 404: 500 | description: Not found 501 | default: 502 | $ref: '#/responses/Error' 503 | 504 | post: 505 | summary: Approve version. Deprecated, will be removed in the future. Will not actually save the approval in the DB. 506 | deprecated: true 507 | description: | 508 | Approves a version. 509 | tags: 510 | - Approvals 511 | operationId: "org.zalando.stups.kio.api/approve-version!" 512 | parameters: 513 | - $ref: '#/parameters/ApplicationID' 514 | - $ref: '#/parameters/VersionID' 515 | - name: approval 516 | in: body 517 | description: Approval information 518 | schema: 519 | '$ref': '#/definitions/StoreApproval' 520 | responses: 521 | 200: 522 | description: Version was approved. 523 | default: 524 | $ref: '#/responses/Error' 525 | 526 | # definitions 527 | 528 | parameters: 529 | ApplicationID: 530 | name: application_id 531 | in: path 532 | type: string 533 | description: ID of the application 534 | required: true 535 | pattern: "^[a-z][a-z0-9-]*[a-z0-9]$" 536 | #example: kio 537 | 538 | VersionID: 539 | name: version_id 540 | in: path 541 | type: string 542 | description: ID of the version 543 | required: true 544 | pattern: "^[A-Za-z0-9]+([._-][A-Za-z0-9]+)*$" 545 | 546 | responses: 547 | Error: 548 | description: An error occured. 549 | schema: 550 | $ref: '#/definitions/Error' 551 | 552 | definitions: 553 | Error: 554 | type: object 555 | properties: 556 | message: 557 | type: string 558 | 559 | StoreApplication: 560 | type: object 561 | properties: 562 | team_id: 563 | type: string 564 | description: ID of the team, responsible for this application 565 | example: stups 566 | incident_contact: 567 | type: string 568 | description: 24x7 contact, e.g. team ID of on-call support team 569 | example: sre 570 | active: 571 | type: boolean 572 | description: if the application is active 573 | example: true 574 | name: 575 | type: string 576 | description: A human-readable name 577 | example: Kio 578 | subtitle: 579 | type: string 580 | description: An additional title for the application 581 | example: STUPS' application registry 582 | description: 583 | type: string 584 | description: Purpose of this application 585 | example: Kio manages all application base information. 586 | service_url: 587 | type: string 588 | description: URL of the application 589 | example: https://kio.example.com/ 590 | scm_url: 591 | type: string 592 | description: URL of SCM repository 593 | example: https://github.com/zalando-stups/kio.git 594 | documentation_url: 595 | type: string 596 | description: URL of documentation 597 | example: https://github.com/zalando-stups/kio 598 | specification_url: 599 | type: string 600 | description: URL of the specification tool 601 | example: https://github.com/zalando-stups/kio/issues 602 | support_url: 603 | type: string 604 | description: URL of filing support requests 605 | example: https://github.com/zalando-stups/kio/issues 606 | criticality_level: 607 | description: Criticality level (tier) of an application 608 | type: integer 609 | minimum: 1 # Tier-1 610 | maximum: 4 # not relevant 611 | example: 2 612 | required_approvers: 613 | type: integer 614 | minimum: 1 615 | description: DEPRECATED! WILL BE REMOVED IN NEXT RELASE! Minimum number of approvers needed for a version. Used by fullstop. 616 | example: 2 617 | default: 2 618 | specification_type: 619 | type: string 620 | description: Where tickets for the application are managed 621 | example: Github 622 | publicly_accessible: 623 | type: boolean 624 | description: | 625 | Marks an app as "publicly available" (on the internet). A public app usually has a landing or login 626 | page and does not expose confidential data to unauthorized users. Examples are: Blogs, Job Portals, 627 | Shops, and so on. Secured (REST)-APIs and micro services are no public endpoints, even if they are 628 | available over the internet. 629 | example: true 630 | required: 631 | - team_id 632 | - active 633 | - name 634 | 635 | StoreVersion: 636 | type: object 637 | properties: 638 | artifact: 639 | type: string 640 | description: Software artifact reference of this version 641 | example: docker://stups/kio:1.0 642 | notes: 643 | type: string 644 | description: Release notes in Markdown format 645 | example: | 646 | **Release 1.0** 647 | 648 | * initial commit 649 | * bugfixes 650 | required: 651 | - artifact 652 | 653 | StoreApproval: 654 | type: object 655 | properties: 656 | approval_type: 657 | type: string 658 | description: Kind of approval like 'TESTED' or 'REVIEWED'. 659 | example: TESTED 660 | notes: 661 | type: string 662 | description: Some hints on what was approved 663 | example: | 664 | I tested this kio version carefully. 665 | required: 666 | - approval_type 667 | 668 | securityDefinitions: 669 | oauth2: 670 | type: oauth2 671 | flow: implicit 672 | authorizationUrl: https://example.com/oauth2/dialog 673 | scopes: 674 | uid: Unique identifier of the user accessing the service. 675 | -------------------------------------------------------------------------------- /resources/db/applications.sql: -------------------------------------------------------------------------------- 1 | -- name: read-applications 2 | SELECT a_id as id, 3 | a_team_id as team_id, 4 | a_incident_contact as incident_contact, 5 | a_active as active, 6 | a_criticality_level as criticality_level, 7 | a_name as name, 8 | a_subtitle as subtitle, 9 | a_service_url as service_url, 10 | a_support_url as support_url, 11 | a_scm_url as scm_url, 12 | a_documentation_url as documentation_url, 13 | a_specification_url as specification_url, 14 | a_last_modified as last_modified, 15 | a_last_modified_by as last_modified_by, 16 | a_description as description, 17 | a_specification_type as specification_type, 18 | a_created as created, 19 | a_created_by as created_by, 20 | a_publicly_accessible as publicly_accessible 21 | FROM zk_data.application 22 | WHERE a_last_modified <= COALESCE(:modified_before, a_last_modified) 23 | AND a_last_modified >= COALESCE(:modified_after, a_last_modified) 24 | AND a_active = COALESCE(:active, a_active) 25 | AND a_team_id = COALESCE(:team_id, a_team_id) 26 | AND a_incident_contact IS NOT DISTINCT FROM COALESCE(:incident_contact, a_incident_contact); 27 | 28 | -- name: read-applications-json 29 | SELECT array_to_json(array_agg(row_to_json(t))) AS apps 30 | FROM ( 31 | SELECT a_id as id, 32 | a_team_id as team_id, 33 | a_incident_contact as incident_contact, 34 | a_active as active, 35 | a_name as name, 36 | a_subtitle as subtitle, 37 | a_service_url as service_url, 38 | a_support_url as support_url, 39 | a_scm_url as scm_url, 40 | a_documentation_url as documentation_url, 41 | a_specification_url as specification_url, 42 | a_last_modified as last_modified, 43 | a_last_modified_by as last_modified_by, 44 | a_description as description, 45 | a_specification_type as specification_type, 46 | a_criticality_level as criticality_level, 47 | a_created as created, 48 | a_created_by as created_by, 49 | a_publicly_accessible as publicly_accessible 50 | FROM zk_data.application 51 | WHERE a_last_modified <= COALESCE(:modified_before, a_last_modified) 52 | AND a_last_modified >= COALESCE(:modified_after, a_last_modified) 53 | AND a_active = COALESCE(:active, a_active) 54 | AND a_team_id = COALESCE(:team_id, a_team_id) 55 | AND a_incident_contact IS NOT DISTINCT FROM COALESCE(:incident_contact, a_incident_contact) 56 | ) t; 57 | 58 | -- name: search-applications 59 | SELECT a_id as id, 60 | a_team_id as team_id, 61 | a_incident_contact as incident_contact, 62 | a_active as active, 63 | a_name as name, 64 | a_subtitle as subtitle, 65 | a_service_url as service_url, 66 | a_support_url as support_url, 67 | a_scm_url as scm_url, 68 | a_documentation_url as documentation_url, 69 | a_specification_url as specification_url, 70 | a_last_modified as last_modified, 71 | a_last_modified_by as last_modified_by, 72 | a_description as description, 73 | a_specification_type as specification_type, 74 | a_criticality_level as criticality_level, 75 | a_created as created, 76 | a_created_by as created_by, 77 | a_publicly_accessible as publicly_accessible, 78 | ts_rank_cd(vector, query) AS matched_rank, 79 | ts_headline('english', a_id || ' ' || 80 | a_name || ' ' || 81 | COALESCE(a_subtitle, '') || ' ' || 82 | COALESCE(a_description, ''), query) AS matched_description 83 | FROM (SELECT a_id, 84 | a_team_id, 85 | a_incident_contact, 86 | a_active, 87 | a_name, 88 | a_subtitle, 89 | a_service_url, 90 | a_support_url, 91 | a_scm_url, 92 | a_documentation_url, 93 | a_specification_url, 94 | a_last_modified, 95 | a_last_modified_by, 96 | a_description, 97 | a_specification_type, 98 | a_criticality_level, 99 | a_created, 100 | a_created_by, 101 | a_publicly_accessible, 102 | setweight(to_tsvector('english', a_id), 'A') 103 | || setweight(to_tsvector('english', a_name), 'B') 104 | || setweight(to_tsvector('english', COALESCE(a_subtitle, '')), 'C') 105 | || setweight(to_tsvector('english', COALESCE(a_description, '')), 'D') 106 | as vector 107 | FROM zk_data.application 108 | WHERE a_last_modified <= COALESCE(:modified_before, a_last_modified) 109 | AND a_last_modified >= COALESCE(:modified_after, a_last_modified) 110 | AND a_team_id = COALESCE(:team_id, a_team_id) 111 | AND a_incident_contact IS NOT DISTINCT FROM COALESCE(:incident_contact, a_incident_contact) 112 | AND a_active = COALESCE(:active, a_active)) as apps, 113 | plainto_tsquery('english', COALESCE(:searchquery, '')) query 114 | WHERE query @@ vector 115 | ORDER BY matched_rank DESC; 116 | 117 | --name: read-application 118 | SELECT a_id as id, 119 | a_team_id as team_id, 120 | a_incident_contact as incident_contact, 121 | a_active as active, 122 | a_name as name, 123 | a_subtitle as subtitle, 124 | a_description as description, 125 | a_service_url as service_url, 126 | a_scm_url as scm_url, 127 | a_documentation_url as documentation_url, 128 | a_specification_url as specification_url, 129 | a_support_url as support_url, 130 | a_specification_type as specification_type, 131 | a_criticality_level as criticality_level, 132 | a_created as created, 133 | a_created_by as created_by, 134 | a_last_modified as last_modified, 135 | a_last_modified_by as last_modified_by, 136 | a_publicly_accessible as publicly_accessible 137 | FROM zk_data.application 138 | WHERE a_id = :id; 139 | 140 | -- name: create-or-update-application! 141 | WITH application_update AS ( 142 | UPDATE zk_data.application 143 | SET a_team_id = :team_id, 144 | a_incident_contact = :incident_contact, 145 | a_active = :active, 146 | a_name = :name, 147 | a_subtitle = :subtitle, 148 | a_description = :description, 149 | a_service_url = :service_url, 150 | a_scm_url = :scm_url, 151 | a_documentation_url = :documentation_url, 152 | a_specification_url = :specification_url, 153 | a_specification_type = :specification_type, 154 | a_support_url = :support_url, 155 | a_last_modified = NOW(), 156 | a_last_modified_by = :last_modified_by, 157 | a_publicly_accessible = :publicly_accessible, 158 | a_criticality_level = :criticality_level 159 | WHERE a_id = :id 160 | RETURNING *) 161 | INSERT INTO zk_data.application ( 162 | a_id, 163 | a_team_id, 164 | a_incident_contact, 165 | a_active, 166 | a_name, 167 | a_subtitle, 168 | a_description, 169 | a_service_url, 170 | a_scm_url, 171 | a_documentation_url, 172 | a_specification_url, 173 | a_specification_type, 174 | a_support_url, 175 | a_created_by, 176 | a_last_modified_by, 177 | a_publicly_accessible, 178 | a_criticality_level) 179 | SELECT :id, 180 | :team_id, 181 | :incident_contact, 182 | :active, 183 | :name, 184 | :subtitle, 185 | :description, 186 | :service_url, 187 | :scm_url, 188 | :documentation_url, 189 | :specification_url, 190 | :specification_type, 191 | :support_url, 192 | :created_by, 193 | :last_modified_by, 194 | :publicly_accessible, 195 | :criticality_level 196 | WHERE NOT EXISTS (SELECT * FROM application_update); 197 | 198 | -- name: update-application-criticality! 199 | UPDATE zk_data.application 200 | SET 201 | a_last_modified = NOW(), 202 | a_last_modified_by = :last_modified_by 203 | WHERE a_id = :id; 204 | -------------------------------------------------------------------------------- /resources/db/migration/V10__Nullable_criticality_level.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE zk_data.application 3 | ALTER COLUMN a_criticality_level DROP NOT NULL; 4 | 5 | ALTER TABLE zk_data.application ALTER COLUMN a_criticality_level SET DEFAULT NULL; 6 | -------------------------------------------------------------------------------- /resources/db/migration/V11__Add_not_relevant_criticality_level.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE zk_data.application 2 | DROP CONSTRAINT application_a_criticality_level_check 3 | , ADD CONSTRAINT application_a_criticality_level_check CHECK (a_criticality_level >= 1 AND a_criticality_level <= 4); 4 | 5 | -------------------------------------------------------------------------------- /resources/db/migration/V12__Add Support field.sql: -------------------------------------------------------------------------------- 1 | -- adds a new column for storing contact to support for the application 2 | 3 | ALTER TABLE zk_data.application 4 | ADD COLUMN a_support_url TEXT; 5 | -------------------------------------------------------------------------------- /resources/db/migration/V1__Basic schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA zk_data; 2 | SET search_path TO zk_data; 3 | 4 | -- base entity: application 5 | CREATE TABLE application ( 6 | -- the official application ID, like 'kio' or 'pierone' 7 | a_id TEXT NOT NULL, 8 | -- the team ID of the owning team 9 | a_team_id TEXT NOT NULL, 10 | 11 | -- if the application is active 12 | a_active BOOL NOT NULL, 13 | 14 | -- a human readable name, like 'Kio' or 'Pier One' 15 | a_name TEXT NOT NULL, 16 | -- a human readable addition to the name like 'Application Registry' 17 | a_subtitle TEXT, 18 | -- some markdown formatted long description 19 | a_description TEXT, 20 | 21 | -- URL where to access the service if it provides one 22 | a_service_url TEXT, 23 | -- URL to the source code management system if there is some 24 | a_scm_url TEXT, 25 | -- URL to the application's documentation if it provides one 26 | a_documentation_url TEXT, 27 | -- URL to the specification tool if it provides one 28 | a_specification_url TEXT, 29 | 30 | PRIMARY KEY (a_id) 31 | ); 32 | 33 | -- an application's version 34 | CREATE TABLE version ( 35 | -- a unique version of the application like '1.0' 36 | v_id TEXT NOT NULL, 37 | -- the application's ID (see above) 38 | v_application_id TEXT NOT NULL REFERENCES application (a_id), 39 | 40 | -- reference to the used artifact, e.g. the docker image and version like 'docker://stups/kio:1.0' 41 | v_artifact TEXT, 42 | 43 | -- release notes in markdown 44 | v_notes TEXT, 45 | 46 | PRIMARY KEY (v_id, v_application_id) 47 | ); 48 | 49 | -- an approval for an application's version 50 | CREATE TABLE approval ( 51 | -- the application 52 | ap_application_id TEXT NOT NULL, 53 | -- the application's version 54 | ap_version_id TEXT NOT NULL, 55 | -- the approving user 56 | ap_user_id TEXT NOT NULL, 57 | -- what kind of approval 58 | ap_approval_type TEXT NOT NULL, 59 | 60 | -- when this was actually approved 61 | ap_approved_at TIMESTAMP NOT NULL DEFAULT NOW(), 62 | 63 | -- some hints 64 | ap_notes TEXT, 65 | 66 | PRIMARY KEY (ap_application_id, ap_version_id, ap_user_id, ap_approval_type), 67 | FOREIGN KEY (ap_application_id, ap_version_id) REFERENCES version (v_application_id, v_id) 68 | ); 69 | -------------------------------------------------------------------------------- /resources/db/migration/V2__Add v_last_modified.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE zk_data.version 2 | -- when the version was created or last modified 3 | ADD COLUMN v_last_modified TIMESTAMP NOT NULL DEFAULT NOW(); 4 | -------------------------------------------------------------------------------- /resources/db/migration/V3__Add required_approvers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE zk_data.application 2 | -- how many approvers a version needs 3 | ADD COLUMN a_required_approvers SMALLINT DEFAULT 2 CHECK (a_required_approvers > 0); 4 | -------------------------------------------------------------------------------- /resources/db/migration/V4__Add specification_Type.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE zk_data.application 2 | -- where the tickets for the app are managed 3 | ADD COLUMN a_specification_type TEXT; 4 | -------------------------------------------------------------------------------- /resources/db/migration/V5__Add auditing fields.sql: -------------------------------------------------------------------------------- 1 | -- 1) add columns 2 | 3 | -- application auditing 4 | ALTER TABLE zk_data.application 5 | ADD COLUMN a_created TIMESTAMP DEFAULT NOW(), 6 | ADD COLUMN a_created_by TEXT, 7 | ADD COLUMN a_last_modified TIMESTAMP DEFAULT NOW(), 8 | ADD COLUMN a_last_modified_by TEXT; 9 | 10 | -- version auditing 11 | ALTER TABLE zk_data.version 12 | ADD COLUMN v_created TIMESTAMP DEFAULT NOW(), 13 | ADD COLUMN v_created_by TEXT, 14 | ADD COLUMN v_last_modified_by TEXT; 15 | -- last_modified is already there with migration V2 16 | 17 | 18 | -- 2) fill with data 19 | UPDATE zk_data.application 20 | SET a_created = NOW(), 21 | a_created_by = 'kio-migration', 22 | a_last_modified = NOW(), 23 | a_last_modified_by = 'kio-migration'; 24 | 25 | UPDATE zk_data.version 26 | SET v_created = NOW(), 27 | v_created_by = 'kio-migration', 28 | v_last_modified = NOW(), -- this overwrites existing value, but better be consistent 29 | v_last_modified_by = 'kio-migration'; 30 | 31 | -- 3) set columns NOT NULL 32 | ALTER TABLE zk_data.application 33 | ALTER COLUMN a_created SET NOT NULL, 34 | ALTER COLUMN a_created_by SET NOT NULL, 35 | ALTER COLUMN a_last_modified SET NOT NULL, 36 | ALTER COLUMN a_last_modified_by SET NOT NULL; 37 | 38 | ALTER TABLE zk_data.version 39 | ALTER COLUMN v_created SET NOT NULL, 40 | ALTER COLUMN v_created_by SET NOT NULL, 41 | ALTER COLUMN v_last_modified_by SET NOT NULL; 42 | -------------------------------------------------------------------------------- /resources/db/migration/V6__ Add criticality fields.sql: -------------------------------------------------------------------------------- 1 | -- 1) create new field a_criticality_level 2 | 3 | ALTER TABLE zk_data.application 4 | ADD COLUMN a_criticality_level SMALLINT DEFAULT 2 CHECK (a_criticality_level >= 1 AND 5 | a_criticality_level <= 3); 6 | 7 | -- 2) fill it based on a_required_approvers 8 | 9 | UPDATE zk_data.application 10 | SET a_criticality_level = 1 11 | WHERE a_required_approvers = 1; 12 | 13 | UPDATE zk_data.application 14 | SET a_criticality_level = 2 15 | WHERE a_required_approvers > 1; 16 | 17 | -- 3) set new column NOT NULL 18 | 19 | ALTER TABLE zk_data.application 20 | ALTER COLUMN a_criticality_level SET NOT NULL; 21 | 22 | -- TODO in future migration: DROP COLUMN a_required_approvers 23 | -------------------------------------------------------------------------------- /resources/db/migration/V7__Add publicly_accessible field.sql: -------------------------------------------------------------------------------- 1 | -- adds a new column, without locking the table. 2 | 3 | -- 1) create the new field (w/o default and nullable) 4 | 5 | ALTER TABLE zk_data.application 6 | ADD COLUMN a_publicly_accessible BOOLEAN; 7 | 8 | -- 2) set default value 9 | ALTER TABLE zk_data.application 10 | ALTER COLUMN a_publicly_accessible SET DEFAULT false; 11 | 12 | -- 3) migrate missing values 13 | 14 | UPDATE zk_data.application 15 | SET a_publicly_accessible = false 16 | WHERE a_publicly_accessible IS NULL; 17 | 18 | -- 4) set new column NOT NULL 19 | 20 | ALTER TABLE zk_data.application 21 | ALTER COLUMN a_publicly_accessible SET NOT NULL; 22 | -------------------------------------------------------------------------------- /resources/db/migration/V8__Add_incident_contact_field.sql: -------------------------------------------------------------------------------- 1 | -- adds a new column, without locking the table. 2 | 3 | ALTER TABLE zk_data.application 4 | ADD COLUMN a_incident_contact TEXT; 5 | -------------------------------------------------------------------------------- /resources/db/migration/V9__Add_indexes.sql: -------------------------------------------------------------------------------- 1 | -- adds indexes 2 | 3 | CREATE INDEX application_a_last_modified_idx ON zk_data.application(a_last_modified); 4 | CREATE INDEX application_a_active_idx ON zk_data.application(a_active); 5 | CREATE INDEX application_a_team_id_idx ON zk_data.application(a_team_id); 6 | -------------------------------------------------------------------------------- /src/org/zalando/stups/kio/api.clj: -------------------------------------------------------------------------------- 1 | ; Copyright 2015 Zalando SE 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License") 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; http://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | (ns org.zalando.stups.kio.api 16 | (:require [org.zalando.stups.friboo.system.http :refer [def-http-component]] 17 | [org.zalando.stups.friboo.config :refer [require-config]] 18 | [org.zalando.stups.friboo.ring :refer :all] 19 | [org.zalando.stups.friboo.log :as log] 20 | [org.zalando.stups.friboo.user :as u] 21 | [org.zalando.stups.friboo.auth :as auth] 22 | [org.zalando.stups.kio.sql :as sql] 23 | [org.zalando.stups.kio.audit :as audit] 24 | [org.zalando.stups.kio.metrics :as metrics] 25 | [clj-time.coerce :as tcoerce] 26 | [io.sarnowski.swagger1st.util.api :as api] 27 | [ring.util.response :refer :all] 28 | [clojure.string :as str] 29 | [clojure.java.jdbc :refer [with-db-transaction]] 30 | [clojure.core.memoize :as memo] 31 | [clj-http.client :as http] 32 | [cheshire.core :as json])) 33 | 34 | ; define the API component and its dependencies 35 | (def-http-component API "api/kio-api.yaml" [db http-audit-logger app-metrics] :dependencies-as-map true) 36 | 37 | (def default-http-configuration 38 | {:http-port 8080}) 39 | 40 | ; TODO should be replaced with tokeninfo, but requires test changes 41 | (defn from-token 42 | [request field & return-default] 43 | (get-in request 44 | [:tokeninfo field] 45 | (when return-default return-default))) 46 | 47 | (defn tokeninfo 48 | [request] 49 | (clojure.walk/keywordize-keys (:tokeninfo request))) 50 | 51 | (defn require-uid 52 | "Checks whether uid is present on token, throws 403 otherwise" 53 | [request] 54 | (when-not (from-token request "uid") 55 | (log/warn "ACCESS DENIED (unauthorized) because no uid in tokeninfo.") 56 | (api/throw-error 403 "Unauthorized"))) 57 | 58 | (defn is-admin-in-realm? 59 | [uid realm {:keys [configuration]}] 60 | (when (and uid realm) 61 | (let [uid-with-realm (str realm "/" uid) 62 | allowed-uids-with-realm (or (:admin-users configuration) "") 63 | allowed (set (str/split allowed-uids-with-realm #","))] 64 | (allowed uid-with-realm)))) 65 | 66 | (defn require-write-authorization 67 | "If user is employee, check that is in correct team. 68 | If user is service, check that it has application.write scope and is correct team. 69 | If user is listed as admin grant access to user" 70 | [request team] 71 | (require-uid request) 72 | 73 | (let [realm (str "/" (u/require-realms #{"employees" "services"} request)) 74 | uid (from-token request "uid") 75 | is-admin? (is-admin-in-realm? uid realm request)] 76 | (when-not is-admin? 77 | (let [has-auth? (auth/get-auth request team) 78 | is-robot? (= "/services" realm) 79 | has-scope? (set (from-token request "scope"))] 80 | (when-not has-auth? 81 | (api/throw-error 403 "Unauthorized")) 82 | (when (and 83 | is-robot? 84 | (not (has-scope? "application.write"))) 85 | (api/throw-error 403 "Unauthorized")))))) 86 | 87 | ;; applications 88 | 89 | ;;https://github.com/clojure/core.memoize/blob/master/docs/Using.md#overriding-the-cache-keys 90 | (defn ^{:clojure.core.memoize/args-fn first} 91 | read-applications-into-string 92 | [params db-spec] 93 | (let [result (sql/cmd-read-applications params {:connection db-spec})] 94 | (when (not-empty result) 95 | (json/generate-string result)))) 96 | 97 | (def read-application-memo 98 | (memo/ttl #'read-applications-into-string :ttl/threshold 120000)) 99 | 100 | (defn read-applications 101 | [{:keys [search modified_before modified_after team_id incident_contact active]} request {:keys [db]}] 102 | (u/require-realms #{"employees" "services"} request) 103 | (let [conn {:connection db} 104 | params {:searchquery search 105 | :team_id team_id 106 | :incident_contact incident_contact 107 | :active active 108 | :modified_before (tcoerce/to-sql-time modified_before) 109 | :modified_after (tcoerce/to-sql-time modified_after)}] 110 | (if (nil? search) 111 | (do 112 | (log/debug "Read all applications.") 113 | (-> (if (and (nil? team_id) (nil? incident_contact)) 114 | (read-application-memo params db) 115 | (sql/cmd-read-applications params conn)) 116 | (response) 117 | (content-type-json))) 118 | (do 119 | (log/debug "Search in applications with term %s." search) 120 | (-> (sql/cmd-search-applications params conn) 121 | (response) 122 | (content-type-json)))))) 123 | 124 | (defn load-application 125 | "Loads a single application by ID, used for team checks." 126 | [application_id db] 127 | (-> (sql/cmd-read-application {:id application_id} 128 | {:connection db}) 129 | (first))) 130 | 131 | (defn enrich-application 132 | "Adds calculated field(s) to an application" 133 | [application] 134 | (assoc application :required_approvers (if (= (:criticality_level application) 1) 1 2))) 135 | 136 | (defn enrich-applications 137 | [applications] 138 | (map enrich-application applications)) 139 | 140 | (defn read-application [{:keys [application_id]} request {:keys [db]}] 141 | (u/require-realms #{"employees" "services"} request) 142 | (log/debug "Read application %s." application_id) 143 | (-> (sql/cmd-read-application {:id application_id} {:connection db}) 144 | (enrich-applications) 145 | (single-response) 146 | (content-type-json))) 147 | 148 | (defn team-exists? [request team] 149 | (when-not (str/blank? team) 150 | (let [magnificent-url (get-in request [:configuration :magnificent-url]) 151 | token (get-in request [:tokeninfo "access_token"]) 152 | response (http/get 153 | (str magnificent-url "/teams/" team) 154 | {:content-type :json 155 | :oauth-token token 156 | :throw-exceptions false}) 157 | status (:status response)] 158 | (= 200 status)))) 159 | 160 | (defn default-fields [creator-user-id] 161 | {:incident_contact nil 162 | :specification_url nil 163 | :documentation_url nil 164 | :subtitle nil 165 | :scm_url nil 166 | :support_url nil 167 | :service_url nil 168 | :description nil 169 | :specification_type nil 170 | :publicly_accessible false 171 | :criticality_level nil 172 | :created_by creator-user-id}) 173 | 174 | (defn- value-not-nil? [[_ v]] (some? v)) 175 | 176 | (defn created-or-updated-app [app-id old-app new-app user-id] 177 | {:pre [(map? new-app) 178 | (not (clojure.string/blank? app-id)) 179 | (not (clojure.string/blank? user-id))] 180 | :post [(map? %) 181 | (seq %) 182 | (= (:last_modified_by %) user-id) 183 | (or (some? old-app) 184 | (= (:created_by %) user-id)) 185 | (= (:id %) app-id)]} 186 | (let [old-app (or old-app (default-fields user-id)) 187 | new-app (into {} (filter value-not-nil? new-app)) 188 | merged-fields (merge old-app new-app)] 189 | (assoc merged-fields 190 | :id app-id 191 | :last_modified_by user-id))) 192 | 193 | (defn create-or-update-application! [{:keys [application application_id]} request {:keys [db http-audit-logger]}] 194 | (let [uid (from-token request "uid") 195 | existing_application (load-application application_id db) 196 | existing_team_id (:team_id existing_application) 197 | team_id (:team_id application)] 198 | 199 | (if (nil? existing_application) 200 | (require-write-authorization request team_id) 201 | (require-write-authorization request existing_team_id)) 202 | 203 | (if (or (= team_id existing_team_id) 204 | (team-exists? request (:team_id application))) 205 | (try 206 | (let [app-to-save (created-or-updated-app application_id existing_application application uid) 207 | log-fn (:log-fn http-audit-logger)] 208 | (sql/cmd-create-or-update-application! app-to-save {:connection db}) 209 | (log-fn (audit/app-modified 210 | (tokeninfo request) 211 | app-to-save)) 212 | (log/audit "Created/updated application %s using data %s." application_id application) 213 | (response nil)) 214 | (catch AssertionError err 215 | (-> {:message (format "Internal inconsistency: %s" (.getMessage err)) 216 | :application_id application_id 217 | :old-app existing_application 218 | :new-app application 219 | :uid uid} 220 | (response) 221 | (status 500) 222 | (content-type-json)))) 223 | (-> {:message (format "Team %s does not exist." team_id)} 224 | (response) 225 | (status 400) 226 | (content-type-json))))) 227 | 228 | (defn read-application-approvals [_ request {:keys [app-metrics]}] 229 | (metrics/mark-deprecation app-metrics :deprecation-application-approvals-get) 230 | (u/require-internal-user request) 231 | (->> [] 232 | (response) 233 | (content-type-json))) 234 | 235 | ;; versions 236 | 237 | (defn read-versions-by-application [_ request {:keys [app-metrics]}] 238 | (metrics/mark-deprecation app-metrics :deprecation-versions-get) 239 | (u/require-realms #{"employees" "services"} request) 240 | (-> [] 241 | (response) 242 | (content-type-json))) 243 | 244 | (defn read-version-by-application [_ request {:keys [app-metrics]}] 245 | (metrics/mark-deprecation app-metrics :deprecation-version-get) 246 | (u/require-realms #{"employees" "services"} request) 247 | (-> (not-found {}) 248 | (content-type-json))) 249 | 250 | (defn create-or-update-version! [{:keys [application_id]} request {:keys [db app-metrics]}] 251 | (metrics/mark-deprecation app-metrics :deprecation-version-put) 252 | (if-let [application (load-application application_id db)] 253 | (do 254 | (require-write-authorization request (:team_id application)) 255 | (response nil)) 256 | (api/error 404 "application not found"))) 257 | 258 | ;; approvals 259 | 260 | (defn read-approvals-by-version [_ request {:keys [db app-metrics]}] 261 | (metrics/mark-deprecation app-metrics :deprecation-version-approvals-get) 262 | (u/require-realms #{"employees" "services"} request) 263 | (-> [] 264 | (response) 265 | (content-type-json))) 266 | 267 | (defn approve-version! [{:keys [application_id]} request {:keys [db app-metrics]}] 268 | (metrics/mark-deprecation app-metrics :deprecation-version-approvals-put) 269 | (if-let [application (load-application application_id db)] 270 | (do 271 | (u/require-internal-team (:team_id application) request) 272 | (response nil)) 273 | (api/error 404 "application not found"))) 274 | -------------------------------------------------------------------------------- /src/org/zalando/stups/kio/audit.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.audit 2 | (:require [clj-time.format :as tf] 3 | [clj-time.core :as t])) 4 | 5 | (def date-formatter 6 | (tf/formatters :date-time)) 7 | 8 | (defn get-date 9 | [] 10 | (tf/unparse date-formatter (t/now))) 11 | 12 | (defn drop-nil-values 13 | [record] 14 | (into {} (remove (comp nil? second) record))) 15 | 16 | (defn app-modified 17 | [tokeninfo app] 18 | {:event_type {:namespace "cloud.zalando.com" 19 | :name "application-modified" 20 | :version "3"} 21 | :triggered_at (get-date) 22 | :triggered_by {:type "USER" 23 | :id (:uid tokeninfo) 24 | :additional {:realm (:realm tokeninfo)}} 25 | :payload {:application (drop-nil-values (merge 26 | {:id (:id app) 27 | :repo_url (:scm_url app) 28 | :app_url (:service_url app) 29 | :owner (:team_id app)} 30 | (dissoc app 31 | :id 32 | :scm_url 33 | :service_url 34 | :team_id)))}}) 35 | -------------------------------------------------------------------------------- /src/org/zalando/stups/kio/core.clj: -------------------------------------------------------------------------------- 1 | ; Copyright 2015 Zalando SE 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License") 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; http://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | (ns org.zalando.stups.kio.core 16 | (:require [com.stuartsierra.component :refer [using system-map]] 17 | [org.zalando.stups.friboo.config :as config] 18 | [org.zalando.stups.friboo.system :as system] 19 | [org.zalando.stups.kio.sql :as sql] 20 | [org.zalando.stups.kio.api :as api] 21 | [org.zalando.stups.kio.metrics :as app-metrics] 22 | [org.zalando.stups.friboo.system.oauth2 :as o2] 23 | [org.zalando.stups.friboo.system.audit-logger.http :as http-logger] 24 | [org.zalando.stups.friboo.log :as log]) 25 | (:gen-class)) 26 | 27 | (defn run 28 | "Initializes and starts the whole system." 29 | [default-configuration] 30 | (let [configuration (config/load-configuration 31 | (system/default-http-namespaces-and :db :oauth2 :httplogger) 32 | [sql/default-db-configuration 33 | api/default-http-configuration 34 | default-configuration]) 35 | 36 | system (system/http-system-map configuration 37 | api/map->API [:db :http-audit-logger :app-metrics] 38 | :db (sql/map->DB {:configuration (:db configuration)}) 39 | :app-metrics (using 40 | (app-metrics/map->DeprecationMetrics {}) 41 | [:metrics]) 42 | :http-audit-logger (using 43 | (http-logger/map->HTTP {:configuration (assoc (:httplogger configuration) 44 | :token-name "http-audit-logger")}) 45 | [:tokens]) 46 | :tokens (o2/map->OAuth2TokenRefresher {:configuration (:oauth2 configuration) 47 | :tokens {"http-audit-logger" ["uid"]}}))] 48 | 49 | (system/run configuration system))) 50 | 51 | (defn -main 52 | "The actual main for our uberjar." 53 | [& args] 54 | (try 55 | (run {}) 56 | (catch Exception e 57 | (log/error e "Could not start system because of %s." (str e)) 58 | (System/exit 1)))) 59 | -------------------------------------------------------------------------------- /src/org/zalando/stups/kio/metrics.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.metrics 2 | (:require [metrics.meters :as meters] 3 | [com.stuartsierra.component :as component])) 4 | 5 | (defn mark-deprecation [component metric] 6 | (meters/mark! (metric component))) 7 | 8 | (defrecord DeprecationMetrics [metrics] 9 | component/Lifecycle 10 | 11 | (start [component] 12 | (let [{:keys [metrics-registry]} metrics] 13 | (assoc component 14 | :deprecation-versions-get (meters/meter metrics-registry "deprecation.versions.get") 15 | :deprecation-version-get (meters/meter metrics-registry "deprecation.version.get") 16 | :deprecation-version-put (meters/meter metrics-registry "deprecation.version.put") 17 | :deprecation-application-approvals-get (meters/meter metrics-registry "deprecation.application-approvals.get") 18 | :deprecation-version-approvals-get (meters/meter metrics-registry "deprecation.version-approvals.get") 19 | :deprecation-version-approvals-put (meters/meter metrics-registry "deprecation.version-approvals.put")))) 20 | (stop [component] 21 | component)) 22 | -------------------------------------------------------------------------------- /src/org/zalando/stups/kio/sql.clj: -------------------------------------------------------------------------------- 1 | ; Copyright 2015 Zalando SE 2 | ; 3 | ; Licensed under the Apache License, Version 2.0 (the "License") 4 | ; you may not use this file except in compliance with the License. 5 | ; You may obtain a copy of the License at 6 | ; 7 | ; http://www.apache.org/licenses/LICENSE-2.0 8 | ; 9 | ; Unless required by applicable law or agreed to in writing, software 10 | ; distributed under the License is distributed on an "AS IS" BASIS, 11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ; See the License for the specific language governing permissions and 13 | ; limitations under the License. 14 | 15 | (ns org.zalando.stups.kio.sql 16 | (:require [environ.core :as env] 17 | [yesql.core :refer [defqueries]] 18 | [org.zalando.stups.friboo.system.db :refer [def-db-component generate-hystrix-commands]])) 19 | 20 | ;;USE env variable AUTO_MIGRATION to configure auto-migration? 21 | (def-db-component DB :auto-migration? (Boolean/parseBoolean (env/env :auto-migration))) 22 | 23 | (def default-db-configuration 24 | {:db-classname "org.postgresql.Driver" 25 | :db-subprotocol "postgresql" 26 | :db-subname "//localhost:5432/kio" 27 | :db-user "postgres" 28 | :db-password "postgres" 29 | :db-init-sql "SET search_path TO zk_data, public"}) 30 | 31 | (defqueries "db/applications.sql") 32 | (generate-hystrix-commands) 33 | -------------------------------------------------------------------------------- /test/org/zalando/stups/kio/integration_test/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.integration-test.api-test 2 | (:require [com.stuartsierra.component :as component] 3 | [clojure.test :refer [deftest is testing]] 4 | [midje.sweet :refer :all] 5 | [clj-http.client :as http] 6 | [cheshire.core :as json] 7 | [org.zalando.stups.kio.core :refer [run]] 8 | [org.zalando.stups.kio.api :as api] 9 | [org.zalando.stups.friboo.system.oauth2 :as oauth2])) 10 | 11 | (defn api-url [& path] 12 | (let [url (apply str "http://localhost:8080" path)] 13 | (println (str "[request] " url)) 14 | url)) 15 | 16 | (defrecord NoTokenRefresher 17 | [configuration] 18 | com.stuartsierra.component/Lifecycle 19 | (start [this] this) 20 | (stop [this] this)) 21 | 22 | (deftest ^:integration integration-test 23 | (with-redefs [api/require-write-authorization (constantly nil) 24 | oauth2/map->OAuth2TokenRefresher map->NoTokenRefresher 25 | oauth2/access-token (constantly "token") 26 | api/from-token (constantly "barfoo")] 27 | 28 | (let [system (run {}) 29 | request-options {:throw-exceptions false 30 | :content-type :json} 31 | application {:team_id "bar" 32 | :active true 33 | :name "FooBar"} 34 | version {:notes "My new version" 35 | :artifact "docker://stups/foo1:1.0-master"} 36 | version-req (-> 37 | request-options 38 | (assoc :body (json/encode version)) 39 | (assoc :as :json))] 40 | (against-background [(before :contents (http/put 41 | (api-url "/apps/foo1") 42 | (assoc request-options :body (json/encode application)))) 43 | (after :contents (component/stop system))] 44 | (facts "API" 45 | (facts "it validates version numbers correctly" 46 | (fact "1.0-master works" 47 | (http/put 48 | (api-url "/apps/foo1/versions/1.0-master") 49 | version-req) => (contains {:status 200})) 50 | 51 | (fact "1.0 works" 52 | (http/put 53 | (api-url "/apps/foo1/versions/1.0") 54 | version-req) => (contains {:status 200})) 55 | 56 | (fact "1..-..1 does not work" 57 | (http/put 58 | (api-url "/apps/foo1/versions/1..-..1") 59 | version-req) => (contains {:status 400})))))))) 60 | -------------------------------------------------------------------------------- /test/org/zalando/stups/kio/unit_test/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.unit-test.api-test 2 | (:require [clojure.test :as t] 3 | [midje.sweet :refer :all] 4 | [org.zalando.stups.kio.core :refer [run]] 5 | [org.zalando.stups.kio.sql :as sql] 6 | [org.zalando.stups.kio.api :as api] 7 | [org.zalando.stups.kio.metrics :as metrics] 8 | [org.zalando.stups.friboo.user :as u] 9 | [org.zalando.stups.friboo.auth :as auth])) 10 | 11 | (defn with-status? 12 | "Checks if exception has status-code in ex-data" 13 | [status] 14 | (fn [e] 15 | (let [data (ex-data e)] 16 | (= status (:http-code data))))) 17 | 18 | (t/deftest ^:unit enrich-application 19 | (facts "enrich-application" 20 | (fact "it properly sets required_approvers" 21 | (api/enrich-application {:criticality_level 1}) => (contains {:required_approvers 1}) 22 | (api/enrich-application {:criticality_level 2}) => (contains {:required_approvers 2}) 23 | (api/enrich-application {:criticality_level 3}) => (contains {:required_approvers 2})))) 24 | 25 | (t/deftest ^:unit test-read-access 26 | (facts "read stuff" 27 | (fact "applications without search - cached empty response [will fail on rerun in the same REPL => db call is memoized]" 28 | (api/read-applications {} .request. {:db .db.}) =not=> (contains {:body []}) 29 | (provided 30 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 31 | "realm" "employees"}} 32 | (sql/cmd-read-applications anything {:connection .db.}) => [])) 33 | (fact "applications without search - service request" 34 | (api/read-applications {:team_id "foo"} .request. {:db .db.}) => (contains {:body [{:test 1}]}) 35 | (provided 36 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 37 | "realm" "services"}} 38 | (sql/cmd-read-applications anything {:connection .db.}) => [{:test 1}])) 39 | (fact "applications with search" 40 | (api/read-applications .params. .request. {:db .db.}) =not=> (throws Exception) 41 | (provided 42 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 43 | "realm" "employees"}} 44 | .params. =contains=> {:search "foo bar"} 45 | (sql/cmd-search-applications anything {:connection .db.}) => [])) 46 | (fact "applications with search" 47 | (api/read-applications .params. .request. {:db .db.}) => (contains {:body []}) 48 | (provided 49 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 50 | "realm" "services"}} 51 | .params. =contains=> {:search "foo bar"} 52 | (sql/cmd-search-applications anything {:connection .db.}) => [])) 53 | (fact "single app - service request [will fail on rerun in the same REPL => db call is memoized]" 54 | (api/read-application {:application_id "foo"} .request. {:db .db.}) => (contains {:body {:id "foo" :required_approvers 2}}) 55 | (provided 56 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 57 | "realm" "services"}} 58 | (sql/cmd-read-application anything {:connection .db.}) => [{:id "foo"}])) 59 | (fact "single app" 60 | (api/read-application {:application_id "foo1"} .request. {:db .db.}) => (contains {:body {} :status 404}) 61 | (provided 62 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 63 | "realm" "services"}} 64 | (sql/cmd-read-application anything {:connection .db.}) => [])))) 65 | 66 | (t/deftest ^:unit test-write-application 67 | (facts "writing applications" 68 | (fact "when updating application, the team in db is compared" 69 | (api/create-or-update-application! .params. .request. {:db .db. :http-audit-logger .logger.}) =not=> (throws Exception) 70 | (provided 71 | .logger. =contains=> {:log-fn identity} 72 | .params. =contains=> {:application_id "app-id" 73 | :application {:team_id .api-team-id. 74 | :id "app-id" 75 | :active true 76 | :name "test"}} 77 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 78 | "realm" "/employees"}} 79 | (api/load-application "app-id" .db.) => {:team_id .db-team-id. :id "app-id"} 80 | (sql/cmd-create-or-update-application! anything {:connection .db.}) => nil 81 | (api/team-exists? .request. .api-team-id.) => true 82 | (api/require-write-authorization .request. .db-team-id.) => nil)) 83 | 84 | (fact "when creating application, the team in body is compared" 85 | (api/create-or-update-application! .params. .request. {:db .db. :http-audit-logger .logger.}) =not=> (throws Exception) 86 | (provided 87 | .logger. =contains=> {:log-fn identity} 88 | .params. =contains=> {:application_id "app-id" 89 | :application {:team_id .api-team-id. 90 | :active true 91 | :name "test"}} 92 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 93 | "realm" "/employees"}} 94 | (api/load-application "app-id" .db.) => nil 95 | (sql/cmd-create-or-update-application! anything {:connection .db.}) => nil 96 | (api/team-exists? .request. .api-team-id.) => true 97 | (api/require-write-authorization .request. .api-team-id.) => nil)) 98 | 99 | (fact "when creating/updating application, the team is checked" 100 | (api/create-or-update-application! .params. .request. {:db .db. :http-audit-logger .logger.}) => (throws Exception) 101 | (provided 102 | .logger. =contains=> {:log-fn identity} 103 | .params. =contains=> {:application_id "app-id" 104 | :application {:team_id nil 105 | :active true 106 | :name "test"}} 107 | .request. =contains=> {:tokeninfo {"uid" "nikolaus" 108 | "realm" "/employees"}} 109 | (api/load-application "app-id" .db.) => nil)) 110 | 111 | (fact "when creating/updating application, the team is checked" 112 | (api/create-or-update-application! .params. .request. {:db .db. :http-audit-logger .logger.}) => (throws Exception) 113 | (provided 114 | .logger. =contains=> {:log-fn identity} 115 | .params. =contains=> {:application_id "app-id" 116 | :application {:team_id " " 117 | :active true 118 | :name "test"}} 119 | .request. =contains=> {:configuration {:magnificent-url .magnificent-url.} 120 | :tokeninfo {"uid" "nikolaus" 121 | "realm" "/employees"}} 122 | (api/load-application "app-id" .db.) => nil)))) 123 | 124 | (t/deftest ^:unit test-require-write-access 125 | (facts "write access" 126 | (fact "access is denied if the uid is missing" 127 | (api/require-write-authorization .request. .team.) => (throws Exception anything (with-status? 403)) 128 | (provided 129 | .request. =contains=> {:tokeninfo {"realm" "/services" 130 | "scope" ["uid" "application.write"]} 131 | :configuration {:admin-users "/services/stups_robot,foo"}})) 132 | 133 | (fact "access is denied if the realm is neither services nor employees" 134 | (api/require-write-authorization .request. .team.) => (throws Exception anything (with-status? 403)) 135 | (provided 136 | .request. =contains=> {:tokeninfo {"realm" "/somerealm" 137 | "scope" ["uid" "application.write"]} 138 | :configuration {:admin-users "/services/stups_robot,foo"}})) 139 | 140 | (fact "a robot does get access if it has write scope and required uid" 141 | (api/require-write-authorization .request. .team.) => nil 142 | (provided 143 | .request. =contains=> {:tokeninfo {"realm" "/services" 144 | "uid" "stups_robot" 145 | "scope" ["uid" "application.write"]}} 146 | (auth/get-auth .request. .team.) => true)) 147 | 148 | (fact "a robot does get access if it is configured as admin user" 149 | (api/require-write-authorization .request. .team.) => nil 150 | (provided 151 | .request. =contains=> {:tokeninfo {"realm" "/services" 152 | "uid" "stups_robot" 153 | "scope" ["uid"]} 154 | :configuration {:admin-users "/services/stups_robot,foo"}})) 155 | 156 | (fact "a human does get access if it is configured as admin user" 157 | (api/require-write-authorization .request. .team.) => nil 158 | (provided 159 | .request. =contains=> {:tokeninfo {"realm" "/employees" 160 | "uid" "stups_robot" 161 | "scope" ["uid"]} 162 | :configuration {:admin-users "/employees/stups_robot,foo"}})) 163 | 164 | (fact "a human does not get access if it is configured as admin user in wrong realm" 165 | (api/require-write-authorization .request. .team.) => (throws Exception anything (with-status? 403)) 166 | (provided 167 | .request. =contains=> {:tokeninfo {"realm" "/employees" 168 | "uid" "stups_robot" 169 | "scope" ["uid"]} 170 | :configuration {:admin-users "/wrongrealm/stups_robot,foo"}} 171 | (auth/get-auth .request. .team.) => false)) 172 | 173 | (fact "a human does get access if it is in the required team" 174 | (api/require-write-authorization .request. .team.) => nil 175 | (provided 176 | .request. =contains=> {:tokeninfo {"realm" "/employees" 177 | "uid" "npiccolotto" 178 | "scope" ["uid"]}} 179 | (auth/get-auth .request. .team.) => true) 180 | 181 | (fact "a human does not get access if it is not in the required team" 182 | (api/require-write-authorization .request. .team.) => (throws Exception (with-status? 403)) 183 | (provided 184 | .request. =contains=> {:tokeninfo {"realm" "/employees" 185 | "uid" "npiccolotto" 186 | "scope" ["uid"]}} 187 | (auth/get-auth .request. .team.) => false)) 188 | 189 | (fact "a robot can not write without write scope" 190 | (api/require-write-authorization .request. .team.) => (throws Exception (with-status? 403)) 191 | (provided 192 | .request. =contains=> {:tokeninfo {"realm" "/services" 193 | "uid" "robobro" 194 | "scope" ["uid"]}} 195 | (auth/get-auth .request. .team.) => true)) 196 | 197 | (fact "a robot can write with write scope and correct team" 198 | (api/require-write-authorization .request. .team.) => nil 199 | (provided 200 | .request. =contains=> {:tokeninfo {"realm" "/services" 201 | "uid" "robobro" 202 | "scope" ["uid" "application.write"]}} 203 | (auth/get-auth .request. .team.) => true)) 204 | 205 | (fact "a robot can not write with write scope to another team" 206 | (api/require-write-authorization .request. .team.) => (throws Exception (with-status? 403)) 207 | (provided 208 | .request. =contains=> {:tokeninfo {"realm" "/services" 209 | "uid" "robobro" 210 | "scope" ["uid"]}} 211 | (auth/get-auth .request. .team.) => false)) 212 | 213 | (fact "a robot can write applications" 214 | (api/create-or-update-application! .params. .request. {:db .db. :http-audit-logger .logger.}) => (contains {:status 200}) 215 | (provided 216 | .logger. =contains=> {:log-fn identity} 217 | .request. =contains=> {:tokeninfo {"uid" "robobro" 218 | "realm" "/services" 219 | "scope" ["uid" "application.write"]}} 220 | .params. =contains=> {:application {:team_id .team-id.} 221 | :application_id "app-id"} 222 | (auth/get-auth .request. .team-id.) => true 223 | (api/load-application "app-id" .db.) => {:team_id .team-id. 224 | :id "app-id"} 225 | (sql/cmd-create-or-update-application! anything {:connection .db.}) => nil)) 226 | 227 | (fact "a robot can write versions" 228 | (api/create-or-update-version! .params. .request. {:db .db. :app-metrics .app-metrics.}) =not=> (throws Exception) 229 | (provided 230 | .params. =contains=> {:version .version. 231 | :version_id .version-id. 232 | :application_id "app-id"} 233 | .request. =contains=> {:tokeninfo {"uid" "robobro" 234 | "realm" "/services" 235 | "scope" ["uid" "application.write"]}} 236 | .version. =contains=> {:id .version-id.} 237 | (auth/get-auth .request. .team-id.) => true 238 | (metrics/mark-deprecation .app-metrics. :deprecation-version-put) => nil 239 | (api/load-application "app-id" .db.) => {:team_id .team-id. 240 | :id "app-id"})) 241 | 242 | (fact "a robot can not write approvals" 243 | (api/approve-version! .params. .request. {:db .db. :app-metrics .app-metrics.}) => (throws Exception (with-status? 403)) 244 | (provided 245 | .request. =contains=> {:tokeninfo {"uid" "robobro" 246 | "realm" "/services" 247 | "scope" ["uid" "application.write"]} 248 | :configuration {:service-user-url "http://robot.com" 249 | :magnificent-url "magnificent-url"}} 250 | .params. =contains=> {:version_id .version-id. 251 | :application_id "app-id" 252 | :notes .notes.} 253 | (metrics/mark-deprecation .app-metrics. :deprecation-version-approvals-put) => nil 254 | (api/load-application "app-id" .db.) => {:team_id .team-id. 255 | :id "app-id"})) 256 | 257 | (fact "a human can write approvals" 258 | (api/approve-version! .params. .request. {:db .db. :app-metrics .app-metrics.}) =not=> (throws Exception) 259 | (provided 260 | .request. =contains=> {:tokeninfo {"uid" .uid. 261 | "realm" "/employees" 262 | "scope" ["uid"]} 263 | :configuration {:team-service-url "http://employee.com" 264 | :magnificent-url "magnificent-url"}} 265 | .params. =contains=> {:version_id .version-id. 266 | :application_id "app-id" 267 | :approval {:notes .notes.}} 268 | 269 | (u/require-internal-team .team-id. .request.) => nil 270 | (metrics/mark-deprecation .app-metrics. :deprecation-version-approvals-put) => nil 271 | (api/load-application "app-id" .db.) => {:team_id .team-id. 272 | :id "app-id"}))))) 273 | 274 | (t/deftest ^:unit created-or-updated-app 275 | (t/is (= {:id "x" :a nil :b 2 :c 3 :last_modified_by "bob"} 276 | (api/created-or-updated-app "x" {:a nil :b 2} {:b nil :c 3} "bob")) 277 | "should not update fields to nil") 278 | (t/is (= {:id "x" :a true :b false :c true :d false :last_modified_by "bob"} 279 | (api/created-or-updated-app "x" 280 | {:a false :b true :c true :d false} 281 | {:a true :b false} 282 | "bob")) 283 | "boolean fields should be updated to match their value in new-app") 284 | (t/is (= {:id "x" :a 1 :last_modified_by "bar"} 285 | (api/created-or-updated-app "x" {:last_modified_by "foo"} {:a 1} "bar")) 286 | ":last_modified_by should be updated") 287 | (t/is (= {:id "x" :a 1 :b 2 :last_modified_by "bob"} 288 | (api/created-or-updated-app "x" {:id "y" :a 1} {:id "z" :b 2} "bob")) 289 | ":id should never be updated")) -------------------------------------------------------------------------------- /test/org/zalando/stups/kio/unit_test/audit_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.unit-test.audit-test 2 | (:require [midje.sweet :refer :all] 3 | [clojure.test :refer [deftest]] 4 | [org.zalando.stups.kio.audit :as audit])) 5 | 6 | (def tokeninfo 7 | {:uid "uid" 8 | :realm "realm"}) 9 | 10 | (def app 11 | {:id "id" 12 | :scm_url "scm_url" 13 | :service_url "service_url" 14 | :team_id "team_id" 15 | :nil-value nil 16 | :additional "additional"}) 17 | 18 | (def app-modified 19 | (audit/app-modified tokeninfo app)) 20 | 21 | (deftest ^:unit test-audit 22 | (facts "app-modified" 23 | (fact "creates correct envelope" 24 | app-modified => (just {:triggered_at anything 25 | :triggered_by anything 26 | :event_type anything 27 | :payload anything}) 28 | (:triggered_at app-modified) => #"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z" 29 | (:event_type app-modified) => (just {:namespace "cloud.zalando.com" 30 | :name "application-modified" 31 | :version "3"}) 32 | (:triggered_by app-modified) => (just {:type "USER" 33 | :id "uid" 34 | :additional {:realm "realm"}})) 35 | (fact "creates correct payload" 36 | (:payload app-modified) => (just {:application {:id (:id app) 37 | :repo_url (:scm_url app) 38 | :app_url (:service_url app) 39 | :owner (:team_id app) 40 | :additional (:additional app)}})))) 41 | -------------------------------------------------------------------------------- /test/org/zalando/stups/kio/unit_test/db_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.zalando.stups.kio.unit-test.db-test 2 | (:require [midje.sweet :refer :all] 3 | [clojure.test :refer [deftest]] 4 | [com.stuartsierra.component :as component] 5 | [org.zalando.stups.kio.sql :as sql])) 6 | 7 | (def config {:classname "org.postgresql.Driver" 8 | :subprotocol "postgresql" 9 | :subname "//localhost:5432/postgres" 10 | :user "postgres" 11 | :password "postgres" 12 | :init-sql "SET search_path TO zk_data, public"}) 13 | 14 | (deftest ^:unit apply-migrations 15 | (fact "run migrations against db so that docs can be generated" 16 | (component/stop (org.zalando.stups.friboo.system.db/start-component (sql/map->DB {:configuration config}) true)))) 17 | --------------------------------------------------------------------------------