├── .cfignore ├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .markdownlint.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SIEMENSAPINOTICE.md ├── agent ├── .gitignore ├── README.md ├── agentconfig.sample.json ├── assetdata.sample.json ├── index.js ├── package.json └── yarn.lock ├── angular.json ├── devops ├── README.md ├── build_mdsp_zip.sh ├── devopsadmin │ ├── .cfignore │ ├── config.js │ ├── deploy_dev.sh │ ├── index.js │ ├── package.json │ ├── services │ │ ├── index.js │ │ ├── notification.js │ │ ├── templateEmail.html │ │ └── templateSms.html │ └── yarn.lock ├── grafana │ ├── .cfignore │ ├── .gitignore │ ├── README.md │ ├── deploy_dev.sh │ └── sample-conf │ │ ├── defaults.custom.ini │ │ └── provisioning │ │ ├── dashboards │ │ ├── default.yaml │ │ └── json │ │ │ └── .gitkeep │ │ └── datasources │ │ └── prometheus.yaml ├── manifest.yml ├── prometheus │ ├── .cfignore │ ├── .gitignore │ ├── README.md │ ├── conf │ │ └── prometheus.yml.sample │ └── deploy_dev.sh └── vars-file.yml.sample ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ ├── app.po.ts │ ├── mdsp.e2e-spec.ts │ ├── todo.e2e-spec.ts │ └── userinfo.e2e-spec.ts └── tsconfig.e2e.json ├── images ├── architecture.png ├── devopsadmin-grafana.png ├── todo-api-docs.png └── todo.png ├── manifest.yml ├── package.json ├── proxy.conf.js ├── server ├── model.js ├── openapi-spec-urls.js ├── package.json ├── server.js ├── specs │ └── api.yaml └── yarn.lock ├── src ├── app │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── interceptors │ │ └── auth.interceptor.ts │ ├── mindsphere.service.mock.ts │ ├── mindsphere.service.ts │ ├── tenantinfo.ts │ ├── todo.service.mock.ts │ ├── todo.service.ts │ ├── todo.ts │ ├── todos │ │ ├── todos.component.html │ │ ├── todos.component.spec.ts │ │ └── todos.component.ts │ ├── userinfo.ts │ └── userinfo │ │ ├── userinfo.component.html │ │ ├── userinfo.component.scss │ │ ├── userinfo.component.spec.ts │ │ └── userinfo.component.ts ├── assets │ ├── favicon │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── mdsp-app-info.json ├── browserslist ├── environments │ ├── .gitignore │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tools ├── README.md └── cf-ssh.sh ├── tsconfig.json ├── tslint.json └── yarn.lock /.cfignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /coverage/ 8 | /server/static/ 9 | /test-reports/ 10 | 11 | # MindSphere Operator zip 12 | /todo.zip 13 | /devops/build_zip/ 14 | /devops/devopsadmin.zip 15 | 16 | # NPM and Yarn 17 | node_modules/ 18 | npm-debug.log 19 | yarn-debug.log 20 | yarn-error.log 21 | 22 | # IDEs and editors 23 | /.idea/ 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | /.vscode/* 33 | !/.vscode/settings.json 34 | !/.vscode/tasks.json 35 | !/.vscode/launch.json 36 | !/.vscode/extensions.json 37 | 38 | # misc 39 | /.sass-cache 40 | /connect.lock 41 | /libpeerconnection.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright Siemens AG 2018 2 | # SPDX-License-Identifier: MIT 3 | 4 | # The following CI/CD variables are required: 5 | # - CF_ORG 6 | # - CF_SPACE_DEV 7 | # - CF_API 8 | # - CF_ACCESS_TOKEN 9 | # - CF_REFRESH_TOKEN 10 | # - CF_TECHUSER_CLIENT_ID 11 | # - CF_TECHUSER_CLIENT_SECRET 12 | # - CF_TECHUSER_OAUTH_ENDPOINT 13 | # - CF_API_PUBLISH_EMAIL_ENDPOINT 14 | # - MDSP_TENANT 15 | # - MDSP_REGION 16 | # - NOTIFICATION_EMAIL_ADDRESSES 17 | 18 | # Optionally you can also set the following ones if you are behind a proxy 19 | # - npm_config_registry 20 | # - npm_config_sass_binary_site 21 | # - npm_config_chromedriver_cdnurl 22 | # - https_proxy 23 | # - no_proxy 24 | 25 | stages: 26 | - test 27 | - deploy 28 | - notify 29 | 30 | test: 31 | stage: test 32 | image: node:carbon 33 | variables: 34 | SELENIUM_URL: http://selenium__standalone-chrome:4444/wd/hub 35 | MONGODB_URL: mongodb://mongo:27017/e2e 36 | services: 37 | - selenium/standalone-chrome:3.5.2 38 | - mongo:3.2 39 | before_script: 40 | - apt-get update 41 | - apt-get -y install zip 42 | script: 43 | - yarn 44 | - yarn commitlint 45 | - yarn lint 46 | - yarn build:prod --no-progress 47 | - yarn build:dev --no-progress 48 | - yarn test --no-progress --code-coverage 49 | - yarn --cwd server 50 | - yarn --cwd devops/devopsadmin 51 | - yarn --cwd agent 52 | - yarn license:all 53 | - export BASE_URL=http://$(hostname -i):3000 54 | - yarn e2e 55 | - yarn mdsp:zip 56 | artifacts: 57 | reports: 58 | junit: 59 | - coverage/*.xml 60 | - test-reports/*.xml 61 | 62 | mindsphere: 63 | stage: deploy 64 | image: node:carbon 65 | environment: 66 | name: dev 67 | before_script: 68 | - curl -L "https://packages.cloudfoundry.org/stable?release=linux64-binary&source=github" | tar -zx 69 | - mv cf /usr/local/bin 70 | - mkdir -p ~/.cf 71 | - echo "{\"ConfigVersion\":3,\"Target\":\"$CF_API\",\"AccessToken\":\"bearer ${CF_ACCESS_TOKEN}\",\"RefreshToken\":\"${CF_REFRESH_TOKEN}\"}" > ~/.cf/config.json 72 | script: 73 | - cf target -o "$CF_ORG" -s "$CF_SPACE_DEV" 74 | - yarn 75 | - yarn build:prod --no-progress 76 | - yarn --cwd server 77 | - cf push --var mdspTenant="$MDSP_TENANT" --var mdspRegion="$MDSP_REGION" 78 | only: 79 | - master 80 | 81 | # Authentication based on flow: 82 | # https://developer.mindsphere.io/howto/howto-selfhosted-api-access.html 83 | # The value of `CF_API_PUBLISH_EMAIL_ENDPOINT` should be e.g. 84 | # https://gateway.eu1.mindsphere.io/api/notification 85 | .notify-common: ¬ify-common 86 | stage: notify 87 | image: node:carbon 88 | script: 89 | - set -euo pipefail 90 | - apt update && apt install jq -y 91 | - CF_TOKEN=$(curl -s -u "${CF_TECHUSER_CLIENT_ID}:${CF_TECHUSER_CLIENT_SECRET}" 92 | -d "grant_type=client_credentials" 93 | -X POST "${CF_TECHUSER_OAUTH_ENDPOINT}" | jq -r -e '.access_token') 94 | - >- 95 | curl -s -X POST 96 | -H "Authorization: Bearer ${CF_TOKEN}" 97 | -H "Content-Type: application/json" 98 | --data ' 99 | { 100 | "body": {"message": "MindSphere devops-demo deployment successful: '"${CI_ENVIRONMENT_NAME}"'"}, 101 | "recipientsTo": "'"${NOTIFICATION_EMAIL_ADDRESSES}"'", 102 | "from": "gitlab", 103 | "subject": "MindSphere devops-demo deployment: '"${CI_ENVIRONMENT_NAME}"'"} 104 | ' 105 | "${CF_API_PUBLISH_EMAIL_ENDPOINT}" 106 | 107 | notify: 108 | environment: 109 | name: dev 110 | only: 111 | - master 112 | <<: *notify-common 113 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": { 3 | "tables": false, 4 | "code_blocks": false 5 | }, 6 | "no-hard-tabs": true, 7 | "no-bare-urls": false, 8 | "fenced-code-language": false 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [support.oss@siemens.com](mailto:support.oss@siemens.com). 59 | All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions in several forms, e.g. 4 | 5 | * Sponsoring 6 | * Documenting 7 | * Testing 8 | * Coding 9 | * etc. 10 | 11 | Please read 12 | [14 Ways to Contribute to Open Source without Being a Programming Genius or a 13 | Rock Star](http://blog.smartbear.com/programming/14-ways-to-contribute-to-open-source-without-being-a-programming-genius-or-a-rock-star/) 14 | 15 | Please check issues and look for unassigned ones or create a new one. 16 | 17 | Working together in an open and welcoming environment is the foundation of our 18 | success, so please respect our [Code of Conduct](CODE_OF_CONDUCT.md). 19 | 20 | ## Guidelines 21 | 22 | ### Workflow 23 | 24 | We use the 25 | [Feature Branch Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) 26 | and review all changes we merge to master. 27 | 28 | Unfortunatelly that works only if the contributor has at least developer access 29 | rights in this project. 30 | 31 | If you plan to contribute regularly, please request developer access to be 32 | able to use our preferred feature branch workflow. 33 | 34 | Otherwise use [Forking Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow), 35 | since adding everyone to this project would be problematic. 36 | 37 | ### Git Commit 38 | 39 | The cardinal rule for creating good commits is to ensure there is only one 40 | "logical change" per commit. There are many reasons why this is an important 41 | rule: 42 | 43 | * The smaller the amount of code being changed, the quicker & easier it is to 44 | review & identify potential flaws. 45 | * If a change is found to be flawed later, it may be necessary to revert the 46 | broken commit. This is much easier to do if there are not other unrelated code 47 | changes entangled with the original commit. 48 | * When troubleshooting problems using Git's bisect capability, small well 49 | defined changes will aid in isolating exactly where the code problem was 50 | introduced. 51 | * When browsing history using Git annotate/blame, small well defined changes 52 | also aid in isolating exactly where & why a piece of code came from. 53 | 54 | Things to avoid when creating commits 55 | 56 | * Mixing whitespace changes with functional code changes. 57 | * Mixing two unrelated functional changes. 58 | * Sending large new features in a single giant commit. 59 | 60 | ### Git Commit Conventions 61 | 62 | We use git commit as per [Conventional Commits](https://conventionalcommits.org/): 63 | 64 | ```text 65 | docs(contributing): add commit message guidelines 66 | ``` 67 | 68 | Example: 69 | 70 | ```text 71 | (): 72 | ``` 73 | 74 | Allowed types: 75 | 76 | * **feat**: A new feature 77 | * **fix**: A bug fix 78 | * **docs**: Documentation only changes 79 | * **style**: Changes that do not affect the meaning of the code (white-space, 80 | formatting, missing semi-colons, etc) 81 | * **refactor**: A code change that neither fixes a bug or adds a feature 82 | * **perf**: A code change that improves performance 83 | * **test**: Adding missing tests 84 | * **chore**: Changes to the build process or auxiliary tools and libraries such 85 | as documentation generation 86 | 87 | #### What to use as scope 88 | 89 | In most cases the changed component is a good choice as scope 90 | e.g. if the change is done in the ui, the scope should be *ui*. 91 | 92 | For documentation changes the section that was changed makes a good scope name 93 | e.g. use *faq* if you changed that section. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Siemens AG 2018 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 18 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 20 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MindSphere DevOps Demo 2 | 3 | ![TODO app](./images/todo.png) 4 | 5 | ![TODO OpenAPI 3 Navigator](./images/todo-api-docs.png) 6 | 7 | ![DevOpsAdmin Grafana](./images/devopsadmin-grafana.png) 8 | 9 | Close interaction of development and operations is essential to accelerate 10 | delivery of applications. This is a demo across the whole DevOps cycle with 11 | MindSphere, by using well known and widely used open source tools. 12 | 13 | High level architecture diagram (draw.io png with embedded xml): 14 | 15 | ![High-level Architecture](./images/architecture.png) 16 | 17 | The demo consists of: 18 | 19 | - a simple todo app using the MEAN (MongoDB, Express.js, Angular, Node.js) stack 20 | - Angular App (root folder) 21 | - [Backend](server) 22 | - the backend provides swagger-ui for navigating both the server apis and 23 | the MindSphere APIs (under `/api-docs`) 24 | - local Angular dev server setup that proxies requests to MindSphere, 25 | allowing local development 26 | - a devops admin backend that provides access to prometheus and grafana 27 | - [devopsadmin app](devops/devopsadmin) 28 | - [Prometheus on CloudFoundry](devops/prometheus) 29 | - [Grafana on CloudFoundry](devops/grafana) 30 | - a [sample agent](agent) that can be used to simulate an actual device sending 31 | IoT data to the MindSphere APIs 32 | 33 | Additionally, [tooling to ease ssh connectivity to running cf applications](tools/README.md) 34 | is provided. 35 | 36 | Please refer to the official MindSphere & CloudFoundry developer documentation 37 | for detailed information about the platform: 38 | 39 | - https://developer.mindsphere.io/ 40 | - https://docs.cloudfoundry.org/devguide/ 41 | 42 | ## Todo app 43 | 44 | The todo app provides examples on CI/CD including unit and e2e tests. 45 | 46 | ### Backend Configuration 47 | 48 | The following environment variables are recognized by the todo backend: 49 | 50 | | Variable | Description | Required | Default | 51 | |--------------|-------------|----------|---------| 52 | | `MDSP_TENANT` | MindSphere tenant identifier | only on MindSphere deploy | *empty* | 53 | | `MDSP_REGION` | MindSphere region identifier | only on MindSphere deploy | *empty* | 54 | 55 | ### Local Development 56 | 57 | This project includes support for running the web interface in local 58 | development mode connected to MindSphere. In order to reach the MindSphere 59 | APIs you need to provide user credentials for your user. 60 | 61 | The local Angular development server is setup to use a local proxy based on 62 | WebPack that forwards api requests: 63 | 64 | - `/api/**` will be forwarded to `https://gateway..mindsphere.io` 65 | This applies to all [MindSphere API calls](https://developer.mindsphere.io/apis/index.html). 66 | You can check the [MindSphere service source](src/app/mindsphere.service.ts) 67 | for a sample. 68 | - `/v1/**` will be forwarded to `http://localhost:3000` 69 | This applies to all local Node.js todo backend server API calls. You can 70 | start the backend locally from the `server/` directory. 71 | 72 | To be able to reach the MindSphere APIs from your local environment you need 73 | to setup authentication credentials for the MindSphere `/api/**` endpoints. 74 | Please note the next steps are only needed if you call directly MindSphere 75 | APIs from your frontend. They are not needed to interact with the local 76 | todo API backend. 77 | 78 | 1. As a first one-time step you need to register your application in the 79 | MindSphere Developer Cockpit by following the [official documentation](https://developer.mindsphere.io/howto/howto-cf-running-app.html#configure-the-application-via-the-developer-cockpit) 80 | - Create the application 81 | - Register endpoints (a single `/**` is enough for this app) 82 | - **IMPORTANT** Configure the [application Roles & Scopes](https://developer.mindsphere.io/howto/howto-cf-running-app.html#configure-the-application-roles-scopes) 83 | Your application will only have access to the MindSphere APIs that are 84 | configured in this step. Also assign the core role `mdsp:core:tm.tenantUser` 85 | or the MindSphere OS Bar won't be able to show the tenant information 86 | - Register the application 87 | - Assign the application scopes to the users that should have access (in the 88 | tenant Settings app) 89 | 1. Access your new application with a web browser and authenticate. On 90 | successful authentication the MindSphere gateway will setup some session 91 | cookies. Use the browser developer tools to copy the cookies `SESSION` 92 | and `XSRF-TOKEN` 93 | 1. Create a file `src/environments/.environment.mdsplocal.ts` (notice the dot 94 | in the name) with the same contents as `src/environments/environment.ts`. 95 | This file will be ignored by git 96 | 1. In this file set the variables `xsrfTokenHeader` and `sessionCookie` 97 | to the values copied before 98 | 1. These [credentials will be valid](https://developer.mindsphere.io/concepts/concept-gateway-url-schemas.html#restrictions) 99 | for a maximum of 12 hours and have an inactivity timeout of 30 minutes. 100 | When they expire, you can execute the same flow again by logging in to 101 | MindSphere 102 | 103 | Then start the local todo backend and Angular dev server. You will be able 104 | to enjoy live reload of changes done in the source code of the Angular 105 | app: 106 | 107 | ```sh 108 | # Start mongodb server 109 | docker run -p 27017:27017 mongo 110 | 111 | # Start nodejs backend 112 | yarn --cwd server 113 | yarn --cwd server start 114 | 115 | # Start Angular dev server 116 | yarn 117 | yarn start 118 | ``` 119 | 120 | Now load `http://localhost:4200` 121 | 122 | You can also reach the API navigator under `http://localhost:3000/api-docs` 123 | 124 | ### Deploy to MindSphere 125 | 126 | We provide a manifest file to be used for deployment to MindSphere: 127 | 128 | - The manifest uses `random-route: true`, this will create the application 129 | with a random internal route to ensure no naming conflicts with other apps 130 | in the same space (e.g. `todo-funny-chipmunk.apps.eu1.mindsphere`) 131 | - Please note that if you intend later to deploy devopsadmin, Grafana and 132 | Prometheus, you'll need to set in their configuration the appropriate 133 | internal random name assigned by the route directive 134 | - Take a look at the `.gitlab-ci.yml` file for an example of an actual 135 | deployment performed by our CI pipeline 136 | 137 | Use `cf login` to connect via cli and make sure you can interact with the 138 | MindSphere CloudFoundry backend. Follow the 139 | [MindSphere developer documentation](https://developer.mindsphere.io). 140 | 141 | Before deploying, ensure that the appropriate cloudfoundry services are 142 | available: 143 | 144 | - The mongodb service is required. You can choose whichever name you please, 145 | the app will auto-discover it on startup: 146 | 147 | ```sh 148 | cf create-service mongodb32 mongodb-xs todo-mongodb 149 | ``` 150 | 151 | - Create the LogMe (ELK) service for log aggregation. This is 152 | *not a requirement* and you could remove it from the manifest definition 153 | The same service can be used to aggregate any 154 | number of app logs. The MindSphere platform will automatically gather 155 | the logs after binding: 156 | 157 | ```sh 158 | cf create-service logme logme-xs todo-logme 159 | ``` 160 | 161 | Build & push the todo app, set authentication environment variables, and 162 | bind the services: 163 | 164 | ```sh 165 | # Build static angular app into the server/ directory 166 | yarn 167 | yarn build:prod --no-progress 168 | 169 | # Push nodejs server 170 | yarn --cwd server 171 | cf push --var mdspTenant="${MDSP_TENANT}" --var mdspRegion="${MDSP_REGION}" 172 | ``` 173 | 174 | (*Only once*) in the MindSphere Developer Cockpit, some CSP policy adaptations 175 | are needed on the `default-src` directive: 176 | 177 | - allow connections to the OpenAPI specs hosted on `developer.mindsphere.io` 178 | - allow connections to the piam endpoint of the gateway; this is required 179 | for login redirects when the user session token expires 180 | 181 | Example (substitute `` and `` accordingly): 182 | 183 | ``` 184 | default-src 'self' .piam..mindsphere.io developer.mindsphere.io static..mindsphere.io; 185 | ``` 186 | 187 | More information under: https://developer.mindsphere.io/concepts/concept-csp.html 188 | 189 | ### Live development server 190 | 191 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app 192 | will automatically reload if you change any of the source files. 193 | 194 | ### Live development server (with todo api server) 195 | 196 | 1. Run the todo api server available on the `server/` directory. This will 197 | start the api server on `http://localhost:3000` 198 | 1. Run `yarn start`. Navigate to `http://localhost:4200/`. The app will proxy 199 | api calls to `http://localhost:3000` and automatically reload if you change 200 | any of the source files 201 | 202 | ### Code scaffolding 203 | 204 | Run `ng generate component component-name` to generate a new component. You can 205 | also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 206 | 207 | ### Build static Angular UI 208 | 209 | Run `ng build` to build the project. The build artifacts will be stored in the 210 | `dist/` directory. Use the `--prod` flag for a production build. 211 | 212 | ### Running unit tests 213 | 214 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 215 | 216 | ### Running end-to-end tests 217 | 218 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 219 | 220 | ### Further help 221 | 222 | To get more help on the Angular CLI use `ng help` or go check out the 223 | [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 224 | 225 | ## Known Issues / Limitations 226 | 227 | - The [gitlab-ci](.gitlab-ci.yml) integration requires manually setting 228 | authentication with access and refresh tokens available as protected 229 | [CI/CD Gitlab Variables](https://docs.gitlab.com/ce/ci/variables/), 230 | and they need to be renewed every 30 days. This can be copied directly from 231 | your CloudFoundry CLI `~/.cf/config.json` file after successful `cf login`. 232 | - Storage for [Prometheus](devops/prometheus) is currently transient, pending 233 | support for some kind of dynamic persistent storage for apps, or direct 234 | support for Prometheus in MindSphere. 235 | - Prometheus metrics http endpoints are read-only but not protected. 236 | 237 | ## License 238 | 239 | This project is licensed under the MIT License 240 | -------------------------------------------------------------------------------- /SIEMENSAPINOTICE.md: -------------------------------------------------------------------------------- 1 | # Siemens API Notice 2 | 3 | This project has been released under an [Open Source license](./LICENSE.md). 4 | The release may include and/or use APIs to Siemens’ or third parties’ products 5 | or services. In no event shall the project’s Open Source license grant any 6 | rights in or to these APIs, products or services that would alter, expand, be 7 | inconsistent with, or supersede any terms of separate license agreements 8 | applicable to those APIs. “API” means application programming interfaces and 9 | their specifications and implementing code that allows other software to 10 | communicate with or call on Siemens’ or third parties’ products 11 | or services and may be made available through Siemens’ or third parties’ 12 | products, documentations or otherwise. 13 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | /agentconfig.json 2 | /assetdata.json 3 | /.mc/ 4 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # DevOps Demo Agent 2 | 3 | This is a sample showing how to onboard an agent and upload data (data points, 4 | events, files). 5 | 6 | The demo is based on the starter code generated by the 7 | [mindconnect-nodejs library](https://github.com/mindsphere/mindconnect-nodejs) 8 | and uses this library for connectivity. 9 | 10 | ## Prerequisites 11 | 12 | It's required to create two assets in the MindSphere Asset Manager with the 13 | [same configuration as the sample in mindconnect-nodejs](https://github.com/mindsphere/mindconnect-nodejs#step-0-create-an-asset-type-and-aspect-types) 14 | for this to work: 15 | 16 | ``` 17 | AspectType 'Environment' 18 | Variables 19 | 'Humidity' INT % 20 | 'Pressure' DOUBLE kPA 21 | 'Temperature' DOUBLE ºC 22 | 23 | AspectType 'Vibration' 24 | Variables 25 | 'Acceleration' DOUBLE mm/s^2 26 | 'Displacement' DOUBLE mm 27 | 'Frequency' DOUBLE Hz 28 | 'Velocity' DOUBLE mm/s 29 | 30 | AssetType 'TestAssetType' 31 | Aspects 32 | 'EnvironmentData': Type 'Environment' 33 | 'VibrationData': Type 'Vibration' 34 | 35 | Asset 'TestAsset' 36 | Type: 'TestAssetType' 37 | 38 | Asset 'TestAgent' 39 | Type: 'MindConnect Lib' 40 | ``` 41 | 42 | - The first Asset is the actual instantiation of the defined Aspects + Aspect 43 | Types, the type will be whatever Asset Type you defined earlier in the 44 | Asset Manager 45 | - The second Asset is the Agent, and must be of type `core.mclib`. We need to 46 | define all the Aspects again in the agent, and then need to map them to the 47 | actual values in the first Asset 48 | 49 | 1. Create the Aspect Types with their Variables 50 | 1. Create an Asset Type that references the Aspect Types you created earlier 51 | 1. Create the first Asset with type the Asset Type created earlier 52 | 1. Create the second Asset (the Agent) with type `MindConnect Lib`: 53 | - Choose a security profile and generate the onboarding key json 54 | - Copy the key data, you'll need it later for the `agentconfig.json` file 55 | 1. Configure the Agent: 56 | - In the *Configuration* tab, creating a Data Source for each of the Aspect 57 | Types instantiated earlier, adding inside Data Points matching each of the 58 | Variables of the Aspect Type (the name can be chosen freely, but the Data 59 | Type and Unit must match) 60 | - Then in the *Data mappings* tab, link each of the variables (Data Points) 61 | to the Aspects of the Asset. You'll have to click the *Link variable* link 62 | and then search for the Asset. Please note that you will only be able to 63 | link Data Points that are compatible (type and units match) 64 | 65 | After this you will have an Agent linked to the Asset, and you will be able to 66 | deliver data points to the Asset via the Agent. 67 | 68 | ## How to run 69 | 70 | 1. Install the dependencies 71 | ```sh 72 | yarn install 73 | ``` 74 | 1. Download the [initial JSON token of your agent](https://developer.mindsphere.io/howto/howto-agent-onboard.html#getting-the-boarding-configuration) 75 | and save it as `agentconfig.json`. If you prefer to do this manually, 76 | you can use `agentconfig.sample.json` as a template 77 | 1. Copy the file `assetmanager.sample.json` to `assetmanager.json` and adapt the 78 | identifiers of the Asset and the Data Points. You can find them in the Asset 79 | and Agent configurations of the MindSphere Asset Manager 80 | 1. Run the code. The script will perform the agent onboarding and upload 81 | timeseries data, create an event and upload a file 82 | ```sh 83 | yarn start 84 | ``` 85 | 1. Now you can go to the MindSphere Fleet Manager to view the uploaded data 86 | 87 | ## Notes 88 | 89 | The timeseries data in MindSphere can be assigned to either the Agent or the 90 | Assets. There's use cases in which it is useful to map from a single agent to 91 | one or multiple assets, e.g. when you have an environment temperature sensor 92 | data and you want to assign that timeseries data to all the assets which are 93 | in the room. 94 | 95 | The files and the events don't require mappings and they can be assigned 96 | directly to the assets. 97 | 98 | It is in most cases the best option to keep everything in the actual Asset and 99 | not to upload any data to the Agent. The Agent has other interesting data 100 | though, like the *Online Status* 101 | 102 | ## TODOs 103 | 104 | - Perform the Agent onboarding automatically via API calls to the 105 | *Agent Management Service* 106 | -------------------------------------------------------------------------------- /agent/agentconfig.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "baseUrl": "https://southgate.eu1.mindsphere.io", 4 | "iat": "", 5 | "clientCredentialProfile": [ 6 | "SHARED_SECRET" 7 | ], 8 | "clientId": "", 9 | "tenant": "" 10 | }, 11 | "expiration": "" 12 | } 13 | -------------------------------------------------------------------------------- /agent/assetdata.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetId": "", 3 | "dataPoints": { 4 | "environment": { 5 | "temperatureId": "", 6 | "humidityId": "", 7 | "pressureId": "" 8 | }, 9 | "vibration": { 10 | "accelerationId": "", 11 | "displacementId": "", 12 | "frequencyId": "", 13 | "velocityId": "" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /agent/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2019 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const {MindConnectAgent, retry} = require('@mindconnect/mindconnect-nodejs'); 7 | 8 | (async function () { 9 | 10 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 11 | const configuration = require('./agentconfig.json'); 12 | const assetdata = require('./assetdata.json'); 13 | const agent = new MindConnectAgent(configuration); 14 | const log = (text) => { 15 | console.log(`[${new Date().toISOString()}] ${text.toString()}`); 16 | }; 17 | const RETRYTIMES = 5; // retry the operation before giving up and throwing exception 18 | 19 | for (let index = 0; index < 5; index++) { 20 | try { 21 | 22 | log(`Iteration : ${index}`); 23 | // onboarding the agent 24 | if (!agent.IsOnBoarded()) { 25 | // wrapping the call in the retry function makes the agent a bit more resilient 26 | // if you don't want to retry the operations you can always just call await agent.OnBoard(); instead. 27 | await retry(RETRYTIMES, () => agent.OnBoard()); 28 | log('Agent onboarded'); 29 | } 30 | 31 | if (!agent.HasDataSourceConfiguration()) { 32 | await retry(RETRYTIMES, () => agent.GetDataSourceConfiguration()); 33 | log('Configuration acquired'); 34 | } 35 | 36 | const values = [ 37 | { 38 | 'dataPointId': assetdata.dataPoints.environment.temperatureId, 39 | 'qualityCode': '0', 40 | 'value': (Math.sin(index) * (20 + index % 2) + 25).toString() 41 | }, 42 | { 43 | 'dataPointId': assetdata.dataPoints.environment.pressureId, 44 | 'qualityCode': '0', 45 | 'value': (Math.cos(index) * (20 + index % 25) + 25).toString() 46 | }, 47 | { 48 | 'dataPointId': assetdata.dataPoints.environment.humidityId, 49 | 'qualityCode': '0', 50 | 'value': ((index + 30) % 100).toString() 51 | }, 52 | { 53 | 'dataPointId': assetdata.dataPoints.vibration.accelerationId, 54 | 'qualityCode': '0', 55 | 'value': (1000.0 + index).toString() 56 | }, 57 | { 58 | 'dataPointId': assetdata.dataPoints.vibration.frequencyId, 59 | 'qualityCode': '0', 60 | 'value': (60.0 + (index * 0.1)).toString() 61 | }, 62 | { 63 | 'dataPointId': assetdata.dataPoints.vibration.displacementId, 64 | 'qualityCode': '0', 65 | 'value': (index % 10).toString() 66 | }, 67 | { 68 | 'dataPointId': assetdata.dataPoints.vibration.velocityId, 69 | 'qualityCode': '0', 70 | 'value': (50.0 + index).toString() 71 | } 72 | ]; 73 | 74 | // same like above, you can also just call await agent.PostData(values) if you don't want to retry the operation 75 | // this is how to send the data with specific timestamp 76 | // await agent.PostData(values, new Date(Date.now() - 86400 * 1000)); 77 | 78 | await retry(RETRYTIMES, () => agent.PostData(values)); 79 | log('Data posted'); 80 | await sleep(1000); 81 | 82 | const event = { 83 | // 'entityId': agent.ClientId(), // use assetid if you want to send event somewhere else :) 84 | 'entityId': assetdata.assetId, 85 | 'sourceType': 'Event', 86 | 'sourceId': 'application', 87 | 'source': 'Meowz', 88 | 'severity': 20, // 0-99 : 20:error, 30:warning, 40: information 89 | 'timestamp': new Date().toISOString(), 90 | 'description': 'Test' 91 | }; 92 | 93 | // send event with current timestamp; you can also just call agent.PostEvent(event) if you don't want to retry the operation 94 | await retry(RETRYTIMES, () => agent.PostEvent(event)); 95 | log('event posted'); 96 | await sleep(1000); 97 | 98 | // upload file; you can also just call await agent.Upload(...) if you don't want to retry the operation 99 | await retry(RETRYTIMES, () => agent.Upload('README.md', 'application/json', 'Demo File', true, assetdata.assetId)); 100 | log('file uploaded'); 101 | await sleep(1000); 102 | 103 | const yesterday = new Date(); 104 | yesterday.setDate(yesterday.getDate() - 1); 105 | const bulk = [ 106 | { 107 | 'timestamp': yesterday.toISOString(), 108 | 'values': [ 109 | { 110 | 'dataPointId': assetdata.dataPoints.environment.temperatureId, 111 | 'qualityCode': '0', 112 | 'value': '10' 113 | }, 114 | { 115 | 'dataPointId': assetdata.dataPoints.environment.pressureId, 116 | 'qualityCode': '0', 117 | 'value': '10' 118 | }, 119 | { 120 | 'dataPointId': assetdata.dataPoints.environment.humidityId, 121 | 'qualityCode': '0', 122 | 'value': '10' 123 | } 124 | ] 125 | }, 126 | { 127 | 'timestamp': new Date().toISOString(), 128 | 'values': [ 129 | { 130 | 'dataPointId': assetdata.dataPoints.environment.temperatureId, 131 | 'qualityCode': '0', 132 | 'value': '10' 133 | }, 134 | { 135 | 'dataPointId': assetdata.dataPoints.environment.pressureId, 136 | 'qualityCode': '0', 137 | 'value': '10' 138 | }, 139 | { 140 | 'dataPointId': assetdata.dataPoints.environment.humidityId, 141 | 'qualityCode': '0', 142 | 'value': '10' 143 | } 144 | ] 145 | } 146 | ]; 147 | 148 | await retry(RETRYTIMES, () => agent.BulkPostData(bulk)); 149 | log('bulk data uploaded'); 150 | await sleep(1000); 151 | 152 | } catch (err) { 153 | // add proper error handling (e.g. store data somewhere, retry later etc. ) 154 | console.error(err); 155 | } 156 | } 157 | })(); 158 | -------------------------------------------------------------------------------- /agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devops-agent", 3 | "version": "0.1.0", 4 | "description": "Sample MindConnect NodeJS Agent", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "Diego Louzán ", 10 | "contributors": [ 11 | "Roger Meier " 12 | ], 13 | "license": "MIT", 14 | "private": true, 15 | "engines": { 16 | "node": ">=8" 17 | }, 18 | "dependencies": { 19 | "@mindconnect/mindconnect-nodejs": "^3.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /agent/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@mindconnect/mindconnect-nodejs@^3.3.0": 6 | version "3.3.0" 7 | resolved "https://registry.yarnpkg.com/@mindconnect/mindconnect-nodejs/-/mindconnect-nodejs-3.3.0.tgz#32c49890ff32541cb7429ba34674a7f7e1e062a8" 8 | integrity sha512-i/9VN8zQZiFe0JN2kfhAeGPEdUboTQB2Zg8E9LryjNBNXFn99Yt46fWBEP28PhMqNyMAuUJGZf2OF6URdCz8mA== 9 | dependencies: 10 | ajv "^6.9.2" 11 | ajv-keywords "^3.4.0" 12 | async-lock "^1.1.4" 13 | buffer-concat "^1.0.0" 14 | chalk "^2.4.2" 15 | commander "^2.19.0" 16 | csvtojson "^2.0.8" 17 | debug "^4.1.1" 18 | https-proxy-agent "^2.2.1" 19 | json-groupby "^1.1.0" 20 | jsonwebtoken "^8.5.0" 21 | lodash "^4.17.11" 22 | mime-types "^2.1.22" 23 | node-fetch "^1.7.3" 24 | rsa-pem-to-jwk "^1.1.3" 25 | url-search-params-polyfill "^5.0.0" 26 | uuid "^3.3.2" 27 | 28 | agent-base@^4.1.0: 29 | version "4.2.1" 30 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" 31 | integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== 32 | dependencies: 33 | es6-promisify "^5.0.0" 34 | 35 | ajv-keywords@^3.4.0: 36 | version "3.4.0" 37 | resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" 38 | integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== 39 | 40 | ajv@^6.9.2: 41 | version "6.10.0" 42 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" 43 | integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== 44 | dependencies: 45 | fast-deep-equal "^2.0.1" 46 | fast-json-stable-stringify "^2.0.0" 47 | json-schema-traverse "^0.4.1" 48 | uri-js "^4.2.2" 49 | 50 | ansi-styles@^3.2.1: 51 | version "3.2.1" 52 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 53 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 54 | dependencies: 55 | color-convert "^1.9.0" 56 | 57 | async-lock@^1.1.4: 58 | version "1.1.4" 59 | resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.1.4.tgz#863aff9d5c243f75034349be7df9c3ceb7a54254" 60 | integrity sha512-9vsVXt+mIvb8rV0G6V1x68Bvp/VksPJoZJxF/n/l9N60chNJ44opPr9WdZZfAV3leUdXt4xNvfyNWyY/j5enBA== 61 | 62 | bluebird@^3.5.1: 63 | version "3.5.3" 64 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" 65 | integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== 66 | 67 | buffer-concat@^1.0.0: 68 | version "1.0.0" 69 | resolved "https://registry.yarnpkg.com/buffer-concat/-/buffer-concat-1.0.0.tgz#2defadadcb145e0ad3c0b961cde1910b118d1fe6" 70 | integrity sha1-Le+trcsUXgrTwLlhzeGRCxGNH+Y= 71 | 72 | buffer-equal-constant-time@1.0.1: 73 | version "1.0.1" 74 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 75 | integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= 76 | 77 | chalk@^2.4.2: 78 | version "2.4.2" 79 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 80 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 81 | dependencies: 82 | ansi-styles "^3.2.1" 83 | escape-string-regexp "^1.0.5" 84 | supports-color "^5.3.0" 85 | 86 | color-convert@^1.9.0: 87 | version "1.9.3" 88 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 89 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 90 | dependencies: 91 | color-name "1.1.3" 92 | 93 | color-name@1.1.3: 94 | version "1.1.3" 95 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 96 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 97 | 98 | commander@^2.19.0: 99 | version "2.19.0" 100 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" 101 | integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== 102 | 103 | csvtojson@^2.0.8: 104 | version "2.0.8" 105 | resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.8.tgz#d889f19576b2b33ead235490d2e5c9791481e8d3" 106 | integrity sha512-DC6YFtsJiA7t/Yz+KjzT6GXuKtU/5gRbbl7HJqvDVVir+dxdw2/1EgwfgJdnsvUT7lOnON5DvGftKuYWX1nMOQ== 107 | dependencies: 108 | bluebird "^3.5.1" 109 | lodash "^4.17.3" 110 | strip-bom "^2.0.0" 111 | 112 | debug@^3.1.0: 113 | version "3.2.6" 114 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 115 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 116 | dependencies: 117 | ms "^2.1.1" 118 | 119 | debug@^4.1.1: 120 | version "4.1.1" 121 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 122 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 123 | dependencies: 124 | ms "^2.1.1" 125 | 126 | ecdsa-sig-formatter@1.0.10: 127 | version "1.0.10" 128 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" 129 | integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= 130 | dependencies: 131 | safe-buffer "^5.0.1" 132 | 133 | encoding@^0.1.11: 134 | version "0.1.12" 135 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 136 | integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= 137 | dependencies: 138 | iconv-lite "~0.4.13" 139 | 140 | es6-promise@^4.0.3: 141 | version "4.2.5" 142 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" 143 | integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== 144 | 145 | es6-promisify@^5.0.0: 146 | version "5.0.0" 147 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 148 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= 149 | dependencies: 150 | es6-promise "^4.0.3" 151 | 152 | escape-string-regexp@^1.0.5: 153 | version "1.0.5" 154 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 155 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 156 | 157 | fast-deep-equal@^2.0.1: 158 | version "2.0.1" 159 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 160 | integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 161 | 162 | fast-json-stable-stringify@^2.0.0: 163 | version "2.0.0" 164 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 165 | integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= 166 | 167 | has-flag@^3.0.0: 168 | version "3.0.0" 169 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 170 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 171 | 172 | https-proxy-agent@^2.2.1: 173 | version "2.2.1" 174 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" 175 | integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== 176 | dependencies: 177 | agent-base "^4.1.0" 178 | debug "^3.1.0" 179 | 180 | iconv-lite@~0.4.13: 181 | version "0.4.24" 182 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 183 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 184 | dependencies: 185 | safer-buffer ">= 2.1.2 < 3" 186 | 187 | is-stream@^1.0.1: 188 | version "1.1.0" 189 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 190 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 191 | 192 | is-utf8@^0.2.0: 193 | version "0.2.1" 194 | resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 195 | integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= 196 | 197 | json-groupby@^1.1.0: 198 | version "1.1.0" 199 | resolved "https://registry.yarnpkg.com/json-groupby/-/json-groupby-1.1.0.tgz#7af5e0ed70118dd21ea41fa92fa75365f979f55a" 200 | integrity sha1-evXg7XARjdIepB+pL6dTZfl59Vo= 201 | 202 | json-schema-traverse@^0.4.1: 203 | version "0.4.1" 204 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 205 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 206 | 207 | jsonwebtoken@^8.5.0: 208 | version "8.5.0" 209 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e" 210 | integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA== 211 | dependencies: 212 | jws "^3.2.1" 213 | lodash.includes "^4.3.0" 214 | lodash.isboolean "^3.0.3" 215 | lodash.isinteger "^4.0.4" 216 | lodash.isnumber "^3.0.3" 217 | lodash.isplainobject "^4.0.6" 218 | lodash.isstring "^4.0.1" 219 | lodash.once "^4.0.0" 220 | ms "^2.1.1" 221 | semver "^5.6.0" 222 | 223 | jwa@^1.2.0: 224 | version "1.2.0" 225 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.2.0.tgz#606da70c1c6d425cad329c77c99f2df2a981489a" 226 | integrity sha512-Grku9ZST5NNQ3hqNUodSkDfEBqAmGA1R8yiyPHOnLzEKI0GaCQC/XhFmsheXYuXzFQJdILbh+lYBiliqG5R/Vg== 227 | dependencies: 228 | buffer-equal-constant-time "1.0.1" 229 | ecdsa-sig-formatter "1.0.10" 230 | safe-buffer "^5.0.1" 231 | 232 | jws@^3.2.1: 233 | version "3.2.1" 234 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.1.tgz#d79d4216a62c9afa0a3d5e8b5356d75abdeb2be5" 235 | integrity sha512-bGA2omSrFUkd72dhh05bIAN832znP4wOU3lfuXtRBuGTbsmNmDXMQg28f0Vsxaxgk4myF5YkKQpz6qeRpMgX9g== 236 | dependencies: 237 | jwa "^1.2.0" 238 | safe-buffer "^5.0.1" 239 | 240 | lodash.includes@^4.3.0: 241 | version "4.3.0" 242 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 243 | integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= 244 | 245 | lodash.isboolean@^3.0.3: 246 | version "3.0.3" 247 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 248 | integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= 249 | 250 | lodash.isinteger@^4.0.4: 251 | version "4.0.4" 252 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 253 | integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= 254 | 255 | lodash.isnumber@^3.0.3: 256 | version "3.0.3" 257 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 258 | integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= 259 | 260 | lodash.isplainobject@^4.0.6: 261 | version "4.0.6" 262 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 263 | integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= 264 | 265 | lodash.isstring@^4.0.1: 266 | version "4.0.1" 267 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 268 | integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= 269 | 270 | lodash.once@^4.0.0: 271 | version "4.1.1" 272 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 273 | integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= 274 | 275 | lodash@^4.17.11, lodash@^4.17.3: 276 | version "4.17.11" 277 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 278 | integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== 279 | 280 | mime-db@~1.38.0: 281 | version "1.38.0" 282 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" 283 | integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== 284 | 285 | mime-types@^2.1.22: 286 | version "2.1.22" 287 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" 288 | integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== 289 | dependencies: 290 | mime-db "~1.38.0" 291 | 292 | ms@^2.1.1: 293 | version "2.1.1" 294 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 295 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 296 | 297 | node-fetch@^1.7.3: 298 | version "1.7.3" 299 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 300 | integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== 301 | dependencies: 302 | encoding "^0.1.11" 303 | is-stream "^1.0.1" 304 | 305 | object-assign@^2.0.0: 306 | version "2.1.1" 307 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" 308 | integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo= 309 | 310 | optimist@~0.3.5: 311 | version "0.3.7" 312 | resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" 313 | integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= 314 | dependencies: 315 | wordwrap "~0.0.2" 316 | 317 | punycode@^2.1.0: 318 | version "2.1.1" 319 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 320 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 321 | 322 | rsa-pem-to-jwk@^1.1.3: 323 | version "1.1.3" 324 | resolved "https://registry.yarnpkg.com/rsa-pem-to-jwk/-/rsa-pem-to-jwk-1.1.3.tgz#245e76bdb7e7234cfee7ca032d31b54c38fab98e" 325 | integrity sha1-JF52vbfnI0z+58oDLTG1TDj6uY4= 326 | dependencies: 327 | object-assign "^2.0.0" 328 | rsa-unpack "0.0.6" 329 | 330 | rsa-unpack@0.0.6: 331 | version "0.0.6" 332 | resolved "https://registry.yarnpkg.com/rsa-unpack/-/rsa-unpack-0.0.6.tgz#f50ebd56a628378e631f297161026ce9ab4eddba" 333 | integrity sha1-9Q69VqYoN45jHylxYQJs6atO3bo= 334 | dependencies: 335 | optimist "~0.3.5" 336 | 337 | safe-buffer@^5.0.1: 338 | version "5.1.2" 339 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 340 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 341 | 342 | "safer-buffer@>= 2.1.2 < 3": 343 | version "2.1.2" 344 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 345 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 346 | 347 | semver@^5.6.0: 348 | version "5.6.0" 349 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" 350 | integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== 351 | 352 | strip-bom@^2.0.0: 353 | version "2.0.0" 354 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 355 | integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= 356 | dependencies: 357 | is-utf8 "^0.2.0" 358 | 359 | supports-color@^5.3.0: 360 | version "5.5.0" 361 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 362 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 363 | dependencies: 364 | has-flag "^3.0.0" 365 | 366 | uri-js@^4.2.2: 367 | version "4.2.2" 368 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 369 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 370 | dependencies: 371 | punycode "^2.1.0" 372 | 373 | url-search-params-polyfill@^5.0.0: 374 | version "5.0.0" 375 | resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-5.0.0.tgz#09b98337c89dcf6c6a6a0bfeb096f6ba83b7526b" 376 | integrity sha512-+SCD22QJp4UnqPOI5UTTR0Ljuh8cHbjEf1lIiZrZ8nHTlTixqwVsVQTSfk5vrmDz7N09/Y+ka5jQr0ff35FnQQ== 377 | 378 | uuid@^3.3.2: 379 | version "3.3.2" 380 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" 381 | integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== 382 | 383 | wordwrap@~0.0.2: 384 | version "0.0.3" 385 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" 386 | integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= 387 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "todo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "server/static", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/assets" 23 | ], 24 | "styles": [ 25 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 26 | "node_modules/font-awesome/css/font-awesome.min.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "mdsplocal": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/.environment.mdsplocal.ts" 36 | } 37 | ] 38 | }, 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "extractCss": true, 50 | "namedChunks": false, 51 | "aot": true, 52 | "extractLicenses": true, 53 | "vendorChunk": false, 54 | "buildOptimizer": true 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "todo:build", 62 | "proxyConfig": "proxy.conf.js" 63 | }, 64 | "configurations": { 65 | "mdsplocal": { 66 | "browserTarget": "todo:build:mdsplocal" 67 | }, 68 | "production": { 69 | "browserTarget": "todo:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "todo:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "scripts": [], 87 | "assets": [ 88 | "src/favicon.ico", 89 | "src/assets" 90 | ] 91 | } 92 | }, 93 | "lint": { 94 | "builder": "@angular-devkit/build-angular:tslint", 95 | "options": { 96 | "tsConfig": [ 97 | "src/tsconfig.app.json", 98 | "src/tsconfig.spec.json" 99 | ], 100 | "exclude": [ 101 | "**/node_modules/**" 102 | ] 103 | } 104 | } 105 | } 106 | }, 107 | "todo-e2e": { 108 | "root": "e2e/", 109 | "projectType": "application", 110 | "architect": { 111 | "e2e": { 112 | "builder": "@angular-devkit/build-angular:protractor", 113 | "options": { 114 | "protractorConfig": "e2e/protractor.conf.js" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "todo:serve:production" 119 | } 120 | } 121 | }, 122 | "lint": { 123 | "builder": "@angular-devkit/build-angular:tslint", 124 | "options": { 125 | "tsConfig": "e2e/tsconfig.e2e.json", 126 | "exclude": [ 127 | "**/node_modules/**" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "todo" 135 | } 136 | -------------------------------------------------------------------------------- /devops/README.md: -------------------------------------------------------------------------------- 1 | # DevOpsAdmin on MindSphere CloudFoundry 2 | 3 | *devopsadmin* is intended to be a showcase of administrative actions, by both 4 | providing direct calls to MindSphere services using technical credentials 5 | (e.g. notifications) and also act as a proxy for prometheus & grafana. 6 | 7 | Three components are offered: 8 | 9 | 1. *devopsadmin* is provided as a Node.js backend, full source code under 10 | directory `devopsadmin/` 11 | 1. *prometheus* is deployed from its github sources using 12 | the go buildpack. The directory `prometheus/` contains deployment 13 | scripts and sample configuration files 14 | 1. *grafana* is also deployed from its github sources using the go buildpack. 15 | The directory `grafana/` contains deployment scripts and sample 16 | configuration files 17 | 18 | ## Configuration 19 | 20 | The following environment variables are recognized by the devopsadmin backend: 21 | 22 | | Variable | Description | Required | 23 | |------------------------------|---------------------------------------------|----------| 24 | | `MDSP_TENANT` | MindSphere tenant identifier | yes | 25 | | `MDSP_REGION` | MindSphere region identifier | yes | 26 | | `PROMETHEUS_URL` | Full url of to-be-proxied Prometheus server | yes | 27 | | `GRAFANA_URL` | Full url of to-be-proxied Grafana server | yes | 28 | | `TECH_USER_CLIENT_ID` | Technical user client id | yes | 29 | | `TECH_USER_CLIENT_SECRET` | Technical user client secret | yes | 30 | | `NOTIFICATION_EMAIL` | Email address for notifications | yes | 31 | | `NOTIFICATION_MOBILE_NUMBER` | Mobile number for notifications (E.164 fmt) | yes | 32 | 33 | The Technical User is required to be able to send notifications with the 34 | `/notification` endpoint ([see the MindSphere documentation](https://developer.mindsphere.io/apis/advanced-notification/api-notification-overview.html#access)). 35 | 36 | ## Build and Run 37 | 38 | ```sh 39 | yarn --cwd devopsadmin 40 | yarn --cwd devopsadmin start 41 | ``` 42 | 43 | ## MindSphere Deployment 44 | 45 | A single `manifest.yml` is provided for all components (devopsadmin, 46 | prometheus, grafana). This is a requirement since in an operator tenant, 47 | all applications will be deployed in a single cf space. 48 | 49 | The manifest uses template variables. In order to configure the required 50 | variables, copy the `vars-file.yml.sample` file and adapt the values. 51 | 52 | Each of the three components provides a deployment script and can be 53 | deployed independently. Please refer to the [prometheus](prometheus/README.md) 54 | and [grafana](grafana/README.md) readme files for details. 55 | 56 | ```sh 57 | cd devopsadmin 58 | CF_VARS_FILE= \ 59 | ./deploy_dev.sh 60 | 61 | cd ../prometheus 62 | CF_VARS_FILE= \ 63 | PROM_CONF_FILE= \ 64 | ./deploy_dev.sh 65 | 66 | cd ../grafana 67 | CF_VARS_FILE= \ 68 | GF_CONF_DIR= \ 69 | ./deploy_dev.sh 70 | ``` 71 | 72 | The following CSP policy is also required to be able to proxy components 73 | (particularly the `script-src 'unsafe-eval'` option for Grafana). This needs 74 | to be setup in the MindSphere Developer Cockpit before registering the app: 75 | 76 | ``` 77 | default-src 'self' static.eu1.mindsphere.io; style-src * 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' static.eu1.mindsphere.io; img-src * data:; 78 | ``` 79 | 80 | ## MindSphere Operator Tenant Deployment 81 | 82 | The script `build_mdsp_zip.sh` can be used to generate a zip file that can be 83 | provided (together with the manifest file) for MindSphere Operator tenant 84 | deployment. 85 | 86 | The manifest is templated, so remember to inform the MindSphere validation team 87 | of the required parameter values for your environment. 88 | 89 | Please also note that this script assumes that a previous deployment to 90 | development was successfully performed (e.g. generate the static web view for 91 | grafana, or copied the appropriate configuration files). 92 | 93 | ## Debug Proxy Requests 94 | 95 | Set the following environment variable in the cloudfoundry manifest: 96 | 97 | ``` 98 | DEBUG: express-http-proxy 99 | ``` 100 | 101 | ## License 102 | 103 | This project is licensed under the MIT License 104 | -------------------------------------------------------------------------------- /devops/build_mdsp_zip.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GOPATH=$(go env GOPATH) 4 | PROM_SRC_PATH="${GOPATH}/src/github.com/prometheus/prometheus" 5 | GF_SRC_PATH="${GOPATH}/src/github.com/grafana/grafana" 6 | BUILDDIR="build_zip" 7 | 8 | which zip > /dev/null || { echo \"Please install the 'zip' command\"; exit 1; } 9 | 10 | rm -f devopsadmin.zip 11 | rm -rf ${BUILDDIR} 12 | 13 | mkdir ${BUILDDIR} 14 | cp README.md vars-file.yml.sample ${BUILDDIR} 15 | cp -r devopsadmin ${BUILDDIR} 16 | cp -r ${PROM_SRC_PATH} ${BUILDDIR} 17 | cp -r ${GF_SRC_PATH} ${BUILDDIR} 18 | 19 | pushd ${BUILDDIR} 20 | zip -r devopsadmin . \ 21 | -x 'devopsadmin/node_modules/*' \ 22 | -x 'prometheus/.git/*' \ 23 | -x 'prometheus/node_modules/*' \ 24 | -x 'prometheus/data/*' \ 25 | -x 'prometheus/promtool' \ 26 | -x 'prometheus/prometheus' \ 27 | -x 'grafana/.git/*' \ 28 | -x 'grafana/bin/*' \ 29 | -x 'grafana/node_modules/*' 30 | popd 31 | mv ${BUILDDIR}/devopsadmin.zip . 32 | -------------------------------------------------------------------------------- /devops/devopsadmin/.cfignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /devops/devopsadmin/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const config = { 7 | mdsp: { 8 | appname: 'mdsp:core:im', 9 | scope: 'userIamAdmin', 10 | tenant: process.env.MDSP_TENANT, 11 | region: process.env.MDSP_REGION 12 | }, 13 | tech_user: { 14 | client_id: process.env.TECH_USER_CLIENT_ID, 15 | client_secret: process.env.TECH_USER_CLIENT_SECRET 16 | } 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /devops/devopsadmin/deploy_dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ -f "${CF_VARS_FILE}" ]] || { echo "Missing CF_VARS_FILE" && exit 1; } 4 | 5 | cf push devopsadmin \ 6 | -f ../manifest.yml \ 7 | -p . \ 8 | --vars-file "${CF_VARS_FILE}" 9 | -------------------------------------------------------------------------------- /devops/devopsadmin/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const express = require('express'); 7 | const app = express(); 8 | const request = require('superagent'); 9 | const jwt = require('jsonwebtoken'); 10 | const includes = require('lodash.includes'); 11 | const proxy = require('express-http-proxy'); 12 | 13 | const config = require('./config'); 14 | const services = require('./services'); 15 | 16 | const asyncHandler = (func) => 17 | (req, res, next) => Promise.resolve(func(req, res, next)).catch(next); 18 | 19 | const setupConfig = () => { 20 | if (!process.env.MDSP_TENANT) throw new Error('missing MDSP_TENANT configuration'); 21 | if (!process.env.MDSP_REGION) throw new Error('missing MDSP_REGION configuration'); 22 | if (!process.env.PROMETHEUS_URL) throw new Error('missing PROMETHEUS_URL configuration'); 23 | if (!process.env.GRAFANA_URL) throw new Error('missing GRAFANA_URL configuration'); 24 | if (!process.env.TECH_USER_CLIENT_ID) throw new Error('missing TECH_USER_CLIENT_ID configuration'); 25 | if (!process.env.TECH_USER_CLIENT_SECRET) throw new Error('missing TECH_USER_CLIENT_SECRET configuration'); 26 | if (!process.env.NOTIFICATION_EMAIL) throw new Error('missing NOTIFICATION_EMAIL configuration'); 27 | if (!process.env.NOTIFICATION_MOBILE_NUMBER) throw new Error('missing NOTIFICATION_MOBILE_NUMBER configuration'); 28 | }; 29 | setupConfig(); 30 | 31 | // Middleware for checking the scopes in the user token 32 | app.use('/', function (req, res, next) { 33 | let scopes = []; 34 | 35 | if (req.headers.authorization) { 36 | let splitAuthHeader = req.headers.authorization.split(' '); 37 | if (splitAuthHeader[0].toLowerCase() === 'bearer') { 38 | let token = jwt.decode(splitAuthHeader[1], {complete: true}); 39 | 40 | if (token.payload != null) { 41 | scopes = token.payload.scope; 42 | } 43 | } 44 | } 45 | if (includes(scopes, `${config.mdsp.appname}.${config.mdsp.scope}`)) { 46 | next(); 47 | } else { 48 | console.log('unauthorized request'); 49 | res.status(403).send('no access!'); 50 | } 51 | }); 52 | 53 | const rootHtml = ` 54 |

Hello authorized user with scope: ${config.mdsp.appname}.${config.mdsp.scope}

55 | 65 | `; 66 | app.get('/', (req, res) => res.send(rootHtml)); 67 | 68 | app.get('/http/', function (req, res) { 69 | res.writeHead(200, { 'Content-Type': 'application/json' }); 70 | res.end(JSON.stringify(req.headers)); 71 | }); 72 | 73 | app.get('/env/', function (req, res) { 74 | res.writeHead(200, { 'Content-Type': 'application/json' }); 75 | res.end(JSON.stringify(process.env)); 76 | }); 77 | 78 | app.get('/jwt/', function (req, res) { 79 | let authorizationHeader = req.get('authorization'); 80 | 81 | if (authorizationHeader != null) { 82 | authorizationHeader = authorizationHeader.replace('Bearer ', ''); 83 | authorizationHeader = authorizationHeader.replace('bearer ', ''); 84 | token = jwt.decode(authorizationHeader, { complete: true }) 85 | } 86 | 87 | res.writeHead(200, { 'Content-Type': 'application/json' }); 88 | res.end(JSON.stringify(token)); 89 | }); 90 | 91 | app.get('/users/', function (req, res) { 92 | let authorizationHeader = req.get('authorization'); 93 | request 94 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/im/v3/Users?attributes=meta,name,userName,active`) 95 | .set('Authorization', authorizationHeader) 96 | .set('Accept', 'application/json') 97 | .then(function(data) { 98 | res.json({ 99 | resources: data.body.resources 100 | }); 101 | }).catch(err => { 102 | console.error(err.message, err.status); 103 | res.status(err.status).json({message: 'Failed to fetch users'}); 104 | }); 105 | }); 106 | 107 | app.use('/grafana/', proxy(process.env.GRAFANA_URL, { 108 | https: true, 109 | proxyReqOptDecorator: function(proxyReqOpts, srcReq) { 110 | proxyReqOpts.headers.authorization = ''; 111 | return proxyReqOpts; 112 | } 113 | })); 114 | 115 | app.use('/prometheus/', proxy(process.env.PROMETHEUS_URL, { 116 | https: true, 117 | proxyReqOptDecorator: function(proxyReqOpts, srcReq) { 118 | proxyReqOpts.headers.authorization = ''; 119 | return proxyReqOpts; 120 | } 121 | })); 122 | 123 | app.get('/simple-notification/', asyncHandler(async (req, res, next) => { 124 | try { 125 | const data = await services.notification 126 | .sendSimpleNotification(process.env.NOTIFICATION_EMAIL); 127 | res.json({ 128 | message: 'Notification sent', 129 | result: data.body 130 | }); 131 | } catch (err) { 132 | console.error(err); 133 | res.sendStatus(500); 134 | } 135 | })); 136 | 137 | app.get('/complex-notification/', asyncHandler(async (req, res, next) => { 138 | try { 139 | const data = await services.notification 140 | .sendComplexNotification(process.env.NOTIFICATION_EMAIL, process.env.NOTIFICATION_MOBILE_NUMBER); 141 | res.json({ 142 | message: 'Notification sent', 143 | result: data.body 144 | }); 145 | } catch (err) { 146 | console.error(err); 147 | res.sendStatus(500); 148 | } 149 | })); 150 | 151 | app.listen(process.env.PORT || 3000, function(){ 152 | console.log('Express listening on port', this.address().port); 153 | }); 154 | -------------------------------------------------------------------------------- /devops/devopsadmin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devops-admin", 3 | "version": "0.1.0", 4 | "description": "An administrative interface for MindSphere that also proxies other internal components", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://gitlab.com/mindsphere/devops-demo.git" 9 | }, 10 | "author": "Roger Meier ", 11 | "contributors": [ 12 | "Diego Louzán " 13 | ], 14 | "license": "MIT", 15 | "private": true, 16 | "scripts": { 17 | "start": "node index.js", 18 | "license": "license-checker --onlyAllow 'Apache-2.0; BSD; BSD-2-Clause; BSD-3-Clause; ISC; MIT; Unlicense; WTFPL; CC-BY-3.0; CC0-1.0' --production" 19 | }, 20 | "engines": { 21 | "node": ">=8" 22 | }, 23 | "dependencies": { 24 | "express": "^4.16.3", 25 | "express-http-proxy": "^1.2.0", 26 | "jsonwebtoken": "^8.3.0", 27 | "license-checker": "^20.2.0", 28 | "lodash.includes": "^4.3.0", 29 | "superagent": "^3.8.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /devops/devopsadmin/services/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | module.exports.notification = require('./notification'); 7 | -------------------------------------------------------------------------------- /devops/devopsadmin/services/notification.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const config = require('../config'); 7 | const request = require('superagent'); 8 | const jwt = require('jsonwebtoken'); 9 | 10 | const authData = { 11 | technicalUser: { 12 | token: null, 13 | expiresAt: new Date().getTime() 14 | } 15 | }; 16 | 17 | const techUserOauthEndpoint = `https://${config.mdsp.tenant}.piam.${config.mdsp.region}.mindsphere.io/oauth/token`; 18 | 19 | const authenticate = async () => { 20 | let technicalToken = authData.technicalUser.token; 21 | let expiresAt = authData.technicalUser.expiresAt; 22 | 23 | // Expire 1 minute early to account for clock differences 24 | const expiration = expiresAt - 60000; 25 | 26 | if (expiration > new Date().getTime()) { 27 | console.log('Found cached token'); 28 | } else { 29 | // Expired, obtain a new token 30 | technicalToken = await request.post(techUserOauthEndpoint) 31 | .auth(config.tech_user.client_id, config.tech_user.client_secret) 32 | .send('grant_type=client_credentials') 33 | .then(data => { 34 | console.log('Obtained new technical user token'); 35 | authData.technicalUser.token = data.body.access_token; 36 | authData.technicalUser.expiresAt = parseFloat( 37 | jwt.decode(data.body.access_token).exp) * 1000; 38 | return authData.technicalUser.token; 39 | }); 40 | } 41 | 42 | if (!technicalToken) { 43 | throw new Error('empty technical token'); 44 | } 45 | return technicalToken; 46 | }; 47 | 48 | const findAddressTypes = async (token) => { 49 | const addressTypes = await request 50 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/addresstype`) 51 | .set('Authorization', `Bearer ${token}`) 52 | .then(res => { return res.body; }); 53 | console.log('addressTypes:', JSON.stringify(addressTypes)); 54 | return addressTypes; 55 | }; 56 | 57 | const findCommunicationChannels = async (token) => { 58 | const communicationChannels = await request 59 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/communicationchannel/`) 60 | .set('Authorization', `Bearer ${token}`) 61 | .then(res => { return res.body; }); 62 | console.log('communicationChannels:', JSON.stringify(communicationChannels)); 63 | return communicationChannels; 64 | }; 65 | 66 | const findParamTypes = async (token) => { 67 | const paramTypes = await request 68 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/paramtype/`) 69 | .set('Authorization', `Bearer ${token}`) 70 | .then(res => { return res.body; }); 71 | console.log('paramTypes:', JSON.stringify(paramTypes)); 72 | return paramTypes; 73 | }; 74 | 75 | const findOrCreateRecipients = async (token, recipientEmail, recipientMobileNumber) => { 76 | const recipientIds = []; 77 | 78 | // Recipient 1 79 | const recipient1Name = 'testRecipientOne'; 80 | const recipient1Json = { 81 | recipientname: recipient1Name, 82 | recipientdetail: [ 83 | { 84 | address: recipientEmail, 85 | // Personal Mail 86 | addresstypeid: 1 87 | }, 88 | ] 89 | }; 90 | 91 | const recipient1List = await request 92 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/search`) 93 | .set('Authorization', `Bearer ${token}`) 94 | .send({ name: recipient1Name }) 95 | .then(res => { return res.body; }); 96 | 97 | // Use first found result, otherwise create a new recipient 98 | if (recipient1List.length) { 99 | recipientIds.push(recipient1List[0].recipientid); 100 | } else { 101 | const recipient1Id = await request 102 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/`) 103 | .set('Authorization', `Bearer ${token}`) 104 | .send(recipient1Json) 105 | .then(res => { return res.body; }); 106 | recipientIds.push(recipient1Id); 107 | } 108 | 109 | // Recipient 2 110 | const recipient2Name = 'testRecipientTwo'; 111 | const recipient2Json = { 112 | recipientname: recipient2Name, 113 | recipientdetail: [ 114 | { 115 | address: recipientMobileNumber, 116 | // Personal Number 117 | addresstypeid: 4 118 | } 119 | ] 120 | }; 121 | 122 | const recipient2List = await request 123 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/search`) 124 | .set('Authorization', `Bearer ${token}`) 125 | .send({ name: recipient2Name }) 126 | .then(res => { return res.body; }); 127 | 128 | // Use first found result, otherwise create a new recipient 129 | if (recipient2List.length) { 130 | recipientIds.push(recipient2List[0].recipientid); 131 | } else { 132 | const recipient2Id = await request 133 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/`) 134 | .set('Authorization', `Bearer ${token}`) 135 | .send(recipient2Json) 136 | .then(res => { return res.body; }); 137 | recipientIds.push(recipient2Id); 138 | } 139 | 140 | console.log('recipientIds:', recipientIds); 141 | return recipientIds; 142 | }; 143 | 144 | const findOrCreateRecipientGroup = async (token, recipientIds) => { 145 | const recipientGroupName = 'testRecipientGroup'; 146 | const recipientIdsJson = recipientIds.map(recipientId => { 147 | return { recipientId: recipientId }; 148 | }); 149 | const recipientGroupJson = { 150 | groupName: recipientGroupName, 151 | recipientIds: recipientIdsJson 152 | }; 153 | 154 | const recipientGroupList = await request 155 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/recipientgroup/search`) 156 | .set('Authorization', `Bearer ${token}`) 157 | .send({ name: recipientGroupName }) 158 | .then(res => { return res.body; }); 159 | 160 | // Use first found result, otherwise create a new recipient group 161 | let recipientGroupId = null; 162 | if (recipientGroupList.length) { 163 | recipientGroupId = recipientGroupList[0].groupId; 164 | } else { 165 | const recipientGroupResponse = await request 166 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/recipient/recipientgroup`) 167 | .set('Authorization', `Bearer ${token}`) 168 | .send(recipientGroupJson) 169 | .then(res => { return res.body; }); 170 | recipientGroupId = recipientGroupResponse; 171 | } 172 | 173 | console.log('recipientGroupId:', recipientGroupId); 174 | return recipientGroupId; 175 | }; 176 | 177 | const findOrCreateTemplateSet = async (token) => { 178 | const templateSetName = 'testTemplateSet'; 179 | const templateInfo = { 180 | templateParam: [ 181 | { 182 | paramName: 'Source Name', 183 | defaultValue: 'devopsadmin', 184 | placeHolderName: 'name', 185 | // STRING 186 | paramTypeId: 4 187 | } 188 | ], 189 | templatesetName: templateSetName, 190 | templateChannelAndFile: [ 191 | { 192 | // Email 193 | communicationChannel: 1, 194 | fileName: 'templateEmail.html', 195 | operation: 'ADD' 196 | }, 197 | { 198 | // SMS 199 | communicationChannel: 2, 200 | fileName: 'templateSms.html', 201 | operation: 'ADD' 202 | } 203 | ] 204 | }; 205 | 206 | const templateSetList = await request 207 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/template/templatesets`) 208 | .set('Authorization', `Bearer ${token}`) 209 | .query({ templatesetname: templateSetName }) 210 | .then(res => { return res.body; }); 211 | 212 | // Use first found result, otherwise create a new template set 213 | let templateSet = null; 214 | if (templateSetList.length) { 215 | templateSet = templateSetList[0]; 216 | } else { 217 | const templateSetResponse = await request 218 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/template/`) 219 | .set('Authorization', `Bearer ${token}`) 220 | .field('templateInfo', JSON.stringify(templateInfo)) 221 | .attach('templateFiles', __dirname + '/templateEmail.html') 222 | .attach('templateFiles', __dirname + '/templateSms.html') 223 | .then(res => { return res.body; }); 224 | templateSet = templateSetResponse; 225 | } 226 | 227 | console.log('templateSet:', JSON.stringify(templateSet)); 228 | return templateSet; 229 | }; 230 | 231 | const findOrCreateCommunicationCategory = async (token, recipientGroupId, templateSet) => { 232 | if (!recipientGroupId) throw new Error('missing recipientGroupId'); 233 | if (!templateSet) throw new Error('missing templateSet'); 234 | 235 | const commCategoryName = 'testCommunicationCategory'; 236 | const templatesJson = templateSet.templateList.map(template => { 237 | return { 238 | templateId: template.templateId, 239 | templatesetId: templateSet.templatesetId, 240 | commChannelName: template.commChannelName 241 | }; 242 | }); 243 | const commCategoryJson = { 244 | msgCategoryName: commCategoryName, 245 | subject: 'complex devopsadmin MindSphere notification', 246 | priority: 1, 247 | from: 'devopsadmin', 248 | recipients: [ 249 | { 250 | recipientGroupId: recipientGroupId, 251 | position: 'TO' 252 | } 253 | ], 254 | templates: templatesJson 255 | }; 256 | 257 | const commCategoryList = await request 258 | .get(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/communicationcategories/`) 259 | .set('Authorization', `Bearer ${token}`) 260 | .then(res => { return res.body.filter(c => c.msgCategoryName === commCategoryName); }); 261 | 262 | // Find result by name 263 | let commCategoryId = null; 264 | if (commCategoryList.length) { 265 | commCategoryId = commCategoryList[0].msgCategoryId; 266 | } else { 267 | const commCategoryResponse = await request 268 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/communicationcategories/`) 269 | .set('Authorization', `Bearer ${token}`) 270 | .send(commCategoryJson) 271 | .then(res => { return res.body; }); 272 | commCategoryId = commCategoryResponse; 273 | } 274 | 275 | console.log('commCategoryId:', commCategoryId); 276 | return commCategoryId; 277 | }; 278 | 279 | const sendSimpleNotification = async (recipientEmail) => { 280 | const messageJson = { 281 | body: { 282 | message: 'A notification has been triggered from devopsadmin through MindSphere' 283 | }, 284 | recipientsTo: recipientEmail, 285 | from: 'devopsadmin', 286 | subject: 'simple devopsadmin MindSphere notification' 287 | }; 288 | 289 | const technicalToken = await authenticate(); 290 | return await request 291 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/publisher/messages`) 292 | .set('Authorization', `Bearer ${technicalToken}`) 293 | .send(messageJson); 294 | }; 295 | 296 | const sendComplexNotification = async (recipientEmail, recipientMobileNumber) => { 297 | const technicalToken = await authenticate(); 298 | 299 | // Log in the console the valid types 300 | const addressTypes = await findAddressTypes(technicalToken); 301 | const communicationChannels = await findCommunicationChannels(technicalToken); 302 | const paramTypes = await findParamTypes(technicalToken); 303 | 304 | // Create recipient, recipient group, template, and communication category 305 | // Set delivery to both an email address and a mobile phone 306 | const recipientIds = await findOrCreateRecipients(technicalToken, recipientEmail, recipientMobileNumber); 307 | const recipientGroupId = await findOrCreateRecipientGroup(technicalToken, recipientIds); 308 | const templateSet = await findOrCreateTemplateSet(technicalToken); 309 | const commCategoryId = await findOrCreateCommunicationCategory(technicalToken, recipientGroupId, templateSet); 310 | 311 | const messageJson = { 312 | body: { 313 | name: 'devopsadmin' 314 | }, 315 | messageCategoryId: commCategoryId 316 | }; 317 | 318 | // Trigger actual message delivery to both email and mobile 319 | return await request 320 | .post(`https://gateway.${config.mdsp.region}.mindsphere.io/api/notification/v3/publisher/messages`) 321 | .set('Authorization', `Bearer ${technicalToken}`) 322 | .send(messageJson); 323 | }; 324 | 325 | const service = { 326 | sendSimpleNotification: sendSimpleNotification, 327 | sendComplexNotification: sendComplexNotification 328 | }; 329 | 330 | module.exports = service; 331 | -------------------------------------------------------------------------------- /devops/devopsadmin/services/templateEmail.html: -------------------------------------------------------------------------------- 1 | 2 | Email: A complex notification has been triggered from [[${name}]] through MindSphere 3 | 4 | -------------------------------------------------------------------------------- /devops/devopsadmin/services/templateSms.html: -------------------------------------------------------------------------------- 1 | 2 | SMS: A complex notification has been triggered from [[${name}]] through MindSphere 3 | 4 | -------------------------------------------------------------------------------- /devops/devopsadmin/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | abbrev@1: 6 | version "1.1.1" 7 | resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 8 | 9 | accepts@~1.3.5: 10 | version "1.3.5" 11 | resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 12 | dependencies: 13 | mime-types "~2.1.18" 14 | negotiator "0.6.1" 15 | 16 | ansi-regex@^3.0.0: 17 | version "3.0.0" 18 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 19 | 20 | ansi-styles@^3.2.1: 21 | version "3.2.1" 22 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 23 | dependencies: 24 | color-convert "^1.9.0" 25 | 26 | array-find-index@^1.0.2: 27 | version "1.0.2" 28 | resolved "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 29 | 30 | array-flatten@1.1.1: 31 | version "1.1.1" 32 | resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 33 | 34 | asap@^2.0.0: 35 | version "2.0.6" 36 | resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" 37 | 38 | asynckit@^0.4.0: 39 | version "0.4.0" 40 | resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 41 | 42 | balanced-match@^1.0.0: 43 | version "1.0.0" 44 | resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 45 | 46 | body-parser@1.18.2: 47 | version "1.18.2" 48 | resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 49 | dependencies: 50 | bytes "3.0.0" 51 | content-type "~1.0.4" 52 | debug "2.6.9" 53 | depd "~1.1.1" 54 | http-errors "~1.6.2" 55 | iconv-lite "0.4.19" 56 | on-finished "~2.3.0" 57 | qs "6.5.1" 58 | raw-body "2.3.2" 59 | type-is "~1.6.15" 60 | 61 | brace-expansion@^1.1.7: 62 | version "1.1.11" 63 | resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 64 | dependencies: 65 | balanced-match "^1.0.0" 66 | concat-map "0.0.1" 67 | 68 | buffer-equal-constant-time@1.0.1: 69 | version "1.0.1" 70 | resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 71 | 72 | builtin-modules@^1.0.0: 73 | version "1.1.1" 74 | resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" 75 | 76 | bytes@3.0.0: 77 | version "3.0.0" 78 | resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 79 | 80 | chalk@^2.4.1: 81 | version "2.4.1" 82 | resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 83 | dependencies: 84 | ansi-styles "^3.2.1" 85 | escape-string-regexp "^1.0.5" 86 | supports-color "^5.3.0" 87 | 88 | color-convert@^1.9.0: 89 | version "1.9.3" 90 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 91 | dependencies: 92 | color-name "1.1.3" 93 | 94 | color-name@1.1.3: 95 | version "1.1.3" 96 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 97 | 98 | combined-stream@1.0.6: 99 | version "1.0.6" 100 | resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" 101 | dependencies: 102 | delayed-stream "~1.0.0" 103 | 104 | component-emitter@^1.2.0: 105 | version "1.2.1" 106 | resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 107 | 108 | concat-map@0.0.1: 109 | version "0.0.1" 110 | resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 111 | 112 | content-disposition@0.5.2: 113 | version "0.5.2" 114 | resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 115 | 116 | content-type@~1.0.4: 117 | version "1.0.4" 118 | resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 119 | 120 | cookie-signature@1.0.6: 121 | version "1.0.6" 122 | resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 123 | 124 | cookie@0.3.1: 125 | version "0.3.1" 126 | resolved "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 127 | 128 | cookiejar@^2.1.0: 129 | version "2.1.2" 130 | resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" 131 | 132 | core-util-is@~1.0.0: 133 | version "1.0.2" 134 | resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 135 | 136 | debug@2.6.9: 137 | version "2.6.9" 138 | resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 139 | dependencies: 140 | ms "2.0.0" 141 | 142 | debug@^3.0.1, debug@^3.1.0: 143 | version "3.1.0" 144 | resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 145 | dependencies: 146 | ms "2.0.0" 147 | 148 | debuglog@^1.0.1: 149 | version "1.0.1" 150 | resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" 151 | 152 | delayed-stream@~1.0.0: 153 | version "1.0.0" 154 | resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 155 | 156 | depd@1.1.1: 157 | version "1.1.1" 158 | resolved "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 159 | 160 | depd@~1.1.1, depd@~1.1.2: 161 | version "1.1.2" 162 | resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 163 | 164 | destroy@~1.0.4: 165 | version "1.0.4" 166 | resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 167 | 168 | dezalgo@^1.0.0: 169 | version "1.0.3" 170 | resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" 171 | dependencies: 172 | asap "^2.0.0" 173 | wrappy "1" 174 | 175 | ecdsa-sig-formatter@1.0.10: 176 | version "1.0.10" 177 | resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" 178 | dependencies: 179 | safe-buffer "^5.0.1" 180 | 181 | ee-first@1.1.1: 182 | version "1.1.1" 183 | resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 184 | 185 | encodeurl@~1.0.2: 186 | version "1.0.2" 187 | resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 188 | 189 | es6-promise@^4.1.1: 190 | version "4.2.4" 191 | resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" 192 | 193 | escape-html@~1.0.3: 194 | version "1.0.3" 195 | resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 196 | 197 | escape-string-regexp@^1.0.5: 198 | version "1.0.5" 199 | resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 200 | 201 | etag@~1.8.1: 202 | version "1.8.1" 203 | resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 204 | 205 | express-http-proxy@^1.2.0: 206 | version "1.2.0" 207 | resolved "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-1.2.0.tgz#a19087c3e52c00494604e703464d655595fdba51" 208 | dependencies: 209 | debug "^3.0.1" 210 | es6-promise "^4.1.1" 211 | raw-body "^2.3.0" 212 | 213 | express@^4.16.3: 214 | version "4.16.3" 215 | resolved "https://registry.npmjs.org/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" 216 | dependencies: 217 | accepts "~1.3.5" 218 | array-flatten "1.1.1" 219 | body-parser "1.18.2" 220 | content-disposition "0.5.2" 221 | content-type "~1.0.4" 222 | cookie "0.3.1" 223 | cookie-signature "1.0.6" 224 | debug "2.6.9" 225 | depd "~1.1.2" 226 | encodeurl "~1.0.2" 227 | escape-html "~1.0.3" 228 | etag "~1.8.1" 229 | finalhandler "1.1.1" 230 | fresh "0.5.2" 231 | merge-descriptors "1.0.1" 232 | methods "~1.1.2" 233 | on-finished "~2.3.0" 234 | parseurl "~1.3.2" 235 | path-to-regexp "0.1.7" 236 | proxy-addr "~2.0.3" 237 | qs "6.5.1" 238 | range-parser "~1.2.0" 239 | safe-buffer "5.1.1" 240 | send "0.16.2" 241 | serve-static "1.13.2" 242 | setprototypeof "1.1.0" 243 | statuses "~1.4.0" 244 | type-is "~1.6.16" 245 | utils-merge "1.0.1" 246 | vary "~1.1.2" 247 | 248 | extend@^3.0.0: 249 | version "3.0.2" 250 | resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 251 | 252 | finalhandler@1.1.1: 253 | version "1.1.1" 254 | resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 255 | dependencies: 256 | debug "2.6.9" 257 | encodeurl "~1.0.2" 258 | escape-html "~1.0.3" 259 | on-finished "~2.3.0" 260 | parseurl "~1.3.2" 261 | statuses "~1.4.0" 262 | unpipe "~1.0.0" 263 | 264 | form-data@^2.3.1: 265 | version "2.3.2" 266 | resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" 267 | dependencies: 268 | asynckit "^0.4.0" 269 | combined-stream "1.0.6" 270 | mime-types "^2.1.12" 271 | 272 | formidable@^1.2.0: 273 | version "1.2.1" 274 | resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" 275 | 276 | forwarded@~0.1.2: 277 | version "0.1.2" 278 | resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 279 | 280 | fresh@0.5.2: 281 | version "0.5.2" 282 | resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 283 | 284 | fs.realpath@^1.0.0: 285 | version "1.0.0" 286 | resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 287 | 288 | glob@^7.1.1: 289 | version "7.1.3" 290 | resolved "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" 291 | dependencies: 292 | fs.realpath "^1.0.0" 293 | inflight "^1.0.4" 294 | inherits "2" 295 | minimatch "^3.0.4" 296 | once "^1.3.0" 297 | path-is-absolute "^1.0.0" 298 | 299 | graceful-fs@^4.1.2: 300 | version "4.1.11" 301 | resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 302 | 303 | has-flag@^3.0.0: 304 | version "3.0.0" 305 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 306 | 307 | hosted-git-info@^2.1.4: 308 | version "2.7.1" 309 | resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" 310 | 311 | http-errors@1.6.2: 312 | version "1.6.2" 313 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 314 | dependencies: 315 | depd "1.1.1" 316 | inherits "2.0.3" 317 | setprototypeof "1.0.3" 318 | statuses ">= 1.3.1 < 2" 319 | 320 | http-errors@1.6.3, http-errors@~1.6.2: 321 | version "1.6.3" 322 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 323 | dependencies: 324 | depd "~1.1.2" 325 | inherits "2.0.3" 326 | setprototypeof "1.1.0" 327 | statuses ">= 1.4.0 < 2" 328 | 329 | iconv-lite@0.4.19: 330 | version "0.4.19" 331 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 332 | 333 | iconv-lite@0.4.23: 334 | version "0.4.23" 335 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 336 | dependencies: 337 | safer-buffer ">= 2.1.2 < 3" 338 | 339 | inflight@^1.0.4: 340 | version "1.0.6" 341 | resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 342 | dependencies: 343 | once "^1.3.0" 344 | wrappy "1" 345 | 346 | inherits@2, inherits@2.0.3, inherits@~2.0.3: 347 | version "2.0.3" 348 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 349 | 350 | ipaddr.js@1.8.0: 351 | version "1.8.0" 352 | resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" 353 | 354 | is-builtin-module@^1.0.0: 355 | version "1.0.0" 356 | resolved "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" 357 | dependencies: 358 | builtin-modules "^1.0.0" 359 | 360 | isarray@~1.0.0: 361 | version "1.0.0" 362 | resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 363 | 364 | json-parse-better-errors@^1.0.1: 365 | version "1.0.2" 366 | resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" 367 | 368 | jsonwebtoken@^8.3.0: 369 | version "8.3.0" 370 | resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" 371 | dependencies: 372 | jws "^3.1.5" 373 | lodash.includes "^4.3.0" 374 | lodash.isboolean "^3.0.3" 375 | lodash.isinteger "^4.0.4" 376 | lodash.isnumber "^3.0.3" 377 | lodash.isplainobject "^4.0.6" 378 | lodash.isstring "^4.0.1" 379 | lodash.once "^4.0.0" 380 | ms "^2.1.1" 381 | 382 | jwa@^1.1.5: 383 | version "1.1.6" 384 | resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" 385 | dependencies: 386 | buffer-equal-constant-time "1.0.1" 387 | ecdsa-sig-formatter "1.0.10" 388 | safe-buffer "^5.0.1" 389 | 390 | jws@^3.1.5: 391 | version "3.1.5" 392 | resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" 393 | dependencies: 394 | jwa "^1.1.5" 395 | safe-buffer "^5.0.1" 396 | 397 | license-checker@^20.2.0: 398 | version "20.2.0" 399 | resolved "https://registry.npmjs.org/license-checker/-/license-checker-20.2.0.tgz#edb2605fd714514ed16e7eeee5b88a41900241d2" 400 | dependencies: 401 | chalk "^2.4.1" 402 | debug "^3.1.0" 403 | mkdirp "^0.5.1" 404 | nopt "^4.0.1" 405 | read-installed "~4.0.3" 406 | semver "^5.5.0" 407 | spdx "^0.5.1" 408 | spdx-correct "^3.0.0" 409 | spdx-satisfies "^4.0.0" 410 | strip-ansi "^4.0.0" 411 | treeify "^1.1.0" 412 | 413 | lodash.includes@^4.3.0: 414 | version "4.3.0" 415 | resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 416 | 417 | lodash.isboolean@^3.0.3: 418 | version "3.0.3" 419 | resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 420 | 421 | lodash.isinteger@^4.0.4: 422 | version "4.0.4" 423 | resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 424 | 425 | lodash.isnumber@^3.0.3: 426 | version "3.0.3" 427 | resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 428 | 429 | lodash.isplainobject@^4.0.6: 430 | version "4.0.6" 431 | resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 432 | 433 | lodash.isstring@^4.0.1: 434 | version "4.0.1" 435 | resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 436 | 437 | lodash.once@^4.0.0: 438 | version "4.1.1" 439 | resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 440 | 441 | media-typer@0.3.0: 442 | version "0.3.0" 443 | resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 444 | 445 | merge-descriptors@1.0.1: 446 | version "1.0.1" 447 | resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 448 | 449 | methods@^1.1.1, methods@~1.1.2: 450 | version "1.1.2" 451 | resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 452 | 453 | mime-db@~1.36.0: 454 | version "1.36.0" 455 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" 456 | 457 | mime-types@^2.1.12, mime-types@~2.1.18: 458 | version "2.1.20" 459 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" 460 | dependencies: 461 | mime-db "~1.36.0" 462 | 463 | mime@1.4.1: 464 | version "1.4.1" 465 | resolved "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 466 | 467 | mime@^1.4.1: 468 | version "1.6.0" 469 | resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 470 | 471 | minimatch@^3.0.4: 472 | version "3.0.4" 473 | resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 474 | dependencies: 475 | brace-expansion "^1.1.7" 476 | 477 | minimist@0.0.8: 478 | version "0.0.8" 479 | resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 480 | 481 | mkdirp@^0.5.1: 482 | version "0.5.1" 483 | resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 484 | dependencies: 485 | minimist "0.0.8" 486 | 487 | ms@2.0.0: 488 | version "2.0.0" 489 | resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 490 | 491 | ms@^2.1.1: 492 | version "2.1.1" 493 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 494 | 495 | negotiator@0.6.1: 496 | version "0.6.1" 497 | resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 498 | 499 | nopt@^4.0.1: 500 | version "4.0.1" 501 | resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 502 | dependencies: 503 | abbrev "1" 504 | osenv "^0.1.4" 505 | 506 | normalize-package-data@^2.0.0: 507 | version "2.4.0" 508 | resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" 509 | dependencies: 510 | hosted-git-info "^2.1.4" 511 | is-builtin-module "^1.0.0" 512 | semver "2 || 3 || 4 || 5" 513 | validate-npm-package-license "^3.0.1" 514 | 515 | on-finished@~2.3.0: 516 | version "2.3.0" 517 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 518 | dependencies: 519 | ee-first "1.1.1" 520 | 521 | once@^1.3.0: 522 | version "1.4.0" 523 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 524 | dependencies: 525 | wrappy "1" 526 | 527 | os-homedir@^1.0.0: 528 | version "1.0.2" 529 | resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 530 | 531 | os-tmpdir@^1.0.0: 532 | version "1.0.2" 533 | resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 534 | 535 | osenv@^0.1.4: 536 | version "0.1.5" 537 | resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" 538 | dependencies: 539 | os-homedir "^1.0.0" 540 | os-tmpdir "^1.0.0" 541 | 542 | parseurl@~1.3.2: 543 | version "1.3.2" 544 | resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 545 | 546 | path-is-absolute@^1.0.0: 547 | version "1.0.1" 548 | resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 549 | 550 | path-to-regexp@0.1.7: 551 | version "0.1.7" 552 | resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 553 | 554 | process-nextick-args@~2.0.0: 555 | version "2.0.0" 556 | resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 557 | 558 | proxy-addr@~2.0.3: 559 | version "2.0.4" 560 | resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" 561 | dependencies: 562 | forwarded "~0.1.2" 563 | ipaddr.js "1.8.0" 564 | 565 | qs@6.5.1: 566 | version "6.5.1" 567 | resolved "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 568 | 569 | qs@^6.5.1: 570 | version "6.5.2" 571 | resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 572 | 573 | range-parser@~1.2.0: 574 | version "1.2.0" 575 | resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 576 | 577 | raw-body@2.3.2: 578 | version "2.3.2" 579 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 580 | dependencies: 581 | bytes "3.0.0" 582 | http-errors "1.6.2" 583 | iconv-lite "0.4.19" 584 | unpipe "1.0.0" 585 | 586 | raw-body@^2.3.0: 587 | version "2.3.3" 588 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" 589 | dependencies: 590 | bytes "3.0.0" 591 | http-errors "1.6.3" 592 | iconv-lite "0.4.23" 593 | unpipe "1.0.0" 594 | 595 | read-installed@~4.0.3: 596 | version "4.0.3" 597 | resolved "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" 598 | dependencies: 599 | debuglog "^1.0.1" 600 | read-package-json "^2.0.0" 601 | readdir-scoped-modules "^1.0.0" 602 | semver "2 || 3 || 4 || 5" 603 | slide "~1.1.3" 604 | util-extend "^1.0.1" 605 | optionalDependencies: 606 | graceful-fs "^4.1.2" 607 | 608 | read-package-json@^2.0.0: 609 | version "2.0.13" 610 | resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a" 611 | dependencies: 612 | glob "^7.1.1" 613 | json-parse-better-errors "^1.0.1" 614 | normalize-package-data "^2.0.0" 615 | slash "^1.0.0" 616 | optionalDependencies: 617 | graceful-fs "^4.1.2" 618 | 619 | readable-stream@^2.3.5: 620 | version "2.3.6" 621 | resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 622 | dependencies: 623 | core-util-is "~1.0.0" 624 | inherits "~2.0.3" 625 | isarray "~1.0.0" 626 | process-nextick-args "~2.0.0" 627 | safe-buffer "~5.1.1" 628 | string_decoder "~1.1.1" 629 | util-deprecate "~1.0.1" 630 | 631 | readdir-scoped-modules@^1.0.0: 632 | version "1.0.2" 633 | resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" 634 | dependencies: 635 | debuglog "^1.0.1" 636 | dezalgo "^1.0.0" 637 | graceful-fs "^4.1.2" 638 | once "^1.3.0" 639 | 640 | safe-buffer@5.1.1: 641 | version "5.1.1" 642 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 643 | 644 | safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 645 | version "5.1.2" 646 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 647 | 648 | "safer-buffer@>= 2.1.2 < 3": 649 | version "2.1.2" 650 | resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 651 | 652 | "semver@2 || 3 || 4 || 5", semver@^5.5.0: 653 | version "5.5.1" 654 | resolved "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" 655 | 656 | send@0.16.2: 657 | version "0.16.2" 658 | resolved "https://registry.npmjs.org/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 659 | dependencies: 660 | debug "2.6.9" 661 | depd "~1.1.2" 662 | destroy "~1.0.4" 663 | encodeurl "~1.0.2" 664 | escape-html "~1.0.3" 665 | etag "~1.8.1" 666 | fresh "0.5.2" 667 | http-errors "~1.6.2" 668 | mime "1.4.1" 669 | ms "2.0.0" 670 | on-finished "~2.3.0" 671 | range-parser "~1.2.0" 672 | statuses "~1.4.0" 673 | 674 | serve-static@1.13.2: 675 | version "1.13.2" 676 | resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 677 | dependencies: 678 | encodeurl "~1.0.2" 679 | escape-html "~1.0.3" 680 | parseurl "~1.3.2" 681 | send "0.16.2" 682 | 683 | setprototypeof@1.0.3: 684 | version "1.0.3" 685 | resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 686 | 687 | setprototypeof@1.1.0: 688 | version "1.1.0" 689 | resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 690 | 691 | slash@^1.0.0: 692 | version "1.0.0" 693 | resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 694 | 695 | slide@~1.1.3: 696 | version "1.1.6" 697 | resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" 698 | 699 | spdx-compare@^1.0.0: 700 | version "1.0.0" 701 | resolved "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7" 702 | dependencies: 703 | array-find-index "^1.0.2" 704 | spdx-expression-parse "^3.0.0" 705 | spdx-ranges "^2.0.0" 706 | 707 | spdx-correct@^3.0.0: 708 | version "3.0.0" 709 | resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" 710 | dependencies: 711 | spdx-expression-parse "^3.0.0" 712 | spdx-license-ids "^3.0.0" 713 | 714 | spdx-exceptions@^1.0.0: 715 | version "1.0.5" 716 | resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.5.tgz#9d21ac4da4bdb71d060fb74e5a67531d032cbba6" 717 | 718 | spdx-exceptions@^2.1.0: 719 | version "2.1.0" 720 | resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" 721 | 722 | spdx-expression-parse@^3.0.0: 723 | version "3.0.0" 724 | resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 725 | dependencies: 726 | spdx-exceptions "^2.1.0" 727 | spdx-license-ids "^3.0.0" 728 | 729 | spdx-license-ids@^1.0.0: 730 | version "1.2.2" 731 | resolved "http://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" 732 | 733 | spdx-license-ids@^3.0.0: 734 | version "3.0.1" 735 | resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz#e2a303236cac54b04031fa7a5a79c7e701df852f" 736 | 737 | spdx-ranges@^2.0.0: 738 | version "2.0.0" 739 | resolved "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.0.0.tgz#257686798e5edb41d45c1aba3d3f1bb47af8d5ec" 740 | 741 | spdx-satisfies@^4.0.0: 742 | version "4.0.0" 743 | resolved "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.0.tgz#ebc79eec88b68ac75618e2e5ee94fbc347587552" 744 | dependencies: 745 | spdx-compare "^1.0.0" 746 | spdx-expression-parse "^3.0.0" 747 | spdx-ranges "^2.0.0" 748 | 749 | spdx@^0.5.1: 750 | version "0.5.2" 751 | resolved "https://registry.npmjs.org/spdx/-/spdx-0.5.2.tgz#76a428b9b97e7904ef83e62a4af0d06fdb50c265" 752 | dependencies: 753 | spdx-exceptions "^1.0.0" 754 | spdx-license-ids "^1.0.0" 755 | 756 | "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": 757 | version "1.5.0" 758 | resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 759 | 760 | statuses@~1.4.0: 761 | version "1.4.0" 762 | resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 763 | 764 | string_decoder@~1.1.1: 765 | version "1.1.1" 766 | resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 767 | dependencies: 768 | safe-buffer "~5.1.0" 769 | 770 | strip-ansi@^4.0.0: 771 | version "4.0.0" 772 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 773 | dependencies: 774 | ansi-regex "^3.0.0" 775 | 776 | superagent@^3.8.3: 777 | version "3.8.3" 778 | resolved "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" 779 | dependencies: 780 | component-emitter "^1.2.0" 781 | cookiejar "^2.1.0" 782 | debug "^3.1.0" 783 | extend "^3.0.0" 784 | form-data "^2.3.1" 785 | formidable "^1.2.0" 786 | methods "^1.1.1" 787 | mime "^1.4.1" 788 | qs "^6.5.1" 789 | readable-stream "^2.3.5" 790 | 791 | supports-color@^5.3.0: 792 | version "5.5.0" 793 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 794 | dependencies: 795 | has-flag "^3.0.0" 796 | 797 | treeify@^1.1.0: 798 | version "1.1.0" 799 | resolved "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" 800 | 801 | type-is@~1.6.15, type-is@~1.6.16: 802 | version "1.6.16" 803 | resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 804 | dependencies: 805 | media-typer "0.3.0" 806 | mime-types "~2.1.18" 807 | 808 | unpipe@1.0.0, unpipe@~1.0.0: 809 | version "1.0.0" 810 | resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 811 | 812 | util-deprecate@~1.0.1: 813 | version "1.0.2" 814 | resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 815 | 816 | util-extend@^1.0.1: 817 | version "1.0.3" 818 | resolved "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" 819 | 820 | utils-merge@1.0.1: 821 | version "1.0.1" 822 | resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 823 | 824 | validate-npm-package-license@^3.0.1: 825 | version "3.0.4" 826 | resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" 827 | dependencies: 828 | spdx-correct "^3.0.0" 829 | spdx-expression-parse "^3.0.0" 830 | 831 | vary@~1.1.2: 832 | version "1.1.2" 833 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 834 | 835 | wrappy@1: 836 | version "1.0.2" 837 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 838 | -------------------------------------------------------------------------------- /devops/grafana/.cfignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /bin/ 3 | -------------------------------------------------------------------------------- /devops/grafana/.gitignore: -------------------------------------------------------------------------------- 1 | !/sample-conf/ 2 | /conf*/ 3 | -------------------------------------------------------------------------------- /devops/grafana/README.md: -------------------------------------------------------------------------------- 1 | # Grafana on MindSphere CloudFoundry 2 | 3 | ## Tested Environment 4 | 5 | ```sh 6 | $ cf version 7 | cf version 6.38.0+7ddf0aadd.2018-08-07 8 | 9 | $ go version 10 | go version go1.10.1 darwin/amd64 11 | 12 | $ node --version 13 | v8.12.0 14 | 15 | $ yarn --version 16 | 1.7.0 17 | ``` 18 | 19 | ## Configuration 20 | 21 | The following environment variables are recognized by the todo backend: 22 | 23 | | Variable | Description | Required | 24 | |--------------|-------------|----------| 25 | | `GF_DATABASE_URL` | Full url of db, including user / password | yes | 26 | | `GF_SMTP_PASSWORD` | Password of smtp server for email notifications | yes | 27 | 28 | ## CloudFoundry Deployment 29 | 30 | Grafana is deployed from its github sources. Since Grafana is a Golang 31 | project and we require some custom configuration files to be uploaded when 32 | deploying to cloudfoundry, a script is provided that copies the relevant files 33 | into the checked out sources. 34 | 35 | 1. Copy and adapt the directory `sample-conf/`, specifically: 36 | - In `defaults.custom.ini`, set `server.domain` and `smtp` configuration 37 | - In `datasources/prometheus.yaml`, set the appropriate url of prometheus 38 | 39 | 1. Download the grafana source code. This will save the code to your 40 | `${GOPATH}` (typically `~/go` or `~/.go`): 41 | 42 | ```sh 43 | go get github.com/grafana/grafana 44 | ``` 45 | 46 | 1. Checkout the latest tested version (`v5.3.4` at the time of writing): 47 | 48 | ```sh 49 | cd ${GOPATH}/src/github.com/grafana/grafana 50 | git checkout v5.3.4 51 | ``` 52 | 53 | 1. Build the grafana static web interface locally. This is required because we 54 | don't want to depend on two buildpacks (go & nodejs). Building the webpack 55 | locally before uploading to CloudFoundry allows us to keep the deployment 56 | within a single buildpack. It is only required once: 57 | 58 | ```sh 59 | yarn --cwd "$(go env GOPATH)/src/github.com/grafana/grafana" 60 | yarn --cwd "$(go env GOPATH)/src/github.com/grafana/grafana" --prod run build 61 | ``` 62 | 63 | 1. Provision the required db service for grafana: 64 | 65 | ```sh 66 | cf create-service postgresql94 postgresql-xs grafana-postgres 67 | ``` 68 | 69 | 1. The manifest will bind itself to a LogMe (ELK) service to gather all logs, 70 | make sure you have created it in advance: 71 | 72 | ```sh 73 | cf create-service logme logme-xs devopsadmin-logme 74 | ``` 75 | 76 | 1. Ensure you are in the right CloudFoundry space and deploy Grafana. The 77 | command will also copy the required configuration files to the local grafana 78 | source directory before push: 79 | 80 | ```sh 81 | CF_VARS_FILE="" \ 82 | GF_CONF_DIR="" \ 83 | ./deploy_dev.sh 84 | ``` 85 | 86 | The param `CF_VARS_FILE` is your CloudFoundry adapted vars file. 87 | 88 | The param `GF_CONFIG_DIR` points to your adapted configuration directory 89 | that will be uploaded 90 | 91 | ## Limitations: Alerting Notification Channels 92 | 93 | As of grafana v5, [auto-provisioning of alerting setup is not possible](http://docs.grafana.org/administration/provisioning/): 94 | 95 | > We hope to extend this system to later add support for users, orgs and alerts 96 | > as well. 97 | 98 | In order to configure a *Notification Channel*: 99 | 100 | - Login into grafana 101 | - Go to `Alerting > Notification channels > Add channel` 102 | - Set values: 103 | - Name: `MindSphere DevOps` 104 | - Type: `Email` 105 | - Send on all alerts: `checked` 106 | - Email addresses: `devops@example.com` 107 | 108 | ## Grafana Dashboards Json Export 109 | 110 | Import any dashboard into grafana, then save the resulting json found under 111 | `dashboard -> settings -> view json` 112 | 113 | **IMPORTANT** Do not use the `dashboard -> share -> save/view json` option, 114 | this will save a templated wrong json that can't be used for file provisioning 115 | 116 | ## Local Build and Execution 117 | 118 | In case you want to build & run locally the grafana distribution: 119 | 120 | ```sh 121 | cd "$(go env GOPATH)/src/github.com/grafana/grafana" 122 | go run build.go setup 123 | go run build.go build 124 | yarn run build 125 | 126 | bin/*/grafana-server 127 | ``` 128 | 129 | ## License 130 | 131 | This project is licensed under the MIT License 132 | -------------------------------------------------------------------------------- /devops/grafana/deploy_dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ -f "${CF_VARS_FILE}" ]] || { echo "Missing CF_VARS_FILE" && exit 1; } 4 | [[ -d "${GF_CONF_DIR}" ]] || { echo "Missing GF_CONF_DIR" && exit 1; } 5 | 6 | GOPATH=$(go env GOPATH) 7 | GF_SRC_PATH="${GOPATH}/src/github.com/grafana/grafana" 8 | 9 | cp .cfignore ${GF_SRC_PATH} 10 | rm -rf "${GF_SRC_PATH}/mindsphere-conf" 11 | cp -rf ${GF_CONF_DIR} "${GF_SRC_PATH}/mindsphere-conf" 12 | 13 | cf push grafana \ 14 | -f ../manifest.yml \ 15 | -p "${GF_SRC_PATH}" \ 16 | --vars-file "${CF_VARS_FILE}" 17 | -------------------------------------------------------------------------------- /devops/grafana/sample-conf/defaults.custom.ini: -------------------------------------------------------------------------------- 1 | ##################### Grafana Configuration Defaults ##################### 2 | # 3 | # Do not modify this file in grafana installs 4 | # 5 | 6 | # possible values : production, development 7 | app_mode = production 8 | 9 | # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty 10 | instance_name = ${HOSTNAME} 11 | 12 | #################################### Paths ############################### 13 | [paths] 14 | # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) 15 | data = data 16 | 17 | # Temporary files in `data` directory older than given duration will be removed 18 | temp_data_lifetime = 24h 19 | 20 | # Directory where grafana can store logs 21 | logs = data/log 22 | 23 | # Directory where grafana will automatically scan and look for plugins 24 | plugins = data/plugins 25 | 26 | # folder that contains provisioning config files that grafana will apply on startup and while running. 27 | provisioning = mindsphere-conf/provisioning 28 | 29 | #################################### Server ############################## 30 | [server] 31 | # Protocol (http, https, socket) 32 | protocol = http 33 | 34 | # The ip address to bind to, empty will bind to all interfaces 35 | http_addr = 36 | 37 | # The http port to use 38 | http_port = 8080 39 | 40 | # The public facing domain name used to access grafana from a browser 41 | domain = 42 | 43 | # Redirect to correct domain if host header does not match domain 44 | # Prevents DNS rebinding attacks 45 | enforce_domain = false 46 | 47 | # The full public facing url 48 | root_url = https://%(domain)s/grafana/ 49 | 50 | # Log web requests 51 | router_logging = true 52 | 53 | # the path relative working path 54 | static_root_path = public 55 | 56 | # enable gzip 57 | enable_gzip = false 58 | 59 | # https certs & key file 60 | cert_file = 61 | cert_key = 62 | 63 | # Unix socket path 64 | socket = /tmp/grafana.sock 65 | 66 | #################################### Database ############################ 67 | [database] 68 | # You can configure the database connection by specifying type, host, name, user and password 69 | # as separate properties or as on string using the url property. 70 | 71 | # Either "mysql", "postgres" or "sqlite3", it's your choice 72 | type = sqlite3 73 | host = 127.0.0.1:3306 74 | name = grafana 75 | user = root 76 | # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" 77 | password = 78 | # Use either URL or the previous fields to configure the database 79 | # Example: mysql://user:secret@host:port/database 80 | url = 81 | 82 | # Max idle conn setting default is 2 83 | max_idle_conn = 2 84 | 85 | # Max conn setting default is 0 (mean not set) 86 | max_open_conn = 87 | 88 | # Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) 89 | conn_max_lifetime = 14400 90 | 91 | # Set to true to log the sql calls and execution times. 92 | log_queries = 93 | 94 | # For "postgres", use either "disable", "require" or "verify-full" 95 | # For "mysql", use either "true", "false", or "skip-verify". 96 | ssl_mode = disable 97 | 98 | ca_cert_path = 99 | client_key_path = 100 | client_cert_path = 101 | server_cert_name = 102 | 103 | # For "sqlite3" only, path relative to data_path setting 104 | path = grafana.db 105 | 106 | #################################### Session ############################# 107 | [session] 108 | # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" 109 | provider = file 110 | 111 | # Provider config options 112 | # memory: not have any config yet 113 | # file: session dir path, is relative to grafana data_path 114 | # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` 115 | # postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable 116 | # mysql: go-sql-driver/mysql dsn config string, examples: 117 | # `user:password@tcp(127.0.0.1:3306)/database_name` 118 | # `user:password@unix(/var/run/mysqld/mysqld.sock)/database_name` 119 | # memcache: 127.0.0.1:11211 120 | 121 | 122 | provider_config = sessions 123 | 124 | # Session cookie name 125 | cookie_name = grafana_sess 126 | 127 | # If you use session in https only, default is false 128 | cookie_secure = false 129 | 130 | # Session life time, default is 86400 131 | session_life_time = 86400 132 | gc_interval_time = 86400 133 | 134 | # Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) 135 | conn_max_lifetime = 14400 136 | 137 | #################################### Data proxy ########################### 138 | [dataproxy] 139 | 140 | # This enables data proxy logging, default is false 141 | logging = false 142 | 143 | #################################### Analytics ########################### 144 | [analytics] 145 | # Server reporting, sends usage counters to stats.grafana.org every 24 hours. 146 | # No ip addresses are being tracked, only simple counters to track 147 | # running instances, dashboard and error counts. It is very helpful to us. 148 | # Change this option to false to disable reporting. 149 | reporting_enabled = true 150 | 151 | # Set to false to disable all checks to https://grafana.com 152 | # for new versions (grafana itself and plugins), check is used 153 | # in some UI views to notify that grafana or plugin update exists 154 | # This option does not cause any auto updates, nor send any information 155 | # only a GET request to https://grafana.com to get latest versions 156 | check_for_updates = true 157 | 158 | # Google Analytics universal tracking code, only enabled if you specify an id here 159 | google_analytics_ua_id = 160 | 161 | # Google Tag Manager ID, only enabled if you specify an id here 162 | google_tag_manager_id = 163 | 164 | #################################### Security ############################ 165 | [security] 166 | # default admin user, created on startup 167 | admin_user = admin 168 | 169 | # default admin password, can be changed before first start of grafana, or in profile settings 170 | admin_password = admin 171 | 172 | # used for signing 173 | secret_key = SW2YcwTIb9zpOOhoPsMm 174 | 175 | # Auto-login remember days 176 | login_remember_days = 7 177 | cookie_username = grafana_user 178 | cookie_remember_name = grafana_remember 179 | 180 | # disable gravatar profile images 181 | disable_gravatar = false 182 | 183 | # data source proxy whitelist (ip_or_domain:port separated by spaces) 184 | data_source_proxy_whitelist = 185 | 186 | # disable protection against brute force login attempts 187 | disable_brute_force_login_protection = false 188 | 189 | #################################### Snapshots ########################### 190 | [snapshots] 191 | # snapshot sharing options 192 | external_enabled = true 193 | external_snapshot_url = https://snapshots-origin.raintank.io 194 | external_snapshot_name = Publish to snapshot.raintank.io 195 | 196 | # remove expired snapshot 197 | snapshot_remove_expired = true 198 | 199 | #################################### Dashboards ################## 200 | 201 | [dashboards] 202 | # Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1 203 | versions_to_keep = 20 204 | 205 | #################################### Users ############################### 206 | [users] 207 | # disable user signup / registration 208 | allow_sign_up = false 209 | 210 | # Allow non admin users to create organizations 211 | allow_org_create = false 212 | 213 | # Set to true to automatically assign new users to the default organization (id 1) 214 | auto_assign_org = true 215 | 216 | # Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true) 217 | auto_assign_org_id = 1 218 | 219 | # Default role new users will be automatically assigned (if auto_assign_org above is set to true) 220 | auto_assign_org_role = Viewer 221 | 222 | # Require email validation before sign up completes 223 | verify_email_enabled = false 224 | 225 | # Background text for the user field on the login page 226 | login_hint = email or username 227 | 228 | # Default UI theme ("dark" or "light") 229 | default_theme = dark 230 | 231 | # External user management 232 | external_manage_link_url = 233 | external_manage_link_name = 234 | external_manage_info = 235 | 236 | # Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. 237 | viewers_can_edit = false 238 | 239 | [auth] 240 | # Set to true to disable (hide) the login form, useful if you use OAuth 241 | disable_login_form = false 242 | 243 | # Set to true to disable the signout link in the side menu. useful if you use auth.proxy 244 | disable_signout_menu = false 245 | 246 | # URL to redirect the user to after sign out 247 | signout_redirect_url = 248 | 249 | #################################### Anonymous Auth ###################### 250 | [auth.anonymous] 251 | # enable anonymous access 252 | enabled = true 253 | 254 | # specify organization name that should be used for unauthenticated users 255 | org_name = Main Org. 256 | 257 | # specify role for unauthenticated users 258 | org_role = Admin 259 | 260 | #################################### Github Auth ######################### 261 | [auth.github] 262 | enabled = false 263 | allow_sign_up = true 264 | client_id = some_id 265 | client_secret = some_secret 266 | scopes = user:email,read:org 267 | auth_url = https://github.com/login/oauth/authorize 268 | token_url = https://github.com/login/oauth/access_token 269 | api_url = https://api.github.com/user 270 | team_ids = 271 | allowed_organizations = 272 | 273 | #################################### GitLab Auth ######################### 274 | [auth.gitlab] 275 | enabled = false 276 | allow_sign_up = true 277 | client_id = some_id 278 | client_secret = some_secret 279 | scopes = api 280 | auth_url = https://gitlab.com/oauth/authorize 281 | token_url = https://gitlab.com/oauth/token 282 | api_url = https://gitlab.com/api/v4 283 | allowed_groups = 284 | 285 | #################################### Google Auth ######################### 286 | [auth.google] 287 | enabled = false 288 | allow_sign_up = true 289 | client_id = some_client_id 290 | client_secret = some_client_secret 291 | scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email 292 | auth_url = https://accounts.google.com/o/oauth2/auth 293 | token_url = https://accounts.google.com/o/oauth2/token 294 | api_url = https://www.googleapis.com/oauth2/v1/userinfo 295 | allowed_domains = 296 | hosted_domain = 297 | 298 | #################################### Grafana.com Auth #################### 299 | # legacy key names (so they work in env variables) 300 | [auth.grafananet] 301 | enabled = false 302 | allow_sign_up = true 303 | client_id = some_id 304 | client_secret = some_secret 305 | scopes = user:email 306 | allowed_organizations = 307 | 308 | [auth.grafana_com] 309 | enabled = false 310 | allow_sign_up = true 311 | client_id = some_id 312 | client_secret = some_secret 313 | scopes = user:email 314 | allowed_organizations = 315 | 316 | #################################### Generic OAuth ####################### 317 | [auth.generic_oauth] 318 | name = OAuth 319 | enabled = false 320 | allow_sign_up = true 321 | client_id = some_id 322 | client_secret = some_secret 323 | scopes = user:email 324 | auth_url = 325 | token_url = 326 | api_url = 327 | team_ids = 328 | allowed_organizations = 329 | tls_skip_verify_insecure = false 330 | tls_client_cert = 331 | tls_client_key = 332 | tls_client_ca = 333 | 334 | #################################### Basic Auth ########################## 335 | [auth.basic] 336 | enabled = true 337 | 338 | #################################### Auth Proxy ########################## 339 | [auth.proxy] 340 | enabled = false 341 | header_name = X-WEBAUTH-USER 342 | header_property = username 343 | auto_sign_up = true 344 | ldap_sync_ttl = 60 345 | whitelist = 346 | 347 | #################################### Auth LDAP ########################### 348 | [auth.ldap] 349 | enabled = false 350 | config_file = /etc/grafana/ldap.toml 351 | allow_sign_up = true 352 | 353 | #################################### SMTP / Emailing ##################### 354 | [smtp] 355 | enabled = false 356 | host = 357 | user = 358 | # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" 359 | password = 360 | cert_file = 361 | key_file = 362 | skip_verify = false 363 | from_address = 364 | from_name = Grafana 365 | ehlo_identity = 366 | 367 | [emails] 368 | welcome_email_on_sign_up = false 369 | templates_pattern = emails/*.html 370 | 371 | #################################### Logging ########################## 372 | [log] 373 | # Either "console", "file", "syslog". Default is console and file 374 | # Use space to separate multiple modes, e.g. "console file" 375 | mode = console file 376 | 377 | # Either "debug", "info", "warn", "error", "critical", default is "info" 378 | level = info 379 | 380 | # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug 381 | filters = 382 | 383 | # For "console" mode only 384 | [log.console] 385 | level = 386 | 387 | # log line format, valid options are text, console and json 388 | format = console 389 | 390 | # For "file" mode only 391 | [log.file] 392 | level = 393 | 394 | # log line format, valid options are text, console and json 395 | format = text 396 | 397 | # This enables automated log rotate(switch of following options), default is true 398 | log_rotate = true 399 | 400 | # Max line number of single file, default is 1000000 401 | max_lines = 1000000 402 | 403 | # Max size shift of single file, default is 28 means 1 << 28, 256MB 404 | max_size_shift = 28 405 | 406 | # Segment log daily, default is true 407 | daily_rotate = true 408 | 409 | # Expired days of log file(delete after max days), default is 7 410 | max_days = 7 411 | 412 | [log.syslog] 413 | level = 414 | 415 | # log line format, valid options are text, console and json 416 | format = text 417 | 418 | # Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used. 419 | network = 420 | address = 421 | 422 | # Syslog facility. user, daemon and local0 through local7 are valid. 423 | facility = 424 | 425 | # Syslog tag. By default, the process' argv[0] is used. 426 | tag = 427 | 428 | #################################### Usage Quotas ######################## 429 | [quota] 430 | enabled = false 431 | 432 | #### set quotas to -1 to make unlimited. #### 433 | # limit number of users per Org. 434 | org_user = 10 435 | 436 | # limit number of dashboards per Org. 437 | org_dashboard = 100 438 | 439 | # limit number of data_sources per Org. 440 | org_data_source = 10 441 | 442 | # limit number of api_keys per Org. 443 | org_api_key = 10 444 | 445 | # limit number of orgs a user can create. 446 | user_org = 10 447 | 448 | # Global limit of users. 449 | global_user = -1 450 | 451 | # global limit of orgs. 452 | global_org = -1 453 | 454 | # global limit of dashboards 455 | global_dashboard = -1 456 | 457 | # global limit of api_keys 458 | global_api_key = -1 459 | 460 | # global limit on number of logged in users. 461 | global_session = -1 462 | 463 | #################################### Alerting ############################ 464 | [alerting] 465 | # Disable alerting engine & UI features 466 | enabled = true 467 | # Makes it possible to turn off alert rule execution but alerting UI is visible 468 | execute_alerts = true 469 | 470 | #################################### Explore ############################# 471 | [explore] 472 | # Enable the Explore section 473 | enabled = false 474 | 475 | #################################### Internal Grafana Metrics ############ 476 | # Metrics available at HTTP API Url /metrics 477 | [metrics] 478 | enabled = true 479 | interval_seconds = 10 480 | 481 | # Send internal Grafana metrics to graphite 482 | [metrics.graphite] 483 | # Enable by setting the address setting (ex localhost:2003) 484 | address = 485 | prefix = prod.grafana.%(instance_name)s. 486 | 487 | [grafana_net] 488 | url = https://grafana.com 489 | 490 | [grafana_com] 491 | url = https://grafana.com 492 | 493 | #################################### Distributed tracing ############ 494 | [tracing.jaeger] 495 | # jaeger destination (ex localhost:6831) 496 | address = 497 | # tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2) 498 | always_included_tag = 499 | # Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote 500 | sampler_type = const 501 | # jaeger samplerconfig param 502 | # for "const" sampler, 0 or 1 for always false/true respectively 503 | # for "probabilistic" sampler, a probability between 0 and 1 504 | # for "rateLimiting" sampler, the number of spans per second 505 | # for "remote" sampler, param is the same as for "probabilistic" 506 | # and indicates the initial sampling rate before the actual one 507 | # is received from the mothership 508 | sampler_param = 1 509 | 510 | #################################### External Image Storage ############## 511 | [external_image_storage] 512 | # You can choose between (s3, webdav, gcs, azure_blob, local) 513 | provider = 514 | 515 | [external_image_storage.s3] 516 | bucket_url = 517 | bucket = 518 | region = 519 | path = 520 | access_key = 521 | secret_key = 522 | 523 | [external_image_storage.webdav] 524 | url = 525 | username = 526 | password = 527 | public_url = 528 | 529 | [external_image_storage.gcs] 530 | key_file = 531 | bucket = 532 | path = 533 | 534 | [external_image_storage.azure_blob] 535 | account_name = 536 | account_key = 537 | container_name = 538 | 539 | [external_image_storage.local] 540 | # does not require any configuration 541 | -------------------------------------------------------------------------------- /devops/grafana/sample-conf/provisioning/dashboards/default.yaml: -------------------------------------------------------------------------------- 1 | # # config file version 2 | apiVersion: 1 3 | 4 | providers: 5 | - name: 'default' 6 | orgId: 1 7 | folder: '' 8 | type: file 9 | options: 10 | path: ./mindsphere-conf/provisioning/dashboards/json 11 | -------------------------------------------------------------------------------- /devops/grafana/sample-conf/provisioning/dashboards/json/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /devops/grafana/sample-conf/provisioning/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | # # config file version 2 | apiVersion: 1 3 | 4 | # # list of datasources that should be deleted from the database 5 | #deleteDatasources: 6 | # - name: Graphite 7 | # orgId: 1 8 | 9 | # # list of datasources to insert/update depending 10 | # # on what's available in the datbase 11 | datasources: 12 | # name of the datasource. Required 13 | - name: Prometheus 14 | # datasource type. Required 15 | type: prometheus 16 | # access mode. direct or proxy. Required 17 | access: proxy 18 | # org id. will default to orgId 1 if not specified 19 | orgId: 1 20 | # url 21 | url: 22 | # database password, if used 23 | # password: 24 | # # database user, if used 25 | # user: 26 | # # database name, if used 27 | # database: 28 | # # enable/disable basic auth 29 | # basicAuth: 30 | # # basic auth username 31 | # basicAuthUser: 32 | # # basic auth password 33 | # basicAuthPassword: 34 | # # enable/disable with credentials headers 35 | # withCredentials: 36 | # # mark as default datasource. Max one per org 37 | # isDefault: 38 | # # fields that will be converted to json and stored in json_data 39 | # jsonData: 40 | # graphiteVersion: "1.1" 41 | # tlsAuth: true 42 | # tlsAuthWithCACert: true 43 | # httpHeaderName1: "Authorization" 44 | # # json object of data that will be encrypted. 45 | # secureJsonData: 46 | # tlsCACert: "..." 47 | # tlsClientCert: "..." 48 | # tlsClientKey: "..." 49 | # # 50 | # httpHeaderValue1: "Bearer xf5yhfkpsnmgo" 51 | version: 1 52 | # allow users to edit datasources from the UI. 53 | editable: false 54 | -------------------------------------------------------------------------------- /devops/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: devopsadmin 4 | path: ./devopsadmin 5 | buildpacks: 6 | - nodejs_buildpack 7 | services: 8 | - devopsadmin-logme 9 | memory: 64M 10 | disk_quota: 128M 11 | random-route: true 12 | stack: cflinuxfs3 13 | env: 14 | OPTIMIZE_MEMORY: true 15 | MDSP_TENANT: ((mdspTenant)) 16 | MDSP_REGION: ((mdspRegion)) 17 | PROMETHEUS_URL: prometheus-((mdspTenant)).apps.((mdspRegion)).mindsphere.io 18 | GRAFANA_URL: grafana-((mdspTenant)).apps.((mdspRegion)).mindsphere.io 19 | TECH_USER_CLIENT_ID: "((techUserClientId))" 20 | TECH_USER_CLIENT_SECRET: "((techUserClientSecret))" 21 | NOTIFICATION_EMAIL: "((notificationEmail))" 22 | NOTIFICATION_MOBILE_NUMBER: "((notificationMobileNumber))" 23 | - name: prometheus 24 | path: ./prometheus 25 | buildpacks: 26 | - go_buildpack 27 | services: 28 | - devopsadmin-logme 29 | memory: 128M 30 | disk_quota: 1G 31 | random-route: false 32 | routes: 33 | - route: prometheus-((mdspTenant)).apps.((mdspRegion)).mindsphere.io 34 | stack: cflinuxfs3 35 | command: prometheus --config.file=./mindsphere-conf/prometheus.yml --log.level=info --web.listen-address=:8080 --web.route-prefix=/ --web.external-url=${PROM_EXTERNAL_URL} 36 | env: 37 | GOPACKAGENAME: github.com/prometheus/prometheus 38 | GOVERSION: go1.10 39 | GO_INSTALL_PACKAGE_SPEC: github.com/prometheus/prometheus/cmd/prometheus 40 | PROM_EXTERNAL_URL: "((promExternalUrl))" 41 | - name: grafana 42 | path: ./grafana 43 | buildpacks: 44 | - go_buildpack 45 | services: 46 | - grafana-postgres 47 | - devopsadmin-logme 48 | memory: 96M 49 | disk_quota: 256M 50 | random-route: false 51 | routes: 52 | - route: grafana-((mdspTenant)).apps.((mdspRegion)).mindsphere.io 53 | stack: cflinuxfs3 54 | command: grafana-server -config mindsphere-conf/defaults.custom.ini 55 | env: 56 | GOPACKAGENAME: github.com/grafana/grafana 57 | GOVERSION: go1.10 58 | GO_INSTALL_PACKAGE_SPEC: github.com/grafana/grafana/pkg/cmd/grafana-server 59 | GRAFANA_DATABASE_URL: "((grafanaDatabaseUrl))" 60 | GRAFANA_SMTP_PASSWORD: "((grafanaSmtpPassword))" 61 | -------------------------------------------------------------------------------- /devops/prometheus/.cfignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /data/ 3 | /promtool 4 | /prometheus 5 | 6 | # IDEs and editors 7 | /.idea/ 8 | /.vscode/ 9 | -------------------------------------------------------------------------------- /devops/prometheus/.gitignore: -------------------------------------------------------------------------------- 1 | !/conf/*.yml.sample 2 | /conf/*.yml 3 | -------------------------------------------------------------------------------- /devops/prometheus/README.md: -------------------------------------------------------------------------------- 1 | # Prometheus on MindSphere CloudFoundry 2 | 3 | ## Tested Environment 4 | 5 | ```sh 6 | $ cf version 7 | cf version 6.38.0+7ddf0aadd.2018-08-07 8 | 9 | $ go version 10 | go version go1.10.1 darwin/amd64 11 | 12 | $ yarn --version 13 | 1.7.0 14 | ``` 15 | 16 | ## Configuration 17 | 18 | The following environment variables are recognized by the prometheus server: 19 | 20 | | Variable | Description | Required | 21 | |--------------|-------------|----------| 22 | | `PROM_EXTERNAL_URL` | External Prometheus URL | yes | 23 | 24 | ## Deploy to MindSphere 25 | 26 | Prometheus is deployed from its github sources. Since Prometheus is a Golang 27 | project and we require some custom configuration files to be uploaded when 28 | deploying to cloudfoundry, a script is provided that copies the relevant files 29 | into the checked out sources. 30 | 31 | 1. Copy and adapt `conf/prometheus.yml.sample` to `conf/prometheus.yml`, 32 | specifically: 33 | - `targets` of `todo` job, point to the internal address of the todo app 34 | 35 | 1. Follow the instructions on the [devopsadmin readme](../README.md) and copy 36 | and adapt the cf vars file with the required configuration 37 | 38 | 1. Download the prometheus source code. This will save the code to your 39 | `${GOPATH}` (typically `~/go` or `~/.go`) 40 | 41 | ```sh 42 | go get github.com/prometheus/prometheus/cmd/... 43 | ``` 44 | 45 | 1. Checkout the latest tested version (`v2.5.0` at the time of writing): 46 | ```sh 47 | cd ${GOPATH}/src/github.com/prometheus/prometheus 48 | git checkout v2.5.0 49 | ``` 50 | 51 | 1. The manifest will bind itself to a LogMe (ELK) service to gather all logs, 52 | make sure you have created it in advance: 53 | 54 | ```sh 55 | cf create-service logme logme-xs devopsadmin-logme 56 | ``` 57 | 58 | 1. Ensure you are in the right CloudFoundry space and push to MindSphere. The 59 | command will also copy the required configuration files to the prometheus 60 | source before push: 61 | 62 | ```sh 63 | CF_VARS_FILE="" \ 64 | PROM_CONF_FILE="" \ 65 | ./deploy_dev.sh 66 | ``` 67 | 68 | The param `CF_VARS_FILE` is your CloudFoundry adapted vars file. 69 | 70 | The param `PROM_CONF_FILE` points to your adapted configuration file. 71 | 72 | ## Local Build and Execution 73 | 74 | In case you want to build & run locally the prometheus distribution: 75 | 76 | ```sh 77 | cd "$(go env GOPATH)/src/github.com/prometheus/prometheus" 78 | make build 79 | 80 | ./prometheus --config.file=your_config.yml 81 | ``` 82 | 83 | ## License 84 | 85 | This project is licensed under the MIT License 86 | -------------------------------------------------------------------------------- /devops/prometheus/conf/prometheus.yml.sample: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | rule_files: 6 | # - "first.rules" 7 | # - "second.rules" 8 | 9 | scrape_configs: 10 | - job_name: prometheus 11 | static_configs: 12 | - targets: ['localhost:8080'] 13 | metrics_path: '/metrics' 14 | - job_name: todo 15 | static_configs: 16 | - targets: [''] 17 | metrics_path: '/metrics' 18 | scheme: https 19 | -------------------------------------------------------------------------------- /devops/prometheus/deploy_dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ -f "${CF_VARS_FILE}" ]] || { echo "Missing CF_VARS_FILE" && exit 1; } 4 | [[ -f "${PROM_CONF_FILE}" ]] || { echo "Missing PROM_CONF_FILE" && exit 1; } 5 | 6 | GOPATH=$(go env GOPATH) 7 | PROM_SRC_PATH="${GOPATH}/src/github.com/prometheus/prometheus" 8 | 9 | cp .cfignore ${PROM_SRC_PATH} 10 | mkdir -p "${PROM_SRC_PATH}/mindsphere-conf/" 11 | cp ${PROM_CONF_FILE} "${PROM_SRC_PATH}/mindsphere-conf/prometheus.yml" 12 | 13 | cf push prometheus \ 14 | -f ../manifest.yml \ 15 | -p "${PROM_SRC_PATH}" \ 16 | --vars-file "${CF_VARS_FILE}" 17 | -------------------------------------------------------------------------------- /devops/vars-file.yml.sample: -------------------------------------------------------------------------------- 1 | # MindSphere tenant identifier 2 | mdspTenant: "" 3 | 4 | # Mindsphere region identifier 5 | mdspRegion: "" 6 | 7 | # MindSphere technical user credentials id 8 | techUserClientId: "" 9 | 10 | # MindSphere technical user credentials secret 11 | techUserClientSecret: "" 12 | 13 | # Recipient email address used for notifications triggered from devopsadmin 14 | notificationEmail: "" 15 | 16 | # Recipient mobile number used for notifications triggered from devopsadmin 17 | # Uses E.164 format 18 | notificationMobileNumber: "<+41999999999>" 19 | 20 | # Address under which prometheus will be reachable from the internet 21 | # /prometheus/ 22 | promExternalUrl: "-devopsadmin-..mindsphere.io/prometheus/>" 23 | 24 | # Full grafana database service connection url, including user and password 25 | # Can be obtained from `cf env grafana` after binding the app 26 | grafanaDatabaseUrl: "" 27 | 28 | # Password of the smtp server that you use for email notifications in grafana 29 | grafanaSmtpPassword: "" 30 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | // Protractor configuration file, see link for more information 7 | // https://github.com/angular/protractor/blob/master/lib/config.ts 8 | 9 | const { SpecReporter } = require('jasmine-spec-reporter'); 10 | const { JUnitXmlReporter } = require('jasmine-reporters'); 11 | const { spawn } = require('child_process'); 12 | 13 | exports.config = { 14 | allScriptsTimeout: 11000, 15 | specs: [ 16 | './src/**/*.e2e-spec.ts' 17 | ], 18 | capabilities: { 19 | 'browserName': 'chrome' 20 | }, 21 | directConnect: !process.env.SELENIUM_URL, 22 | seleniumAddress: process.env.SELENIUM_URL, 23 | baseUrl: process.env.BASE_URL || 'http://localhost:3000', 24 | framework: 'jasmine', 25 | jasmineNodeOpts: { 26 | showColors: true, 27 | defaultTimeoutInterval: 30000, 28 | print: function() {} 29 | }, 30 | onPrepare() { 31 | require('ts-node').register({ 32 | project: require('path').join(__dirname, './tsconfig.e2e.json') 33 | }); 34 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 35 | 36 | jasmine.getEnv().addReporter(new JUnitXmlReporter({ 37 | savePath: 'test-reports/', 38 | consolidateAll: true 39 | })); 40 | }, 41 | 42 | beforeLaunch() { 43 | if(!process.env.MONGODB_URL) { 44 | process.env.MONGODB_URL = 'mongodb://localhost:27017/e2e'; 45 | } 46 | backendProcess = spawn('yarn', ['start'], {cwd: require('path').join(__dirname, '../server')}); 47 | 48 | backendProcess.stdout.on('data', function (data) { 49 | console.log('backend stdout: ' + data); 50 | }); 51 | 52 | backendProcess.stderr.on('data', function (data) { 53 | console.log('backend stderr: ' + data); 54 | }); 55 | }, 56 | onCleanUp() { 57 | backendProcess.kill(); 58 | } 59 | }; 60 | 61 | let backendProcess; 62 | 63 | if (!process.env.SELENIUM_URL) { 64 | const chromedriver = require('chromedriver'); 65 | exports.config.chromeDriver = chromedriver.path; 66 | console.log('Using ChromeDriver: ', exports.config.chromeDriver); 67 | } 68 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { AppPage } from './app.po'; 7 | 8 | describe('workspace-project App', () => { 9 | let page: AppPage; 10 | 11 | beforeEach(() => { 12 | page = new AppPage(); 13 | }); 14 | 15 | it('should display welcome message', () => { 16 | page.navigateTo(); 17 | expect(page.getParagraphText()).toEqual('TODO'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { browser, by, element } from 'protractor'; 7 | 8 | export class AppPage { 9 | navigateTo() { 10 | browser.sleep(5000); 11 | return browser.get('/'); 12 | } 13 | 14 | getParagraphText() { 15 | return element(by.css('app-root h1')).getText(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/src/mdsp.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { browser, by, element } from 'protractor'; 7 | 8 | describe('MindSphere OS Bar', () => { 9 | 10 | it('should have the poweredByMindSphere image', () => { 11 | browser.get('/'); 12 | expect(element(by.css('img.poweredByMindSphere')).isElementPresent); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/todo.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { browser, by, element } from 'protractor'; 7 | import { protractor } from 'protractor/built/ptor'; 8 | 9 | describe('Todo list manipulation', () => { 10 | 11 | beforeEach(() => { 12 | browser.get('/'); 13 | }); 14 | 15 | it('should be able to add an element', () => { 16 | const taskTitle = element(by.css('input.form-control')); 17 | taskTitle.sendKeys('code'); 18 | browser.actions().sendKeys(protractor.Key.ENTER).perform(); 19 | browser.sleep(2000); 20 | 21 | const firstEntry = element.all(by.css('h1.form-control')).first(); 22 | expect(firstEntry.getText()).toContain('code'); 23 | }); 24 | 25 | it('should be able to delete an element', () => { 26 | const firstEntry = element.all(by.css('.fa.fa-trash-o.text-white')).first(); 27 | firstEntry.click(); 28 | expect(element.all(by.css('h1.form-control')).count()).toBe(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /e2e/src/userinfo.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { browser, by, element } from 'protractor'; 7 | 8 | describe('UserInfo display', () => { 9 | 10 | beforeEach(() => { 11 | browser.get('/'); 12 | }); 13 | 14 | it('should show the user data', () => { 15 | const firstEntry = element.all(by.css('app-userinfo')).first(); 16 | expect(firstEntry.getText()).toContain('john.doe@example.com'); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/images/architecture.png -------------------------------------------------------------------------------- /images/devopsadmin-grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/images/devopsadmin-grafana.png -------------------------------------------------------------------------------- /images/todo-api-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/images/todo-api-docs.png -------------------------------------------------------------------------------- /images/todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/images/todo.png -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Adapt the following variables to your environment via `cf push --var` 3 | # 4 | # - mdspTenant 5 | # - mdspRegion 6 | # 7 | applications: 8 | - name: todo 9 | path: server/ 10 | buildpacks: 11 | - nodejs_buildpack 12 | services: 13 | - todo-mongodb 14 | - todo-logme 15 | memory: 192M 16 | disk_quota: 256M 17 | random-route: true 18 | stack: cflinuxfs3 19 | instances: 1 20 | health-check-type: http 21 | health-check-http-endpoint: /health_check 22 | env: 23 | OPTIMIZE_MEMORY: true 24 | MDSP_TENANT: ((mdspTenant)) 25 | MDSP_REGION: ((mdspRegion)) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-frontend", 3 | "version": "0.5.0", 4 | "description": "The frontend of a simple todo app with a MongoDB backend", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://gitlab.com/mindsphere/devops-demo.git" 8 | }, 9 | "author": "Roger Meier ", 10 | "contributors": [ 11 | "Fabio Huser ", 12 | "Diego Louzán " 13 | ], 14 | "license": "MIT", 15 | "scripts": { 16 | "ng": "ng", 17 | "start": "ng serve --configuration=mdsplocal", 18 | "build": "ng build --configuration=mdsplocal", 19 | "build:dev": "ng build", 20 | "build:prod": "ng build --configuration=production", 21 | "test": "ng test", 22 | "eclint": "eclint check $(git ls-files)", 23 | "lint": "ng lint && markdownlint --ignore '**/node_modules/**' . && yarn eclint", 24 | "e2e": "ng e2e", 25 | "license": "license-checker --onlyAllow 'Apache-2.0; BSD; BSD-2-Clause; BSD-3-Clause; ISC; MIT; Unlicense; WTFPL; CC-BY-3.0; CC0-1.0' --production", 26 | "license:all": "yarn license && yarn --cwd server/ license && yarn --cwd devops/devopsadmin license", 27 | "commitlint": "commitlint --from=origin/master", 28 | "mdsp:zip": "(which zip > /dev/null || { echo \"Please install the 'zip' command\"; exit 1; }) && { rm -f todo.zip; zip -r todo ./server -x 'server/node_modules/*'; }" 29 | }, 30 | "private": true, 31 | "engines": { 32 | "node": ">=8" 33 | }, 34 | "dependencies": { 35 | "@angular/animations": "^6.1.0", 36 | "@angular/common": "^6.1.0", 37 | "@angular/compiler": "^6.1.0", 38 | "@angular/core": "^6.1.0", 39 | "@angular/forms": "^6.1.0", 40 | "@angular/platform-browser": "^6.1.0", 41 | "@angular/platform-browser-dynamic": "^6.1.0", 42 | "@angular/router": "^6.1.0", 43 | "bootstrap": "^4.3.1", 44 | "core-js": "^2.5.4", 45 | "font-awesome": "^4.7.0", 46 | "ngx-cookie-service": "^2.0.2", 47 | "rxjs": "^6.3.3", 48 | "zone.js": "~0.8.26" 49 | }, 50 | "devDependencies": { 51 | "@angular-devkit/build-angular": "~0.8.0", 52 | "@angular/cli": "~6.1.4", 53 | "@angular/compiler-cli": "^6.1.0", 54 | "@angular/language-service": "^6.1.0", 55 | "@commitlint/cli": "^7.5.0", 56 | "@commitlint/config-conventional": "^7.5.0", 57 | "@types/jasmine": "~2.8.6", 58 | "@types/jasminewd2": "~2.0.3", 59 | "@types/node": "~8.9.4", 60 | "chromedriver": "^2.41.0", 61 | "codelyzer": "^4.5.0", 62 | "eclint": "^2.8.0", 63 | "https-proxy-agent": "^2.2.1", 64 | "husky": "^1.3.1", 65 | "jasmine-core": "~2.99.1", 66 | "jasmine-reporters": "^2.3.2", 67 | "jasmine-spec-reporter": "~4.2.1", 68 | "karma": "^3.1.4", 69 | "karma-chrome-launcher": "~2.2.0", 70 | "karma-coverage-istanbul-reporter": "~2.0.0", 71 | "karma-jasmine": "~1.1.1", 72 | "karma-jasmine-html-reporter": "^0.2.2", 73 | "karma-junit-reporter": "^1.2.0", 74 | "karma-webdriver-launcher": "^1.0.5", 75 | "license-checker": "^20.2.0", 76 | "markdownlint-cli": "^0.13.0", 77 | "nyc": "^14.1.1", 78 | "protractor": "~5.4.0", 79 | "ts-node": "~5.0.1", 80 | "tslint": "~5.9.1", 81 | "typescript": "~2.9.2" 82 | }, 83 | "commitlint": { 84 | "extends": [ 85 | "@commitlint/config-conventional" 86 | ] 87 | }, 88 | "husky": { 89 | "hooks": { 90 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /proxy.conf.js: -------------------------------------------------------------------------------- 1 | var HttpsProxyAgent = require('https-proxy-agent'); 2 | var proxyConfig = [ 3 | { 4 | context: '/api', 5 | target: 'https://gateway.eu1.mindsphere.io', 6 | secure: true, 7 | changeOrigin: true, 8 | logLevel: 'debug' 9 | }, 10 | { 11 | context: '/v1', 12 | target: 'http://localhost:3000', 13 | secure: false, 14 | changeOrigin: true, 15 | logLevel: 'debug' 16 | } 17 | ]; 18 | 19 | function setupForCorporateProxy(proxyConfig) { 20 | console.log('Checking corporate proxy settings'); 21 | var proxyServer = process.env.http_proxy || process.env.HTTP_PROXY; 22 | if (proxyServer) { 23 | var agent = new HttpsProxyAgent(proxyServer); 24 | console.log('Using corporate proxy server: ' + proxyServer); 25 | proxyConfig.forEach(function(entry) { 26 | // Do not proxy requests targeted to localhost 27 | if (entry.target.search(/localhost|127\.0\.0\.1/i) < 0) { 28 | entry.agent = agent; 29 | } else { 30 | console.log('Ignoring proxy for localhost target entry:', entry); 31 | } 32 | }); 33 | } else { 34 | console.log('No proxy detected, using direct connection'); 35 | } 36 | return proxyConfig; 37 | } 38 | 39 | module.exports = setupForCorporateProxy(proxyConfig); 40 | -------------------------------------------------------------------------------- /server/model.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const mongoose = require('mongoose'); 7 | 8 | const TodoSchema = mongoose.Schema({ 9 | user_id: { 10 | type: String, 11 | required: true 12 | }, 13 | title: { 14 | type: String, 15 | required: true 16 | } 17 | }); 18 | 19 | module.exports = mongoose.model('Todo', TodoSchema); 20 | -------------------------------------------------------------------------------- /server/openapi-spec-urls.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | module.exports = [ 7 | { 8 | url: '/api-specs/api.yaml', 9 | name: 'Todo API' 10 | }, 11 | { 12 | url: 'https://developer.mindsphere.io/api/specs/agentmanagement-v3.yaml', 13 | name: 'Agent Management API' 14 | }, 15 | { 16 | url: 'https://developer.mindsphere.io/api/specs/anomalydetection-v3.yaml', 17 | name: 'Anomaly Detection API' 18 | }, 19 | { 20 | url: 'https://developer.mindsphere.io/api/specs/assetmanagement-v3.yaml', 21 | name: 'Asset Management API' 22 | }, 23 | { 24 | url: 'https://developer.mindsphere.io/api/specs/dataflowengine-v3.yaml', 25 | name: 'Data Flow Engine API' 26 | }, 27 | { 28 | url: 'https://developer.mindsphere.io/api/specs/eventanalytics-v3.yaml', 29 | name: 'Event Analytics API' 30 | }, 31 | { 32 | url: 'https://developer.mindsphere.io/api/specs/eventmanagement-v3.yaml', 33 | name: 'Event Management API' 34 | }, 35 | { 36 | url: 'https://developer.mindsphere.io/api/specs/identitymanagement-v3.yaml', 37 | name: 'Identity Management API' 38 | }, 39 | { 40 | url: 'https://developer.mindsphere.io/api/specs/iotfile-v3.yaml', 41 | name: 'IoT File API' 42 | }, 43 | { 44 | url: 'https://developer.mindsphere.io/api/specs/iottimeseries-v3.yaml', 45 | name: 'IoT Timeseries API' 46 | }, 47 | { 48 | url: 'https://developer.mindsphere.io/api/specs/iottsaggregates-v3.yaml', 49 | name: 'IoT TS Aggregates API' 50 | }, 51 | { 52 | url: 'https://developer.mindsphere.io/api/specs/kpicalculation-v3.yaml', 53 | name: 'KPI Calculation API' 54 | }, 55 | { 56 | url: 'https://developer.mindsphere.io/api/specs/mindconnect-v3.yaml', 57 | name: 'MindConnect API' 58 | }, 59 | { 60 | url: 'https://developer.mindsphere.io/api/specs/notification-v3.yaml', 61 | name: 'Notification API' 62 | }, 63 | { 64 | url: 'https://developer.mindsphere.io/api/specs/oauthauthorizationserver-v3.yaml', 65 | name: 'OAuth Authorization Server API' 66 | }, 67 | { 68 | url: 'https://developer.mindsphere.io/api/specs/signalcalculation-v3.yaml', 69 | name: 'Signal Calculation API' 70 | }, 71 | { 72 | url: 'https://developer.mindsphere.io/api/specs/signalvalidation-v3.yaml', 73 | name: 'Signal Validation API' 74 | }, 75 | { 76 | url: 'https://developer.mindsphere.io/api/specs/tenantmanagement-v4.yaml', 77 | name: 'Tenant Management API' 78 | }, 79 | { 80 | url: 'https://developer.mindsphere.io/api/specs/trendprediction-v3.yaml', 81 | name: 'Trend Prediction API' 82 | }, 83 | { 84 | url: 'https://developer.mindsphere.io/api/specs/usagetransparency-v3.yaml', 85 | name: 'Usage Transparency API' 86 | } 87 | ]; 88 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "version": "0.5.0", 4 | "description": "The backend of a simple todo app with a MongoDB data store", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://gitlab.com/mindsphere/devops-demo.git" 8 | }, 9 | "author": "Roger Meier ", 10 | "contributors": [ 11 | "Fabio Huser ", 12 | "Diego Louzán " 13 | ], 14 | "license": "MIT", 15 | "scripts": { 16 | "start": "node server.js", 17 | "license": "license-checker --onlyAllow 'MIT; Apache-2.0; ISC;BSD-2-Clause; BSD-3-Clause; WTFPL; AFLv2.1; Unlicense' --production" 18 | }, 19 | "private": true, 20 | "engines": { 21 | "node": ">=8" 22 | }, 23 | "dependencies": { 24 | "body-parser": "^1.18.3", 25 | "cors": "^2.8.4", 26 | "express": "^4.16.3", 27 | "express-prom-bundle": "^4.2.0", 28 | "helmet": "^3.13.0", 29 | "http": "^0.0.0", 30 | "jsonwebtoken": "^8.3.0", 31 | "jwks-rsa": "^1.3.0", 32 | "mongoose": "^5.2.9", 33 | "swagger-ui-express": "^4.0.1" 34 | }, 35 | "devDependencies": { 36 | "license-checker": "^20.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | const http = require('http'); 7 | const express = require('express'); 8 | const helmet = require('helmet'); 9 | const cors = require('cors'); 10 | const bodyparser = require('body-parser'); 11 | const promBundle = require('express-prom-bundle'); 12 | const swaggerUi = require('swagger-ui-express'); 13 | const jwt = require('jsonwebtoken'); 14 | const jwksrsa = require('jwks-rsa'); 15 | 16 | const mongoose = require('mongoose'); 17 | const TodoModel = require('./model'); 18 | 19 | const openApiSpecUrls = require('./openapi-spec-urls.js'); 20 | 21 | 22 | const TodoServer = function() { 23 | 24 | const setupDb = () => { 25 | let mongoUrl = process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017/todo'; 26 | 27 | if (process.env.VCAP_SERVICES) { 28 | const cf_vcap_services = JSON.parse(process.env.VCAP_SERVICES); 29 | mongoUrl = cf_vcap_services.mongodb32[0].credentials.uri; 30 | } 31 | 32 | mongoose.connect(mongoUrl, { useNewUrlParser: true }) 33 | .then(()=> { 34 | console.log('Connected to ' + mongoUrl); 35 | }) 36 | .catch(()=> { 37 | console.error('Error Connecting to ' + mongoUrl); 38 | process.exit(1); 39 | }); 40 | }; 41 | 42 | const setupWebserver = (app) => { 43 | // Security handler 44 | // Disable X-XSS-Protection filter already set by the MindSphere Gateway 45 | app.use(helmet({ 46 | xssFilter: false 47 | })); 48 | 49 | app.use(bodyparser.json()); 50 | app.use(bodyparser.urlencoded({ extended: false })); 51 | app.use(cors()); 52 | }; 53 | 54 | const setupAuth = (app) => { 55 | if (process.env.VCAP_APPLICATION) { 56 | if (!process.env.MDSP_TENANT) throw new Error('missing MDSP_TENANT configuration'); 57 | if (!process.env.MDSP_REGION) throw new Error('missing MDSP_REGION configuration'); 58 | 59 | console.log(`Configured MindSphere Tenant: ${process.env.MDSP_TENANT}`); 60 | console.log(`Configured MindSphere Region: ${process.env.MDSP_REGION}`); 61 | } 62 | 63 | const NON_MDSP_USER = { 64 | id: '0000-0000', 65 | name: 'John Doe', 66 | email: 'john.doe@example.com', 67 | }; 68 | 69 | const jwksClient = jwksrsa({ 70 | cache: true, 71 | rateLimit: true, 72 | jwksRequestsPerMinute: 5, 73 | jwksUri: `https://${process.env.MDSP_TENANT}.piam.${process.env.MDSP_REGION}.mindsphere.io/token_keys` 74 | }); 75 | const getKey = (header, callback) => { 76 | jwksClient.getSigningKey(header.kid, (err, key) => { 77 | const signingKey = key.publicKey || key.rsaPublicKey; 78 | callback(null, signingKey); 79 | }); 80 | }; 81 | const options = { 82 | issuer: `https://${process.env.MDSP_TENANT}.piam.${process.env.MDSP_REGION}.mindsphere.io/oauth/token`, 83 | algorithms: ['RS256'] 84 | }; 85 | 86 | // On API requests, expect a user identifier: 87 | // - local: use static identifier (same for all) 88 | // - mdsp: extract & use mdsp user identifier from jwt 89 | app.use('/v1/', (req, res, next) => { 90 | if (process.env.VCAP_APPLICATION) { 91 | if (req.headers.authorization) { 92 | let splitAuthHeader = req.headers.authorization.split(' '); 93 | if (splitAuthHeader[0].toLowerCase() === 'bearer') { 94 | new Promise((resolve, reject) => { 95 | jwt.verify(splitAuthHeader[1], getKey, options, (err, token) => { 96 | if (err || !token) { 97 | reject(err); 98 | } 99 | resolve(token); 100 | }); 101 | }).then((token) => { 102 | if (token.user_id) { 103 | res.locals.todo_user = { 104 | id: token.user_id, 105 | name: token.user_name, 106 | email: token.email, 107 | }; 108 | next(); 109 | } else { 110 | next('cannot find user id in token'); 111 | } 112 | }).catch((err) => { 113 | next(err); 114 | }); 115 | } 116 | } 117 | } else { 118 | res.locals.todo_user = NON_MDSP_USER; 119 | next(); 120 | } 121 | }); 122 | 123 | // TODO protect prometheus metrics access 124 | }; 125 | 126 | const setupEndpoints = (app) => { 127 | // Static mappings 128 | app.use(express.static(__dirname + '/static/')); 129 | app.use('/api-specs', express.static(__dirname + '/specs/')); 130 | 131 | // Prometheus metrics 132 | app.use(promBundle({includeMethod: true, includePath: true})); 133 | 134 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(null, { 135 | explorer: true, 136 | swaggerOptions: { 137 | validatorUrl: null, 138 | urls: openApiSpecUrls, 139 | requestInterceptor: (request) => { 140 | const getCookie = (name) => { 141 | const pair = document.cookie.match(new RegExp(name + '=([^;]+)')); 142 | return !!pair ? pair[1] : null; 143 | }; 144 | request.headers['x-xsrf-token'] = getCookie('XSRF-TOKEN'); 145 | request.headers.origin = `${window.location.protocol}//${window.location.host}/`; 146 | 147 | // HORRIBLE, HORRIBLE HACK, KITTEN DIE BECAUSE OF THIS 148 | // Solves https://github.com/swagger-api/swagger-js/issues/1027 149 | // 150 | // Check if the url matches '/api/specs' (url points to developer.mindsphere.io); 151 | // if not, then replace the host by the actual url of the todo app 152 | // so that 'Try me out' links work properly 153 | if (request.url.indexOf('http') !== -1) { 154 | const url = new URL(request.url); 155 | if (url.pathname.indexOf('/api/specs') === -1) { 156 | url.hostname = window.location.hostname; 157 | request.url = url.href; 158 | } 159 | } 160 | 161 | return request; 162 | } 163 | } 164 | })); 165 | 166 | app.post('/v1/todo', (req, res) => { 167 | req.body.user_id = getCurrentUser(res).id; 168 | 169 | TodoModel.create(req.body) 170 | .then( 171 | (success) => { 172 | res.sendStatus(201); 173 | }, 174 | (error) => { 175 | res.sendStatus(400); 176 | }); 177 | }); 178 | 179 | app.get('/v1/todo', (req, res) => { 180 | const userId = getCurrentUser(res).id; 181 | TodoModel.find({ user_id: userId }) 182 | .then( 183 | (tasks) => { 184 | res.json(tasks); 185 | }, 186 | (error) => { 187 | res.sendStatus(400); 188 | }); 189 | }); 190 | 191 | app.delete('/v1/todo/:id', (req, res) => { 192 | TodoModel.deleteOne( 193 | { 194 | _id: mongoose.Types.ObjectId(req.params.id), 195 | user_id: getCurrentUser(res).id, 196 | }) 197 | .then( 198 | (success) => { 199 | res.sendStatus(200); 200 | }, 201 | (error) => { 202 | res.sendStatus(400); 203 | }); 204 | }); 205 | 206 | app.get('/v1/me', (req, res) => { 207 | res.json(getCurrentUser(res)); 208 | }); 209 | 210 | app.get('/health_check', (req, res) => { 211 | if (mongoose.connection.readyState === 1) { 212 | res.sendStatus(200); 213 | } 214 | else { 215 | res.sendStatus(503); 216 | } 217 | }); 218 | 219 | // Send all other request to the angular app 220 | app.get('*', (req, res) => { 221 | res.sendFile(__dirname + '/static/'); 222 | }); 223 | }; 224 | 225 | const getCurrentUser = (res) => { 226 | if (!res.locals.todo_user) { 227 | console.error('unknown user, no data available or unauthenticated'); 228 | } 229 | return res.locals.todo_user; 230 | }; 231 | 232 | this.run = () => { 233 | const app = express(); 234 | const server = http.createServer(app); 235 | 236 | setupDb(); 237 | setupWebserver(app); 238 | setupAuth(app); 239 | setupEndpoints(app); 240 | 241 | server.listen(process.env.PORT || 3000, function() { 242 | console.log('Listening on port', server.address().port); 243 | }); 244 | } 245 | }; 246 | 247 | const todoServer = new TodoServer(); 248 | todoServer.run(); 249 | -------------------------------------------------------------------------------- /server/specs/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Todo API 4 | version: 0.2.0 5 | license: 6 | name: MIT 7 | servers: 8 | - url: / 9 | paths: 10 | /v1/todo: 11 | get: 12 | summary: List todo tasks 13 | responses: 14 | '200': 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/TaskFull" 19 | '400': 20 | content: 21 | text/plain: 22 | type: string 23 | enum: ["Bad Request"] 24 | post: 25 | summary: Create todo task 26 | requestBody: 27 | required: true 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/Task" 32 | responses: 33 | '201': 34 | description: Successful create 35 | content: 36 | text/plain: 37 | type: string 38 | enum: ["Created"] 39 | '400': 40 | description: Invalid request 41 | content: 42 | text/plain: 43 | type: string 44 | enum: ["Bad Request"] 45 | /v1/todo/{id}: 46 | delete: 47 | summary: Remove single todo task 48 | parameters: 49 | - name: id 50 | in: path 51 | description: Task identifier 52 | required: true 53 | schema: 54 | type: string 55 | responses: 56 | '200': 57 | description: Successful delete 58 | content: 59 | text/plain: 60 | type: string 61 | enum: ["OK"] 62 | '400': 63 | content: 64 | text/plain: 65 | type: string 66 | enum: ["Bad Request"] 67 | /v1/me: 68 | get: 69 | summary: Query personal information of the user perfoming the request 70 | responses: 71 | '200': 72 | description: User data 73 | content: 74 | application/json: 75 | schema: 76 | $ref: "#/components/schemas/UserInfo" 77 | '400': 78 | description: Invalid request 79 | content: 80 | text/plain: 81 | type: string 82 | enum: ["Bad Request"] 83 | /health_check: 84 | get: 85 | summary: Reports whether the app is healthy or not. Checks availability of required services (e.g. db) 86 | responses: 87 | '200': 88 | description: Healthy 89 | content: 90 | text/plain: 91 | type: string 92 | enum: ["OK"] 93 | '503': 94 | description: Unhealthy 95 | content: 96 | text/plain: 97 | type: string 98 | enum: ["Service Unavailable"] 99 | components: 100 | schemas: 101 | Task: 102 | type: object 103 | properties: 104 | title: 105 | type: string 106 | description: Task name, cannot be empty 107 | required: 108 | - title 109 | TaskFull: 110 | type: object 111 | properties: 112 | _id: 113 | type: string 114 | description: Task identifier. Auto-assigned on object creation 115 | title: 116 | $ref: "#/components/schemas/Task/properties/title" 117 | required: 118 | - _id 119 | - title 120 | UserInfo: 121 | type: object 122 | properties: 123 | id: 124 | type: string 125 | description: User identifier assigned by MindSphere 126 | name: 127 | type: string 128 | description: User name 129 | email: 130 | type: string 131 | description: User email 132 | required: 133 | - id 134 | - name 135 | - email 136 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{title}}

4 |

{{slogan}}

5 |
6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | import { CookieService } from 'ngx-cookie-service'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import { TodosComponent } from './todos/todos.component'; 10 | import { UserInfoComponent } from './userinfo/userinfo.component'; 11 | import { TodoServiceMock } from './todo.service.mock'; 12 | import { TodoService } from './todo.service'; 13 | import { MindSphereService } from './mindsphere.service'; 14 | import { MindSphereServiceMock } from './mindsphere.service.mock'; 15 | 16 | describe('AppComponent', () => { 17 | beforeEach(async(() => { 18 | (window)._mdsp = { init: () => {} }; 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | BrowserModule, 22 | FormsModule, 23 | HttpClientModule 24 | ], 25 | declarations: [ 26 | AppComponent, 27 | TodosComponent, 28 | UserInfoComponent 29 | ], 30 | providers: [ 31 | { provide: TodoService, useClass: TodoServiceMock }, 32 | { provide: MindSphereService, useClass: MindSphereServiceMock }, 33 | CookieService 34 | ], 35 | }).compileComponents(); 36 | })); 37 | 38 | it('should create the app', async(() => { 39 | const fixture = TestBed.createComponent(AppComponent); 40 | const app = fixture.debugElement.componentInstance; 41 | expect(app).toBeTruthy(); 42 | })); 43 | 44 | it(`should have as title 'todo'`, async(() => { 45 | const fixture = TestBed.createComponent(AppComponent); 46 | const app = fixture.debugElement.componentInstance; 47 | expect(app.title).toEqual('TODO'); 48 | })); 49 | 50 | it('should render title in a h1 tag', async(() => { 51 | const fixture = TestBed.createComponent(AppComponent); 52 | fixture.detectChanges(); 53 | const compiled = fixture.debugElement.nativeElement; 54 | expect(compiled.querySelector('h1').textContent).toContain('TODO'); 55 | })); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Component } from '@angular/core'; 7 | declare let _mdsp: any; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html' 12 | }) 13 | export class AppComponent { 14 | title = 'TODO'; 15 | slogan = 'a minimal todo list'; 16 | 17 | constructor() { 18 | _mdsp.init({ 19 | title: 'DevOps Demo', 20 | appId: '_mdspcontent', 21 | initialize: true, 22 | appInfoPath: '/assets/mdsp-app-info.json' 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { NgModule } from '@angular/core'; 7 | import { BrowserModule } from '@angular/platform-browser'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 10 | import { CookieService } from 'ngx-cookie-service'; 11 | 12 | import { AppComponent } from './app.component'; 13 | import { TodosComponent } from './todos/todos.component'; 14 | import { UserInfoComponent } from './userinfo/userinfo.component'; 15 | import { AuthInterceptor } from './interceptors/auth.interceptor'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | BrowserModule, 20 | FormsModule, 21 | HttpClientModule 22 | ], 23 | declarations: [ 24 | AppComponent, 25 | TodosComponent, 26 | UserInfoComponent 27 | ], 28 | providers: [ 29 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 30 | CookieService 31 | ], 32 | bootstrap: [AppComponent] 33 | }) 34 | export class AppModule { } 35 | -------------------------------------------------------------------------------- /src/app/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Inject, Injectable } from '@angular/core'; 7 | import { 8 | HttpEvent, 9 | HttpInterceptor, 10 | HttpHandler, 11 | HttpRequest, HttpErrorResponse 12 | } from '@angular/common/http'; 13 | 14 | import { EMPTY, Observable, throwError } from 'rxjs'; 15 | import { catchError } from 'rxjs/operators'; 16 | import { DOCUMENT } from '@angular/common'; 17 | 18 | @Injectable() 19 | export class AuthInterceptor implements HttpInterceptor { 20 | constructor (@Inject(DOCUMENT) private document: any) {} 21 | 22 | intercept (req: HttpRequest, next: HttpHandler): Observable> { 23 | return next.handle(req).pipe( 24 | catchError((err, caught) => { 25 | // If we detect a 401 Unauthorized on any request (including xhr), 26 | // redirect to the login page managed by the MindSphere Gateway 27 | // This can also happen on failed CORS requests, and Angular sees 28 | // status code 0 29 | // 30 | // Also remove the xsrf-token cookie before redirecting, or the 31 | // preflight OPTIONS request triggered by the browser via CORS will fail 32 | // at mdsp due to the extra added x-xsrf-token header added by angular 33 | if (err instanceof HttpErrorResponse && [0, 401].includes(err.status)) { 34 | console.log('Detected potential CORS or authentication issue'); 35 | console.log('Invalidating XSRF-TOKEN cookie and redirecting to /login'); 36 | 37 | this.document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; 38 | this.document.location.href = '/login'; 39 | return EMPTY; 40 | } 41 | return throwError(err); 42 | }) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/mindsphere.service.mock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { Observable , of} from 'rxjs'; 8 | import { TenantInfo } from './tenantinfo'; 9 | 10 | @Injectable() 11 | export class MindSphereServiceMock { 12 | 13 | getTenantInfo(): Observable { 14 | const TENANT_INFO: TenantInfo = { 15 | prefix: 'mytenant', 16 | name: 'mytenant', 17 | displayName: 'mytenant', 18 | type: 'DEVELOPER', 19 | companyName: 'My Company', 20 | allowedToCreateSubtenant: true, 21 | ETag: 1 22 | }; 23 | return of(TENANT_INFO); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/mindsphere.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 8 | import { CookieService } from 'ngx-cookie-service'; 9 | import { Observable } from 'rxjs'; 10 | import { TenantInfo } from './tenantinfo'; 11 | import { environment } from '../environments/environment'; 12 | import { tap } from 'rxjs/operators'; 13 | 14 | @Injectable({ providedIn: 'root' }) 15 | export class MindSphereService { 16 | 17 | static readonly TENANTMGMT_TENANTINFO_URL = './api/tenantmanagement/v4/tenantInfo'; 18 | mdspXsrfTokenHeader: string; 19 | 20 | constructor(private http: HttpClient, private cookieService: CookieService) { 21 | this.mdspXsrfTokenHeader = this.cookieService.get('XSRF-TOKEN'); 22 | 23 | if (! environment.production) { 24 | console.log('MindSphere dev mode, setting custom xsrf header and session cookie'); 25 | 26 | this.mdspXsrfTokenHeader = environment.mdsp.xsrfTokenHeader; 27 | this.cookieService.set('SESSION', environment.mdsp.sessionCookie); 28 | } 29 | } 30 | 31 | getTenantInfo(): Observable { 32 | return this.http.get( 33 | MindSphereService.TENANTMGMT_TENANTINFO_URL, 34 | { 35 | headers: new HttpHeaders({ 36 | 'x-xsrf-token': this.mdspXsrfTokenHeader, 37 | 'accept': 'application/json', 38 | 'content-type': 'application/json' 39 | }) 40 | }) 41 | .pipe(tap(response => { 42 | console.log('tenantInfo:', response); 43 | })); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/tenantinfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | type TenantType = 7 | | 'DEVELOPER' 8 | | 'USER' 9 | | 'OPERATOR'; 10 | 11 | export class TenantInfo { 12 | ETag: any; 13 | allowedToCreateSubtenant: boolean; 14 | companyName: string; 15 | country?: string; 16 | displayName: string; 17 | name: string; 18 | prefix: string; 19 | type: TenantType; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/todo.service.mock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { Observable , of} from 'rxjs'; 8 | import { Todo } from './todo'; 9 | import { UserInfo } from './userinfo'; 10 | 11 | @Injectable() 12 | export class TodoServiceMock { 13 | 14 | getTodos (): Observable { 15 | const TODOS: Todo[] = [ 16 | { _id: '5b839ff6601f82e43f24da01' , title: 'code' }, 17 | { _id: '5b839ff7601f82e43f24da02' , title: 'build' }, 18 | { _id: '5b839ff9601f82e43f24da03' , title: 'test' } 19 | ]; 20 | return of(TODOS); 21 | } 22 | 23 | addTodo (todo: Todo): Observable { 24 | return of('OK'); 25 | } 26 | 27 | deleteTodo (todo: Todo): Observable { 28 | return of('OK'); 29 | } 30 | 31 | getMe (): Observable { 32 | const ME: UserInfo = { 33 | id: '0000-1111', 34 | name: 'John Doe', 35 | email: 'john.doe@example.com' 36 | }; 37 | return of(ME); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/todo.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { Todo } from './todo'; 10 | import { UserInfo } from './userinfo'; 11 | 12 | @Injectable({ providedIn: 'root' }) 13 | export class TodoService { 14 | 15 | constructor( 16 | private http: HttpClient) { } 17 | 18 | getTodos (): Observable { 19 | return this.http.get('./v1/todo'); 20 | } 21 | 22 | addTodo (todo: Todo): Observable { 23 | return this.http.post('./v1/todo', todo, { 24 | responseType: 'text', 25 | headers: new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) 26 | }); 27 | } 28 | 29 | deleteTodo (todo: Todo): Observable { 30 | const _id = typeof todo === 'string' ? todo : todo._id; 31 | const url = `./v1/todo/${_id}`; 32 | return this.http.delete(url, { 33 | responseType: 'text' 34 | }); 35 | } 36 | 37 | getMe (): Observable { 38 | return this.http.get('./v1/me'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/todo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export class Todo { 7 | _id?: string; 8 | title: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |

{{todo.title}}

12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 7 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 8 | import { CookieService } from 'ngx-cookie-service'; 9 | 10 | import { TodosComponent } from './todos.component'; 11 | import { TodoServiceMock } from '../todo.service.mock'; 12 | import { TodoService } from '../todo.service'; 13 | 14 | describe('TodosComponent', () => { 15 | let component: TodosComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach((() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ TodosComponent ], 21 | providers: [ TodoService, 22 | { provide: TodoService, useClass: TodoServiceMock }, 23 | CookieService 24 | ], 25 | schemas: [NO_ERRORS_SCHEMA] 26 | }) 27 | .compileComponents(); 28 | })); 29 | 30 | beforeEach(() => { 31 | fixture = TestBed.createComponent(TodosComponent); 32 | component = fixture.componentInstance; 33 | fixture.detectChanges(); 34 | }); 35 | 36 | it(`should have todo elements from mock`, async(() => { 37 | fixture = TestBed.createComponent(TodosComponent); 38 | fixture.detectChanges(); 39 | const compiled = fixture.debugElement.nativeElement; 40 | expect(compiled.querySelectorAll('h1')[0].textContent).toContain('code'); 41 | expect(compiled.querySelectorAll('h1')[1].textContent).toContain('build'); 42 | expect(compiled.querySelectorAll('h1')[2].textContent).toContain('test'); 43 | })); 44 | 45 | it('should create', () => { 46 | expect(component).toBeTruthy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Component, OnInit } from '@angular/core'; 7 | 8 | import { Todo } from '../todo'; 9 | import { TodoService } from '../todo.service'; 10 | 11 | @Component({ 12 | selector: 'app-todos', 13 | templateUrl: './todos.component.html' 14 | }) 15 | export class TodosComponent implements OnInit { 16 | todos: Todo[]; 17 | 18 | constructor(private todoService: TodoService) { } 19 | 20 | ngOnInit() { 21 | this.get(); 22 | } 23 | 24 | get(): void { 25 | this.todoService.getTodos() 26 | .subscribe(todos => this.todos = todos); 27 | } 28 | 29 | add(title: string): void { 30 | title = title.trim(); 31 | if (!title) { return; } 32 | this.todoService.addTodo({title}).subscribe(() => this.get()); 33 | } 34 | 35 | delete(todo: Todo): void { 36 | this.todos = this.todos.filter(h => h !== todo); 37 | this.todoService.deleteTodo(todo).subscribe(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/userinfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | export class UserInfo { 7 | id: string; 8 | name: string; 9 | email: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/userinfo/userinfo.component.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/app/userinfo/userinfo.component.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | position: fixed; 3 | width: 100%; 4 | bottom: 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/userinfo/userinfo.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 7 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 8 | import { CookieService } from 'ngx-cookie-service'; 9 | 10 | import { UserInfoComponent } from './userinfo.component'; 11 | import { TodoServiceMock } from '../todo.service.mock'; 12 | import { TodoService } from '../todo.service'; 13 | import { MindSphereService } from '../mindsphere.service'; 14 | import { MindSphereServiceMock } from '../mindsphere.service.mock'; 15 | 16 | describe('UserInfoComponent', () => { 17 | let component: UserInfoComponent; 18 | let fixture: ComponentFixture; 19 | 20 | beforeEach((() => { 21 | TestBed.configureTestingModule({ 22 | declarations: [ UserInfoComponent ], 23 | providers: [ TodoService, 24 | { provide: TodoService, useClass: TodoServiceMock }, 25 | { provide: MindSphereService, useClass: MindSphereServiceMock }, 26 | CookieService 27 | ], 28 | schemas: [NO_ERRORS_SCHEMA] 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(UserInfoComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it(`should have userinfo element from mock`, async(() => { 40 | fixture = TestBed.createComponent(UserInfoComponent); 41 | fixture.detectChanges(); 42 | const compiled = fixture.debugElement.nativeElement; 43 | expect(compiled.querySelectorAll('div')[0].textContent).toContain('john.doe@example.com'); 44 | })); 45 | 46 | it(`should have tenantinfo element from mock`, async(() => { 47 | fixture = TestBed.createComponent(UserInfoComponent); 48 | fixture.detectChanges(); 49 | const compiled = fixture.debugElement.nativeElement; 50 | expect(compiled.querySelectorAll('div')[1].textContent).toContain('mytenant'); 51 | })); 52 | 53 | it('should create', () => { 54 | expect(component).toBeTruthy(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/userinfo/userinfo.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | import { Component, OnInit } from '@angular/core'; 7 | 8 | import { UserInfo } from '../userinfo'; 9 | import { TodoService } from '../todo.service'; 10 | import { MindSphereService } from '../mindsphere.service'; 11 | import { TenantInfo } from '../tenantinfo'; 12 | 13 | @Component({ 14 | selector: 'app-userinfo', 15 | templateUrl: './userinfo.component.html', 16 | styleUrls: ['./userinfo.component.scss'] 17 | }) 18 | export class UserInfoComponent implements OnInit { 19 | userInfo: UserInfo; 20 | tenantInfo: TenantInfo; 21 | 22 | constructor( 23 | private todoService: TodoService, 24 | private mindSphereService: MindSphereService) {} 25 | 26 | ngOnInit() { 27 | this.get(); 28 | } 29 | 30 | get(): void { 31 | this.todoService.getMe() 32 | .subscribe(userInfo => this.userInfo = userInfo); 33 | this.mindSphereService.getTenantInfo() 34 | .subscribe(tenantInfo => this.tenantInfo = tenantInfo); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindsphere/devops-demo/97a23c8814ae21fdb4b3d26d080778d531b9898f/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/mdsp-app-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "MindSphere DevOps Demo", 3 | "appVersion": "0.2.0", 4 | "appCopyright": "© Siemens AG 2018", 5 | "links": { 6 | "default": [ 7 | { 8 | "type": "www", 9 | "name": "Todo - Source Code", 10 | "value": "https://gitlab.com/mindsphere/devops-demo" 11 | } 12 | ], 13 | "de": [ 14 | { 15 | "type": "www", 16 | "name": "Todo - Quell Code", 17 | "value": "https://gitlab.com/mindsphere/devops-demo" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | IE 9-11 10 | -------------------------------------------------------------------------------- /src/environments/.gitignore: -------------------------------------------------------------------------------- 1 | /.environment* 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | mdsp: { 4 | xsrfTokenHeader: null, 5 | sessionCookie: null 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | mdsp: { 8 | xsrfTokenHeader: null, 9 | sessionCookie: null 10 | } 11 | }; 12 | 13 | // In development mode, for easier debugging, you can ignore zone related error 14 | // stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the 15 | // below file. Don't forget to comment it out in production mode 16 | // because it will have a performance impact when errors are thrown 17 | 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | todo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Siemens AG 2018 3 | SPDX-License-Identifier: MIT 4 | */ 5 | 6 | // Karma configuration file, see link for more information 7 | // https://karma-runner.github.io/1.0/config/configuration-file.html 8 | const grid = require('url').parse(process.env.SELENIUM_URL || ''); 9 | 10 | module.exports = function (config) { 11 | config.set({ 12 | basePath: '', 13 | hostname: process.env.SELENIUM_URL ? require('ip').address() : 'localhost', 14 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 15 | plugins: [ 16 | require('karma-jasmine'), 17 | require('karma-chrome-launcher'), 18 | require('karma-webdriver-launcher'), 19 | require('karma-jasmine-html-reporter'), 20 | require('karma-coverage-istanbul-reporter'), 21 | require('karma-junit-reporter'), 22 | require('@angular-devkit/build-angular/plugins/karma') 23 | ], 24 | client: { 25 | clearContext: false // leave Jasmine Spec Runner output visible in browser 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, '../coverage'), 29 | reports: ['text-summary', 'html', 'lcovonly'], 30 | fixWebpackSourcePaths: true 31 | }, 32 | junitReporter: { 33 | outputDir: require('path').join(__dirname, '../coverage'), // results will be saved as $outputDir/$browserName.xml 34 | outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile 35 | suite: '', // suite will become the package name attribute in xml testsuite element 36 | useBrowserName: true, // add browser name to report and classes names 37 | nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element 38 | classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element 39 | properties: {}, // key value pair of properties to add to the section of the report 40 | xmlVersion: null // use '1' if reporting to be per SonarQube 6.2 XML format 41 | }, 42 | customLaunchers: { 43 | 'Chrome-Webdriver': { 44 | base: 'WebDriver', 45 | config: { 46 | hostname: grid.hostname, 47 | port: grid.port 48 | }, 49 | browserName: 'chrome' 50 | } 51 | }, 52 | angularCli: { 53 | environment: 'dev' 54 | }, 55 | reporters: ['progress', 'kjhtml', 'junit'], 56 | port: 9876, 57 | colors: true, 58 | logLevel: config.LOG_INFO, 59 | autoWatch: !process.env.SELENIUM_URL, 60 | browsers: [process.env.SELENIUM_URL ? 'Chrome-Webdriver' : 'Chrome'], 61 | singleRun: process.env.SELENIUM_URL, 62 | browserNoActivityTimeout: 120000 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // 2 | // This file includes polyfills needed by Angular and is loaded before the app. 3 | // You can add your own extra polyfills to this file. 4 | // 5 | // This file is divided into 2 sections: 6 | // 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | // 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | // file. 9 | // 10 | // The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | // automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | // Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | // 14 | // Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | // 16 | 17 | // BROWSER POLYFILLS 18 | 19 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 20 | import 'core-js/es6/symbol'; 21 | import 'core-js/es6/object'; 22 | import 'core-js/es6/function'; 23 | import 'core-js/es6/parse-int'; 24 | import 'core-js/es6/parse-float'; 25 | import 'core-js/es6/number'; 26 | import 'core-js/es6/math'; 27 | import 'core-js/es6/string'; 28 | import 'core-js/es6/date'; 29 | import 'core-js/es6/array'; 30 | import 'core-js/es6/regexp'; 31 | import 'core-js/es6/map'; 32 | import 'core-js/es6/weak-map'; 33 | import 'core-js/es6/set'; 34 | 35 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 36 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 37 | 38 | /** IE10 and IE11 requires the following for the Reflect API. */ 39 | // import 'core-js/es6/reflect'; 40 | 41 | 42 | /** Evergreen browsers require these. **/ 43 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 44 | import 'core-js/es7/reflect'; 45 | 46 | 47 | // Web Animations `@angular/platform-browser/animations` 48 | // Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 49 | // Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 50 | // 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | // By default, zone.js will patch all possible macroTask and DomEvents 54 | // user can disable parts of macroTask/DomEvents patch by setting following flags 55 | 56 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 57 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 58 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 59 | 60 | // in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 61 | // with the following flag, it will bypass `zone.js` patch for IE/Edge 62 | // 63 | // (window as any).__Zone_enable_cross_context_check = true; 64 | 65 | // Zone JS is required by default for Angular itself. 66 | import 'zone.js/dist/zone'; // Included with Angular CLI. 67 | 68 | // APPLICATION IMPORTS 69 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # cf-ssh.sh 2 | 3 | Automates ssh connectivity to MindSphere applications running in CloudFoundry. 4 | 5 | When running MindSphere applications, 6 | [ssh access requires several steps](https://developer.mindsphere.io/paas/paas-cloudfoundry-ssh.html). 7 | To aid in this task, the script provides the following functionality: 8 | 9 | * Automates the generation of the required one-time password 10 | * Tunnels appropriately ssh connections when proxy settings are detected 11 | 12 | Tested in Debian Linux 9.5 and macOS 10.13+ 13 | 14 | ## Prerequisites 15 | 16 | 1. Assumes that access to the appropriate CloudFoundry deployment has been 17 | correctly setup in the environment, particularly 'cf target' and 18 | 'cf apps' work properly 19 | 20 | 1. The following shell commands are available in the environment: 21 | * openssh client 22 | * *(if using HTTP_PROXY)* proxytunnel 23 | * jq 24 | * sshpass 25 | If you are on macOS, please check: https://gist.github.com/arunoda/7790979#installing-on-os-x 26 | * *(only on macOS)* gnu-sed 27 | 28 | ## Recipes 29 | 30 | ### How to ssh to a running application 31 | 32 | ```sh 33 | cf-ssh.sh -v -a my-app 34 | ``` 35 | 36 | ### How to open a local tunnel to a remote application port 37 | 38 | The following command tunnels local port `8080` to remote port `80`: 39 | 40 | ```sh 41 | cf-ssh.sh -v -a my-app -- -L8080:localhost:80 42 | ``` 43 | 44 | Multiple ports are also possible, the options after `--` are just standard 45 | OpenSSH options: 46 | 47 | ```sh 48 | cf-ssh.sh -v -a my-app -- -L8080:localhost:80 -L8081:localhost:8081 49 | ``` 50 | 51 | ### How to open a local tunnel to a remote db service 52 | 53 | MindSphere does not allow direct ssh access to services, so an intermediate 54 | reachable application that is bound to the service is needed. 55 | 56 | Steps: 57 | 58 | 1. Obtain the db connection details by checking the environment variable 59 | `VCAP_SERVICES` of the application bound to the service (use `cf env `) 60 | 1. Extract from this data: db url, db port, user, password 61 | 1. Create an ssh tunnel, forwarding a local port to the remote one: 62 | 63 | ```sh 64 | cf-ssh.sh -v -a my-app -- -L8888:: 65 | ``` 66 | 67 | 1. Now in a different terminal, using your preferred db browser, connect to 68 | the db using the local tunnel at `localhost:8888`, providing the user and 69 | password you extracted earlier 70 | -------------------------------------------------------------------------------- /tools/cf-ssh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright Siemens AG 2018 4 | # SPDX-License-Identifier: MIT 5 | # 6 | # Easy ssh to MindSphere CloudFoundry apps and services 7 | # 8 | # https://google.github.io/styleguide/shell.xml 9 | # 10 | 11 | set -e 12 | 13 | VERBOSE="false" 14 | function .log () { 15 | if [[ ${VERBOSE} == "true" ]]; then 16 | echo -e "[info] $@" 17 | fi 18 | } 19 | 20 | function .err () { 21 | echo -e "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 22 | } 23 | 24 | function show_help() { 25 | cat << EOF 26 | Usage: ${0##*/} -a APP_NAME [-i APP_INDEX] -- [EXTRA_SSH_PARAMS...] 27 | 28 | Open an ssh connection to a CloudFoundry application running in MindSphere, 29 | with an optional local port forwarding. It will also respect the settings of 30 | the 'HTTP_PROXY' env var to forward the connection. 31 | 32 | -a APP_NAME the application to connect to, as returned by 'cf apps' 33 | -i APP_INDEX optional index of the application instance, in case of 34 | multiple replicas 35 | (default: 0) 36 | EXTRA_SSH_PARAMS optional extra standard openssh parameters, they will be 37 | provided as-is to the ssh command 38 | (example: -L8080:localhost:80) 39 | 40 | -v verbose output 41 | -h display this help and exit 42 | 43 | This assumes that access to the appropriate CloudFoundry deployment has been 44 | correctly setup in the environment, particularly: 45 | 46 | * 'cf target' and 'cf apps' work properly 47 | 48 | The following shell tools MUST be installed: 49 | * proxytunnel (if using HTTP_PROXY) 50 | * jq 51 | * sshpass 52 | 53 | Example invocation: 54 | $ ./cf-ssh.sh -v -a my-app -- -L8080:localhost:8080 -L8081:localhost:8081 55 | EOF 56 | } 57 | 58 | function main () { 59 | # Default argument values 60 | local arg_app_name="" 61 | local arg_app_index="0" 62 | 63 | local sed_cmd="sed" 64 | 65 | 66 | # Expects 'proxytunnel' binary in PATH if HTTP_PROXY is set 67 | if [[ -n "${HTTP_PROXY}" ]]; then 68 | [[ $(type -P proxytunnel) ]] || { .err "HTTP_PROXY set and 'proxytunnel' not found in PATH\nPlease install 'proxytunnel'"; exit 1; } 69 | fi 70 | 71 | # Check required command line tools 72 | [[ $(type -P jq) ]] || { .err "'jq' not found in PATH\nPlease install 'jq'"; exit 1; } 73 | [[ $(type -P sshpass) ]] || { .err "'sshpass' not found in PATH\nPlease install 'sshpass'\nIf you are on macOS, please check: https://gist.github.com/arunoda/7790979#installing-on-os-x"; exit 1; } 74 | 75 | # Expect gsed in case we are in macOS 76 | if [[ $OSTYPE == darwin* ]]; then 77 | [[ $(type -P gsed) ]] || { .err "Running macOS and 'gsed' not found in PATH\nPlease install 'gsed', e.g. 'brew install gnu-sed'"; exit 1; } 78 | sed_cmd="gsed" 79 | fi 80 | 81 | # Resetting OPTIND is necessary if getopts was used previously in the script. 82 | # It is a good idea to make OPTIND local if you process options in a function. 83 | OPTIND=1 84 | 85 | while getopts "a:i:hvd" opt; do 86 | case "${opt}" in 87 | h) 88 | show_help 89 | exit 0 90 | ;; 91 | v) 92 | VERBOSE="true" 93 | ;; 94 | a) 95 | arg_app_name="${OPTARG}" 96 | ;; 97 | i) 98 | arg_app_index="${OPTARG}" 99 | ;; 100 | esac 101 | done 102 | 103 | shift "$((OPTIND-1))" # Shift off the options and optional --. 104 | 105 | # If there are input files (for example) that follow the options, they 106 | # will remain in the "$@" positional parameters. 107 | local arg_extra_ssh_params=$@ 108 | 109 | .log "detected params: APP_NAME: '${arg_app_name}', APP_INDEX: '${arg_app_index}', EXTRA_SSH_PARAMS: '${arg_extra_ssh_params}'" 110 | 111 | if [[ -z "${arg_app_name}" ]]; then 112 | .err "missing parameter: APP_NAME: ${arg_app_name}" 113 | exit 1 114 | fi 115 | 116 | if [[ -z "${arg_app_index}" ]]; then 117 | .err "missing parameter: APP_INDEX: '${arg_app_index}" 118 | exit 1 119 | fi 120 | 121 | # Obtain GUID of APP from CF 122 | readonly cf_app_guid="$(cf app ${arg_app_name} --guid)" 123 | if [[ -z "${cf_app_guid}" ]]; then 124 | .err "could not find GUID for app ${arg_app_name}" 125 | exit 1 126 | fi 127 | .log "queried GUID of ${arg_app_name}: ${cf_app_guid}" 128 | 129 | # Parse the SSH endpoint into url and port 130 | local cf_ssh_endpoint="$(cf curl /v2/info | jq -r '.app_ssh_endpoint')" 131 | .log "queried CF SSH endpoint: ${cf_ssh_endpoint}" 132 | 133 | readonly url_sed_expr="s/\([^/]*\/\/\)\?\([^@]*@\)\?\([^:/]*\)\(:\([0-9]\{1,5\}\)\)\?.*/" 134 | readonly cf_ssh_endpoint_url="$(echo "${cf_ssh_endpoint}" | ${sed_cmd} -e "${url_sed_expr}\3/")" 135 | local cf_ssh_endpoint_port="$(echo "${cf_ssh_endpoint}" | ${sed_cmd} -e "${url_sed_expr}\5/")" 136 | 137 | if [[ -z "${cf_ssh_endpoint_port}" ]]; then 138 | cf_ssh_endpoint_port="22" 139 | fi 140 | .log "CF SSH endpoint url: ${cf_ssh_endpoint_url}, port: ${cf_ssh_endpoint_port}" 141 | 142 | # Obtain One-Time Password from CF 143 | readonly cf_ssh_otp="$(cf ssh-code)" 144 | if [[ -z "${cf_ssh_otp}" ]]; then 145 | .err "could not obtain one-time password" 146 | exit 1 147 | else 148 | .log "obtained one-time password" 149 | fi 150 | 151 | # Build SSH user as 'cf::' 152 | readonly cf_ssh_user="cf:${cf_app_guid}/${arg_app_index}" 153 | 154 | # No hostkey checking to support prompt-less sshpass 155 | readonly cf_ssh_no_host_check_opt="StrictHostKeyChecking=no" 156 | 157 | # Setup ProxyCommand in case proxy env var is set 158 | local cf_ssh_proxy_command 159 | if [[ ${HTTP_PROXY} ]]; then 160 | local http_proxy_stripped=${HTTP_PROXY#http://} 161 | cf_ssh_proxy_command="ProxyCommand=proxytunnel -q -p ${http_proxy_stripped} -d %h:%p" 162 | .log "detected http proxy, using ssh option: ${cf_ssh_proxy_command}" 163 | fi 164 | 165 | .log "ssh command user: ${cf_ssh_user}" 166 | .log "ssh command endpoint url: ${cf_ssh_endpoint_url}" 167 | .log "ssh command endpoint port: ${cf_ssh_endpoint_port}" 168 | 169 | # Use different ssh commands in case we are using a proxy command 170 | if [[ -z ${cf_ssh_proxy_command} ]]; then 171 | sshpass -p "${cf_ssh_otp}" ssh -4 -p "${cf_ssh_endpoint_port}" -o "${cf_ssh_no_host_check_opt}" $arg_extra_ssh_params "${cf_ssh_user}"@"${cf_ssh_endpoint_url}" 172 | else 173 | sshpass -p "${cf_ssh_otp}" ssh -4 -p "${cf_ssh_endpoint_port}" -o "${cf_ssh_proxy_command}" -o "${cf_ssh_no_host_check_opt}" $arg_extra_ssh_params "${cf_ssh_user}"@"${cf_ssh_endpoint_url}" 174 | fi 175 | 176 | exit 0 177 | } 178 | 179 | main "$@" 180 | 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------