diff --git a/.gitignore b/.gitignore index b34c80c94b..fd305dfd93 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ src/jetstream/console-database.db src/jetstream/config.properties src/jetstream/db/dbconf.yml src/jetstream/plugins/monocular/chart-repo/chartrepo +src/jetstream/plugins/analysis/container/analyzers # Customisations - these can be removed in the future # Left in for now to prevent these files being checked-in, if they are still present diff --git a/deploy/ci/suse-console-dev-releases.yml b/deploy/ci/suse-console-dev-releases.yml index ef2247a720..be33e148f5 100644 --- a/deploy/ci/suse-console-dev-releases.yml +++ b/deploy/ci/suse-console-dev-releases.yml @@ -66,12 +66,18 @@ resources: password: ((docker-password)) repository: ((docker-repository))/stratos-chartsync - name: kube-terminal-image - type: docker-image - source: - username: ((docker-username)) - password: ((docker-password)) - repository: ((docker-repository))/stratos-kube-terminal - + type: docker-image + source: + username: ((docker-username)) + password: ((docker-password)) + repository: ((docker-repository))/stratos-kube-terminal +- name: analyzers-image + type: docker-image + source: + username: ((docker-username)) + password: ((docker-password)) + repository: ((docker-repository))/stratos-analyzers + # Artifacts - name: image-tag type: s3 @@ -199,6 +205,14 @@ jobs: tag: image-tag/v2-alpha-tag patch_base_reg: ((patch-base-reg)) patch_base_tag: ((patch-base-tag)) + - put: analyzers-image + params: + dockerfile: stratos/src/jetstream/plugins/analysis/container/Dockerfile + build: stratos/src/jetstream/plugins/analysis/container/ + tag: image-tag/v2-alpha-tag + patch_base_reg: ((patch-base-reg)) + patch_base_tag: ((patch-base-tag)) + - name: create-chart plan: - get: stratos diff --git a/deploy/kubernetes/console/templates/analyzers.yaml b/deploy/kubernetes/console/templates/analyzers.yaml new file mode 100644 index 0000000000..ca4d5a0d6d --- /dev/null +++ b/deploy/kubernetes/console/templates/analyzers.yaml @@ -0,0 +1,62 @@ +--- +{{- if semverCompare ">=1.16" (printf "%s.%s" .Capabilities.KubeVersion.Major (trimSuffix "+" .Capabilities.KubeVersion.Minor) )}} +apiVersion: apps/v1 +{{- else }} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Deployment +metadata: + name: stratos-analyzers + labels: + app.kubernetes.io/name: "stratos" + app.kubernetes.io/instance: "{{ .Release.Name }}" + app.kubernetes.io/version: "{{ .Chart.AppVersion }}" + app.kubernetes.io/component: "stratos-analyzers" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + selector: + matchLabels: + app.kubernetes.io/name: "stratos" + app.kubernetes.io/component: "stratos-analyzers" + template: + metadata: + labels: + app.kubernetes.io/name: "stratos" + app.kubernetes.io/instance: "{{ .Release.Name }}" + app.kubernetes.io/version: "{{ .Chart.AppVersion }}" + app.kubernetes.io/component: "stratos-analyzers" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" + app: "{{ .Release.Name }}" + spec: + containers: + - name: analyzers + image: {{.Values.kube.registry.hostname}}/{{.Values.kube.organization}}/stratos-analyzers:{{.Values.consoleVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + ports: + - name: api + containerPort: 8090 + env: + - name: ANALYSIS_SCRIPTS_DIR + value: "/scripts" + - name: ANALYSIS_REPORTS_DIR + value: "/reports" +--- +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Release.Name }}-analyzers" + labels: + app.kubernetes.io/name: "stratos" + app.kubernetes.io/instance: "{{ .Release.Name }}" + app.kubernetes.io/version: "{{ .Chart.AppVersion }}" + app.kubernetes.io/component: "stratos-analyzers-service" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" +spec: + type: ClusterIP + ports: + - name: analyzers + port: 8090 + targetPort: 8090 + selector: + app: "{{ .Release.Name }}" + app.kubernetes.io/component: "stratos-analyzers" diff --git a/deploy/kubernetes/console/values.yaml b/deploy/kubernetes/console/values.yaml index 2335e6ccbb..5dbf69f47d 100644 --- a/deploy/kubernetes/console/values.yaml +++ b/deploy/kubernetes/console/values.yaml @@ -57,6 +57,7 @@ console: servicePort: 80 # nodePort: 30001 + # Name of config map that provides the template files for user invitation emails templatesConfigMapName: @@ -120,6 +121,7 @@ images: fdbserver: stratos-fdbserver fdbdoclayer: stratos-fdbdoclayer chartsync: stratos-chartsync + analyzers: stratos-analyzers # Specify which storage class should be used for PVCs #storageClass: default diff --git a/deploy/kubernetes/custom/__stratos.tpl b/deploy/kubernetes/custom/__stratos.tpl index 732932914d..d5eb091928 100644 --- a/deploy/kubernetes/custom/__stratos.tpl +++ b/deploy/kubernetes/custom/__stratos.tpl @@ -12,6 +12,8 @@ value: "mongodb://{{ .Release.Name }}-fdbdoclayer:27016" - name: SYNC_SERVER_URL value: "http://{{ .Release.Name }}-chartsync:8080" +- name: ANALYSIS_SERVICES_API + value: "http://{{ .Release.Name }}-analyzers:8090" - name: STRATOS_KUBERNETES_NAMESPACE value: "{{ .Release.Namespace }}" - name: STRATOS_KUBERNETES_TERMINAL_IMAGE diff --git a/deploy/kubernetes/custom/custom-build.sh b/deploy/kubernetes/custom/custom-build.sh index 35d6202755..b44f9fbd41 100644 --- a/deploy/kubernetes/custom/custom-build.sh +++ b/deploy/kubernetes/custom/custom-build.sh @@ -25,4 +25,9 @@ function custom_image_build() { # Build and push an image for the Kubernetes Terminal log "-- Building/publishing Kubernetes Terminal" patchAndPushImage stratos-kube-terminal Dockerfile.kubeterminal "${STRATOS_PATH}/deploy/containers/kube-terminal" + + # Analzyers container + log "-- Building/publishing Stratos Analyzers" + patchAndPushImage stratos-analyzers Dockerfile "${STRATOS_PATH}/src/jetstream/plugins/analysis/container" + } \ No newline at end of file diff --git a/docs/extensions.md b/docs/extensions.md index 918c25df18..64c182e6a0 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -174,7 +174,7 @@ First, create the custom-src folder structure - from the top-level of the Strato ``` mkdir -p custom-src/frontend/app/custom -mkdir -p custom-src/frontend/assets/custom +mkdir -p /frontend/assets/custom ``` Next, run the customize task: diff --git a/docs/suse/kube-terminal-dev.md b/docs/suse/suse-extensions-dev.md similarity index 60% rename from docs/suse/kube-terminal-dev.md rename to docs/suse/suse-extensions-dev.md index fe552b064c..9ac1e4e7b3 100644 --- a/docs/suse/kube-terminal-dev.md +++ b/docs/suse/suse-extensions-dev.md @@ -18,4 +18,24 @@ you will need to edit the `src/jetstream/config.properties` file and set these t The Jetstream backend should be configured. -> Note: Ensure you set `ENABLE_TECH_PREVIEW=true` to enable the Kubernetes Terminal feature. \ No newline at end of file +> Note: Ensure you set `ENABLE_TECH_PREVIEW=true` to enable the Kubernetes Terminal feature. + + +# Enabling Security Obvervability Analyzers in local development + +You need to build the docker image for the analyzers container. + +``` +cd src/jetstream/plugins/analysis/container +docker build . -t stratos-analyzers +``` + +Now run this container - this will provide the analysis engines to Stratos: + +`docker run -d -p 8090:8090 stratos-analyzers` + +Edit your Jetstream `config.properties` file and add the following lines: + +``` +ANALYSIS_SERVICES_API=http://127.0.0.1:8090 +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae80a769d8..8bbb6d91b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1610,9 +1610,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/helper-wrap-function": { @@ -1756,6 +1756,146 @@ } } }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", + "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", + "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", + "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@babel/highlight": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", @@ -2163,11 +2303,7 @@ "@babel/code-frame": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", - "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.1" - } + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==" }, "@babel/helper-function-name": { "version": "7.10.1", @@ -2181,58 +2317,110 @@ } }, "@babel/helper-get-function-arity": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz", - "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", - "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", - "dev": true + "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==" }, "@babel/highlight": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", - "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", - "dev": true, + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz", - "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", + "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", "dev": true }, "@babel/template": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", - "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.1", - "@babel/parser": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } } }, "@babel/types": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", - "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -2398,7 +2586,7 @@ "integrity": "sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==", "dev": true, "requires": { - "regenerator-transform": "^0.14.2" + "@babel/helper-plugin-utils": "^7.10.1" } }, "@babel/plugin-transform-spread": { @@ -2436,7 +2624,6 @@ "integrity": "sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", "@babel/helper-plugin-utils": "^7.10.1" } }, @@ -2642,6 +2829,14 @@ "@babel/helper-validator-identifier": "^7.9.0", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + } } }, "@cfstratos/ajsf-core": { @@ -2858,12 +3053,12 @@ } }, "@rollup/plugin-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.0.2.tgz", - "integrity": "sha512-t4zJMc98BdH42mBuzjhQA7dKh0t4vMJlUka6Fz0c+iO5IVnWaEMiYBy1uBj9ruHZzXBW23IPDGL9oCzBkQ9Udg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", "dev": true, "requires": { - "@rollup/pluginutils": "^3.0.4" + "@rollup/pluginutils": "^3.0.8" } }, "@rollup/plugin-node-resolve": { @@ -3445,12 +3640,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", "dev": true, - "optional": true - }, - "angular2-virtual-scroll": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/angular2-virtual-scroll/-/angular2-virtual-scroll-0.4.16.tgz", - "integrity": "sha512-6NWk0DjCh4ebU8+LgfBoKYyp3McxDA/k5vTnEiV32VpVnyhN//eThZpVpggI1D2fJBqgTAY09C8v++qXHHLP7A==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", @@ -3800,7 +3989,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -4078,6 +4266,16 @@ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "dev": true }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -4086,21 +4284,6 @@ "requires": { "inherits": "2.0.1" } - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true } } }, @@ -4763,6 +4946,25 @@ "safe-buffer": "^5.2.0" }, "dependencies": { + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -5090,16 +5292,6 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } } } }, @@ -5202,7 +5394,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5213,7 +5404,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5542,7 +5732,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -5550,8 +5739,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.3", @@ -7410,8 +7598,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.8.1", @@ -9315,8 +9502,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-glob": { "version": "1.0.0", @@ -10921,8 +11107,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts index b7af4a19ea..12db72f9ba 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/application-tabs-base.component.ts @@ -89,7 +89,7 @@ export class ApplicationTabsBaseComponent implements OnInit, OnDestroy { switchMap(space => this.currentUserPermissionsService.can(CfCurrentUserPermissions.APPLICATION_VIEW_ENV_VARS, this.applicationService.cfGuid, space.metadata.guid) ), - map(can => !can) + map(can => !can), ); this.tabLinks = [ diff --git a/src/frontend/packages/core/sass/components/text-status.theme.scss b/src/frontend/packages/core/sass/components/text-status.theme.scss index 68b3d1821b..a054375175 100644 --- a/src/frontend/packages/core/sass/components/text-status.theme.scss +++ b/src/frontend/packages/core/sass/components/text-status.theme.scss @@ -6,6 +6,7 @@ $status-warning: map-get($status-colors, warning); $status-danger: map-get($status-colors, danger); $status-tentative: map-get($status-colors, tentative); + $status-info: map-get($status-colors, info); .text-success { color: $status-success; @@ -23,6 +24,10 @@ color: $status-tentative; } + .text-info { + color: $status-info; + } + // Border colors .border-success { @@ -41,4 +46,8 @@ border-color: $status-tentative; } + .border-info { + border-color: $status-info; + } + } diff --git a/src/frontend/packages/core/sass/mat-desktop.scss b/src/frontend/packages/core/sass/mat-desktop.scss index 3538518e13..cf474347e0 100644 --- a/src/frontend/packages/core/sass/mat-desktop.scss +++ b/src/frontend/packages/core/sass/mat-desktop.scss @@ -17,6 +17,13 @@ $desktop-toggle-button-item-height: $desktop-menu-item-height - 2px; line-height: $desktop-menu-item-height; min-width: 128px; padding: 0 24px; + + &.hasIcon { + padding-left: 16px; + .mat-icon { + margin-right: 12px; + } + } } .mat-menu-panel { diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html index 2c4b59dc38..d7ca4b06f5 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.html @@ -16,10 +16,18 @@
-
+
-
- {{ activeTabLabel }} +
+ {{ data[0] }} +
+
+ + {{ breadcrumbDef.value }} + {{ breadcrumbDef.value }} + chevron_right +
diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss index 26a2d129ce..34b3de01a3 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.scss @@ -79,6 +79,11 @@ $app-header-height: 56px; } .page-header-sub-nav { + $breadcrumb-opacity: .7; + $breadcrumb-hover-opacity: 1; + $breadcrumb-padding: 3px; + $font-size: 20px; + align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .1); display: flex; @@ -91,7 +96,6 @@ $app-header-height: 56px; } &__title { display: none; - $font-size: 20px; font-size: $font-size; font-weight: bold; line-height: $font-size; @@ -106,4 +110,34 @@ $app-header-height: 56px; opacity: .6; width: 100%; } + &__breadcrumb { + font-size: $font-size; + font-weight: bold; + line-height: $font-size; + } + &__breadcrumb, + &__breadcrumb-separator { + opacity: $breadcrumb-opacity; + } + &__breadcrumb-separator { + font-size: 24px; + margin: 0 $breadcrumb-padding; + user-select: none; + } + &__breadcrumbs { + align-items: center; + display: flex; + justify-content: center; + } + &__breadcrumb-nolink { + opacity: $breadcrumb-hover-opacity; + } + &__breadcrumb-link { + cursor: pointer; + outline: none; + &:hover { + opacity: $breadcrumb-hover-opacity; + } + } + } diff --git a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts index 8310e6723e..42a8998120 100644 --- a/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts +++ b/src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts @@ -17,7 +17,9 @@ import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-ca import { TabNavService } from '../../../../tab-nav.service'; import { CustomizationService } from '../../../core/customizations.types'; import { EndpointsService } from '../../../core/endpoints.service'; +import { IHeaderBreadcrumbLink } from '../../../shared/components/page-header/page-header.types'; import { SidePanelService } from '../../../shared/services/side-panel.service'; +import { IPageSideNavTab } from '../page-side-nav/page-side-nav.component'; import { PageHeaderService } from './../../../core/page-header-service/page-header.service'; import { SideNavItem } from './../side-nav/side-nav.component'; @@ -30,7 +32,7 @@ import { SideNavItem } from './../side-nav/side-nav.component'; export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit { public activeTabLabel$: Observable; - public subNavData$: Observable<[string, Portal]>; + public subNavData$: Observable<[string, Portal, IPageSideNavTab, IHeaderBreadcrumbLink[]]>; public isMobile$: Observable; public sideNavMode$: Observable; public sideNavMode: string; @@ -133,8 +135,9 @@ export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit this.tabNavService.getCurrentTabHeaderObservable().pipe( startWith(null) ), - this.tabNavService.tabSubNav$ - ); + this.tabNavService.tabSubNav$, + this.tabNavService.tabSubNavBreadcrumbs$ + ).pipe(map(([tabNav, tabSubNav, tabSubNavBreadcrumb]) => [tabNav ? tabNav.label : null, tabSubNav, tabNav, tabSubNavBreadcrumb])); // Register all health checks for endpoint types that support this entityCatalog.getAllEndpointTypes().forEach(epType => { diff --git a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts index b74dbed831..bab4cd21dc 100644 --- a/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts +++ b/src/frontend/packages/core/src/features/dashboard/page-side-nav/page-side-nav.component.ts @@ -10,6 +10,7 @@ import { TabNavService } from '../../../../tab-nav.service'; import { StratosTabMetadata } from '../../../core/extension/extension-service'; import { CurrentUserPermissionsService } from '../../../core/permissions/current-user-permissions.service'; import { IBreadcrumb } from '../../../shared/components/breadcrumbs/breadcrumbs.types'; +import { map } from 'rxjs/operators'; @@ -54,7 +55,7 @@ export class PageSideNavComponent implements OnInit { } ngOnInit() { - this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable(); + this.activeTab$ = this.tabNavService.getCurrentTabHeaderObservable().pipe(map(item => item ? item.label : null)); } } diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html index ea0067edbc..b0747565c1 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.html @@ -14,6 +14,20 @@
{{ labelSingular && value === '1' ? labelSingular : label }}
+
+
+ info + {{ alertInfo.info }} +
+
+ warning +
{{ alertInfo.warning }}
+
+
+ error + {{ alertInfo.error }} +
+
diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss index 482e94a189..78aa3599eb 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.scss @@ -40,4 +40,26 @@ &__limit { font-size: 18px; } + &__alerts { + cursor: pointer; + display: flex; + flex: 0; + flex-direction: column; + } + &__alert-badge { + align-items: center; + border-radius: 4px; + color: #fff; + display: flex; + font-size: 14px; + margin-bottom: 2px; + padding: 2px 4px; + + &> mat-icon { + font-size: 16px; + height: 16px; + margin-right: 2px; + width: 16x; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss index eba218e2e2..2493db6d56 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.theme.scss @@ -1,6 +1,12 @@ @mixin app-card-number-metric-theme($theme, $app-theme) { $status-colors: map-get($app-theme, status); $subdued: mat-color($app-theme, subdued-color); + + $status-colors: map-get($app-theme, status); + $status-warning: map-get($status-colors, warning); + $status-danger: map-get($status-colors, danger); + $status-info: map-get($status-colors, info); + .number-metric-card { &__icon, &__anchor, @@ -9,4 +15,16 @@ color: $subdued; } } -} + + .number-metric-card__alert-badge { + &-error { + background-color: $status-danger; + } + &-info { + background-color: $status-info; + } + &-warning { + background-color: $status-warning; + } + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts index f0170d28b4..fa02ab447e 100644 --- a/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts +++ b/src/frontend/packages/core/src/shared/components/cards/card-number-metric/card-number-metric.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Store } from '@ngrx/store'; import { BehaviorSubject } from 'rxjs'; @@ -8,6 +8,14 @@ import { StratosStatus } from '../../../../../../store/src/types/shared.types'; import { UtilsService } from '../../../../core/utils.service'; import { determineCardStatus } from '../card-status/card-status.component'; +enum AlertLevel { + OK = 0, + Info, + Warning, + Error, + Unknown, +} + @Component({ selector: 'app-card-number-metric', templateUrl: './card-number-metric.component.html', @@ -26,6 +34,16 @@ export class CardNumberMetricComponent implements OnInit, OnChanges { @Input() textOnly = false; @Input() labelAtTop = false; @Input() link: () => void | string; + @Output() showAlerts = new EventEmitter(); + + @Input('alerts') + set alerts(alerts) { + if (alerts) { + this.processAlerts(alerts); + } + } + + alertInfo: any; formattedValue: string; formattedLimit: string; @@ -102,4 +120,31 @@ export class CardNumberMetricComponent implements OnInit, OnChanges { this.link(); } } + + processAlerts(alerts) { + this.alertInfo = { + info: 0, + warning: 0, + error: 0 + }; + + alerts.forEach((alert) => { + switch (alert.level as AlertLevel) { + case AlertLevel.Warning: + this.alertInfo.warning++; + break; + case AlertLevel.Error: + this.alertInfo.error++; + break; + case AlertLevel.Info: + this.alertInfo.info++; + break; + } + }); + } + + public alertsClicked() { + this.showAlerts.emit(this.alertInfo); + } + } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts index 979c260eb6..5d12e93d83 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/app-table-cell-default/app-table-cell-default.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnDestroy } from '@angular/core'; -import { Subscription, Observable } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { objectHelper } from '../../../../../core/helper-classes/object.helpers'; import { pathGet } from '../../../../../core/utils.service'; @@ -22,6 +22,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements this.pRow = row; if (row) { this.setValue(row, this.schemaKey); + this.setSyncLink(); } } @@ -32,6 +33,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements this.pSchemaKey = schemaKey; if (this.row) { this.setValue(this.row, schemaKey); + this.setSyncLink(); } } @@ -63,7 +65,7 @@ export class TableCellDefaultComponent extends TableCellCustom implements } private setSyncLink() { - if (!this.cellDefinition.getLink) { + if (!this.cellDefinition || !this.cellDefinition.getLink) { return; } const linkValue = this.cellDefinition.getLink(this.row); diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html index 01680e8f60..107f5c8149 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-actions/table-cell-actions.component.html @@ -3,8 +3,9 @@ - diff --git a/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts b/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts index 3ca1675980..8c66274b02 100644 --- a/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts +++ b/src/frontend/packages/core/src/shared/components/page-sub-nav/page-sub-nav.component.ts @@ -1,7 +1,8 @@ import { TemplatePortal } from '@angular/cdk/portal'; -import { AfterViewInit, Component, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy, TemplateRef, ViewChild } from '@angular/core'; import { TabNavService } from '../../../../tab-nav.service'; +import { IHeaderBreadcrumbLink } from '../page-header/page-header.types'; @Component({ selector: 'app-page-sub-nav', @@ -9,6 +10,12 @@ import { TabNavService } from '../../../../tab-nav.service'; styleUrls: ['./page-sub-nav.component.scss'] }) export class PageSubNavComponent implements AfterViewInit, OnDestroy { + + @Input('breadcrumbs') + set breadcrumbs(crumbs: IHeaderBreadcrumbLink[]) { + this.tabNavService.setSubNavBreadcrumbs(crumbs); + } + @ViewChild('subNavTmpl', { static: true }) subNavTmpl: TemplateRef; constructor(private tabNavService: TabNavService) { } diff --git a/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts b/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts index 8d9cedccb8..8c075008f8 100644 --- a/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts +++ b/src/frontend/packages/core/src/shared/components/ssh-viewer/ssh-viewer.component.ts @@ -122,7 +122,7 @@ export class SshViewerComponent implements OnInit, OnDestroy { this.xterm.write(String.fromCharCode(parseInt(c, 16))); } } else { - console.log('Error') + console.error('Error: ', this.errorMessage) const eMsg = this.errorMessage; this.errorMessage = eMsg; } @@ -145,17 +145,17 @@ export class SshViewerComponent implements OnInit, OnDestroy { parseInt(chars[1], 16) === 93 && parseInt(chars[2], 16) === 50 && parseInt(chars[3], 16) === 59) { - let title = ''; - for (let i = 4; i < chars.length - 1; i++) { - title += String.fromCharCode(parseInt(chars[i], 16)); - } - if (title.length > 0 && title.charAt(0) === '!') { - this.errorMessage = title.substr(1); - console.log(this.errorMessage); - return true; - } - this.message = title; + let title = ''; + for (let i = 4; i < chars.length - 1; i++) { + title += String.fromCharCode(parseInt(chars[i], 16)); } + if (title.length > 0 && title.charAt(0) === '!') { + this.errorMessage = title.substr(1); + console.error(this.errorMessage); + return true; + } + this.message = title; + } return false; } } diff --git a/src/frontend/packages/core/src/shared/services/session.service.ts b/src/frontend/packages/core/src/shared/services/session.service.ts new file mode 100644 index 0000000000..1c133f2106 --- /dev/null +++ b/src/frontend/packages/core/src/shared/services/session.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { GeneralEntityAppState } from '../../../../store/src/app-state'; +import { selectSessionData } from '../../../../store/src/reducers/auth.reducer'; + +@Injectable() +export class SessionService { + + constructor(private store: Store) { } + + isTechPreview(): Observable { + return this.store.select(selectSessionData()).pipe( + first(), + map(sessionData => sessionData.config.enableTechPreview || false) + ) + } +} \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/services/snackbar.service.ts b/src/frontend/packages/core/src/shared/services/snackbar.service.ts index 13ba4eb2c6..bfa94ce28e 100644 --- a/src/frontend/packages/core/src/shared/services/snackbar.service.ts +++ b/src/frontend/packages/core/src/shared/services/snackbar.service.ts @@ -4,26 +4,31 @@ import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material/s import { SnackBarReturnComponent } from '../components/snackbar-return/snackbar-return.component'; /** - * Servicve for showing snackbars + * Service for showing snackbars */ @Injectable({ providedIn: 'root', }) export class SnackBarService { - constructor(public snackBar: MatSnackBar) {} + constructor(public snackBar: MatSnackBar) { } private snackBars: MatSnackBarRef[] = []; public show(message: string, closeMessage?: string, duration: number = 5000) { this.snackBars.push(this.snackBar.open(message, closeMessage, { - duration: closeMessage ? null :duration + duration: closeMessage ? null : duration })); } - public showReturn(message: string, returnUrl: string, returnLabel: string) { + public showReturn(message: string, returnUrl: string | string[], returnLabel: string, duration?: number) { this.snackBars.push(this.snackBar.openFromComponent(SnackBarReturnComponent, { - data: { message, returnUrl, returnLabel } + duration, + data: { + message, + returnUrl, + returnLabel, + } })); } diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index e5915d0af6..160c4d90a7 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -117,6 +117,7 @@ import { UsageBytesPipe } from './pipes/usage-bytes.pipe'; import { ValuesPipe } from './pipes/values.pipe'; import { LongRunningOperationsService } from './services/long-running-op.service'; import { MetricsRangeSelectorService } from './services/metrics-range-selector.service'; +import { SessionService } from './services/session.service'; import { UserPermissionDirective } from './user-permission.directive'; @@ -322,6 +323,7 @@ import { UserPermissionDirective } from './user-permission.directive'; InternalEventMonitorFactory, MetricsRangeSelectorService, LongRunningOperationsService, + SessionService ] }) export class SharedModule { } diff --git a/src/frontend/packages/core/tab-nav.service.ts b/src/frontend/packages/core/tab-nav.service.ts index fbdad8cad7..d11ed06666 100644 --- a/src/frontend/packages/core/tab-nav.service.ts +++ b/src/frontend/packages/core/tab-nav.service.ts @@ -5,6 +5,7 @@ import { asapScheduler, BehaviorSubject, combineLatest, Observable, Subject } fr import { filter, map, observeOn, publishReplay, refCount, startWith } from 'rxjs/operators'; import { IPageSideNavTab } from './src/features/dashboard/page-side-nav/page-side-nav.component'; +import { IHeaderBreadcrumbLink } from './src/shared/components/page-header/page-header.types'; @Injectable() @@ -21,6 +22,9 @@ export class TabNavService { private tabSubNavSubject: BehaviorSubject>; public tabSubNav$: Observable>; + private tabSubNavBreadcrumbsSubject: BehaviorSubject; + public tabSubNavBreadcrumbs$: Observable; + private pageHeaderSubject: BehaviorSubject>; public pageHeader$: Observable>; @@ -36,6 +40,10 @@ export class TabNavService { this.tabSubNavSubject.next(portal); } + public setSubNavBreadcrumbs(breadcrumbs: IHeaderBreadcrumbLink[]) { + this.tabSubNavBreadcrumbsSubject.next(breadcrumbs); + } + public setPageHeader(portal: Portal) { this.pageHeaderSubject.next(portal); } @@ -43,12 +51,13 @@ export class TabNavService { public clear() { this.tabNavsSubject.next(undefined); this.tabHeaderSubject.next(undefined); - this.tabSubNavSubject.next(undefined); + this.clearSubNav(); this.pageHeaderSubject.next(undefined); } public clearSubNav() { this.tabSubNavSubject.next(undefined); + this.tabSubNavBreadcrumbsSubject.next(undefined); } public getCurrentTabHeaderObservable() { @@ -63,7 +72,7 @@ export class TabNavService { ); } - public getCurrentTabHeader = (tabs: IPageSideNavTab[]) => { + private getCurrentTabHeader = (tabs: IPageSideNavTab[]) => { if (!tabs) { return null; } @@ -74,7 +83,7 @@ export class TabNavService { if (!activeTab) { return null; } - return activeTab.label; + return activeTab; } private observeSubject(subject: Subject) { @@ -93,6 +102,8 @@ export class TabNavService { this.tabHeader$ = this.observeSubject(this.tabHeaderSubject); this.tabSubNavSubject = new BehaviorSubject(undefined); this.tabSubNav$ = this.observeSubject(this.tabSubNavSubject); + this.tabSubNavBreadcrumbsSubject = new BehaviorSubject(undefined); + this.tabSubNavBreadcrumbs$ = this.observeSubject(this.tabSubNavBreadcrumbsSubject); this.pageHeaderSubject = new BehaviorSubject(undefined); this.pageHeader$ = this.observeSubject(this.pageHeaderSubject); } diff --git a/src/frontend/packages/store/src/actions/pagination.actions.ts b/src/frontend/packages/store/src/actions/pagination.actions.ts index b36603d1e6..0a6f540eec 100644 --- a/src/frontend/packages/store/src/actions/pagination.actions.ts +++ b/src/frontend/packages/store/src/actions/pagination.actions.ts @@ -6,6 +6,7 @@ import { PaginationClientFilter, PaginationParam } from '../types/pagination.typ export const CLEAR_PAGINATION_OF_TYPE = '[Pagination] Clear all pages of type'; export const CLEAR_PAGINATION_OF_ENTITY = '[Pagination] Clear pagination of entity'; export const RESET_PAGINATION = '[Pagination] Reset pagination'; +export const RESET_PAGINATION_OF_TYPE = '[Pagination] Reset pagination of type'; export const CREATE_PAGINATION = '[Pagination] Create pagination'; export const CLEAR_PAGES = '[Pagination] Clear pages only'; export const SET_PAGE = '[Pagination] Set page'; @@ -57,6 +58,13 @@ export class ResetPagination extends BasePaginationAction implements Action { type = RESET_PAGINATION; } +export class ResetPaginationOfType extends BasePaginationAction implements Action { + constructor(pEntityConfig: Partial) { + super(pEntityConfig); + } + type = RESET_PAGINATION_OF_TYPE; +} + export class CreatePagination extends BasePaginationAction implements Action { /** * @param seed The pagination key for the section we should use as a seed when creating the new pagination section. diff --git a/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts b/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts index fa82840e99..509b5f661d 100644 --- a/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts +++ b/src/frontend/packages/store/src/entity-request-pipeline/entity-pagination-request-pipeline.ts @@ -109,7 +109,7 @@ export const basePaginatedRequestPipeline: EntityRequestPipeline = ( ); // Keep, helpful for debugging below chain via tap - // const debug = (val, location) => console.log(`${entity.endpointType}:${entity.entityKey}:${location}: `, val); + // const debug = (val, location) => console.warn(`${entity.endpointType}:${entity.entityKey}:${location}: `, val); return getRequestObjectObservable(request).pipe( first(), diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts index 7e57e52957..a34f1d65cb 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-create-pagination.ts @@ -1,6 +1,6 @@ +import { CreatePagination } from '../../actions/pagination.actions'; import { entityCatalog } from '../../entity-catalog/entity-catalog'; import { EntityCatalogEntityConfig } from '../../entity-catalog/entity-catalog.types'; -import { CreatePagination } from '../../actions/pagination.actions'; import { PaginationEntityState, PaginationState } from '../../types/pagination.types'; import { spreadClientPagination } from './pagination-reducer.helper'; diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts index 603b2b767f..18c1566775 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination.ts @@ -37,26 +37,68 @@ export function getDefaultPaginationEntityState(ignoreMaxed?: boolean): Paginati }; } -export function paginationResetPagination(state: PaginationState, action: ResetPagination): PaginationState { + +export function paginationResetPagination(state: PaginationState, action: ResetPagination, allTypes = false): PaginationState { const entityKey = entityCatalog.getEntityKey(action.entityConfig); - if (!state[entityKey] || !state[entityKey][action.paginationKey]) { + + if (!state[entityKey]) { return state; } - const { ids, pageRequests, pageCount, currentPage, totalResults } = getDefaultPaginationEntityState(); + + const entityState = allTypes ? + paginationResetAllPaginationSections(state, entityKey) : + paginationResetPaginationSection(state, action.paginationKey, entityKey); + + if (!entityState) { + return state; + } + const newState = { ...state }; - const entityState = { - ...newState[entityKey], - [action.paginationKey]: { - ...newState[entityKey][action.paginationKey], - ids, - pageRequests, - pageCount, - currentPage, - totalResults, - } - } as PaginationEntityTypeState; return { ...newState, [entityKey]: entityState }; } + +/** + * Reset all pagination sections of an entity type + */ +function paginationResetAllPaginationSections(state: PaginationState, entityKey: string): PaginationEntityTypeState { + return Object.entries(state[entityKey]).reduce((res, [paginationKey, paginationSection]) => { + res[paginationKey] = paginationResetPaginationState(paginationSection); + return res; + }, {} as PaginationEntityTypeState); +} + +/** + * Reset a single pagination section of an entity type + */ +function paginationResetPaginationSection(state: PaginationState, paginationKey: string, entityKey: string): PaginationEntityTypeState { + + const paginationSection = state[entityKey][paginationKey] + if (!paginationSection) { + return; + } + + const entityState: PaginationEntityTypeState = { + ...state[entityKey], + [paginationKey]: paginationResetPaginationState(paginationSection) + }; + return entityState; +} + +/** + * Reset a pagination section (retain initial/user sort/filter/etc) + */ +function paginationResetPaginationState(oldEntityState: PaginationEntityState) { + const { ids, pageRequests, pageCount, currentPage, totalResults } = getDefaultPaginationEntityState(); + const entityState: PaginationEntityState = { + ...oldEntityState, + ids, + pageRequests, + pageCount, + currentPage, + totalResults, + } + return entityState; +} diff --git a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts index f39aa853f0..d24616c794 100644 --- a/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts +++ b/src/frontend/packages/store/src/reducers/pagination-reducer/pagination.reducer.ts @@ -14,6 +14,7 @@ import { IgnorePaginationMaxedState, REMOVE_PARAMS, RESET_PAGINATION, + RESET_PAGINATION_OF_TYPE, SET_CLIENT_FILTER, SET_CLIENT_FILTER_KEY, SET_CLIENT_PAGE, @@ -121,6 +122,10 @@ function paginate(action, state = {}, updatePagination) { return paginationResetPagination(state, action); } + if (action.type === RESET_PAGINATION_OF_TYPE && !action.keepPages) { + return paginationResetPagination(state, action, true); + } + if (action.type === CLEAR_PAGINATION_OF_TYPE) { const clearAction = action as ClearPaginationOfType; const clearEntityType = entityCatalog.getEntityKey(clearAction.entityConfig.endpointType, clearAction.entityConfig.entityType); diff --git a/src/frontend/packages/suse-extensions/sass/_all-theme.scss b/src/frontend/packages/suse-extensions/sass/_all-theme.scss index b6b53feafa..9f21f4317f 100644 --- a/src/frontend/packages/suse-extensions/sass/_all-theme.scss +++ b/src/frontend/packages/suse-extensions/sass/_all-theme.scss @@ -1,3 +1,7 @@ +// Theming for the copmponents in the Kubernetes package + +@import '../src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme'; +@import '../src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.theme'; @import '../src/custom/helm/list-types/monocular-chart-card/monocular-chart-card.component.theme'; @mixin apply-theme-suse-extensions($stratos-theme) { @@ -5,6 +9,8 @@ $theme: map-get($stratos-theme, theme); $app-theme: map-get($stratos-theme, app-theme); + @include kube-analysis-report-theme($theme, $app-theme); + @include kube-analysis-card-theme($theme, $app-theme); @include monocular-chart-card($theme, $app-theme); } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.html new file mode 100644 index 0000000000..6bfa187493 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.html @@ -0,0 +1,18 @@ + +
+ + + + + + + +
+
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.spec.ts new file mode 100644 index 0000000000..f2d53545a3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnalysisReportRunnerComponent } from './analysis-report-runner.component'; + +describe('AnalysisReportRunnerComponent', () => { + let component: AnalysisReportRunnerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AnalysisReportRunnerComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalysisReportRunnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.ts new file mode 100644 index 0000000000..e14601dd36 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-runner/analysis-report-runner.component.ts @@ -0,0 +1,49 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { KubernetesAnalysisService, KubernetesAnalysisType } from '../../services/kubernetes.analysis.service'; +import { + KubernetesAnalysisInfoComponent, +} from '../../tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component'; + +@Component({ + selector: 'app-analysis-report-runner', + templateUrl: './analysis-report-runner.component.html', + styleUrls: ['./analysis-report-runner.component.scss'] +}) +export class AnalysisReportRunnerComponent implements OnInit { + + canShow$: Observable; + analyzers$: Observable; + @Input() kubeId: string; + @Input() namespace: string; + @Input() app: string; + + constructor( + public analysisService: KubernetesAnalysisService, + private sidePanelService: SidePanelService, + ) { + this.canShow$ = analysisService.hideAnalysis$.pipe(map(h => !h)); + } + + public runAnalysis(id: string) { + this.analysisService.run(id, this.kubeId, this.namespace, this.app); + } + + ngOnInit(): void { + if (this.namespace) { + this.analyzers$ = this.analysisService.namespaceAnalyzers$ + } else { + this.analyzers$ = this.analysisService.analyzers$; + } + } + + showAnalyzersInfo() { + this.sidePanelService.showModal(KubernetesAnalysisInfoComponent, { + analyzers$: this.analysisService.analyzers$ + }); + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html new file mode 100644 index 0000000000..4c13aaac65 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.html @@ -0,0 +1,22 @@ + +
+ + + + + + + + +
+
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.scss new file mode 100644 index 0000000000..2cd2769879 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.scss @@ -0,0 +1,3 @@ +.analysis-menu-divider { + margin: 4px 0; +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts new file mode 100644 index 0000000000..d23be84d77 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../core/md.module'; + +import { AnalysisReportSelectorComponent } from './analysis-report-selector.component'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { KubeBaseGuidMock, KubernetesBaseTestModules } from '../../kubernetes.testing.module'; + +describe('AnalysisReportSelectorComponent', () => { + let component: AnalysisReportSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AnalysisReportSelectorComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalysisReportSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts new file mode 100644 index 0000000000..ffb13fa10c --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-selector/analysis-report-selector.component.ts @@ -0,0 +1,87 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import * as moment from 'moment'; +import { Observable, Subscription } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +import { safeUnsubscribe } from '../../../../../../core/src/core/utils.service'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { AnalysisReport } from '../../store/kube.types'; + +@Component({ + selector: 'app-analysis-report-selector', + templateUrl: './analysis-report-selector.component.html', + styleUrls: ['./analysis-report-selector.component.scss'] +}) +export class AnalysisReportSelectorComponent implements OnInit, OnDestroy { + + public selection = { title: 'None' }; + + public canShow$: Observable; + public analyzers$: Observable; + + @Input() endpoint; + @Input() path; + @Input() prompt = 'Overlay Analysis'; + @Input() allowNone = true; + @Input() autoSelect; + + @Output() selected = new EventEmitter(); + @Output() reportCount = new EventEmitter(); + + autoSelected = false; + + subs: Subscription[] = []; + + constructor(public analysisService: KubernetesAnalysisService) { + this.canShow$ = analysisService.hideAnalysis$.pipe(map(h => !h)); + } + + ngOnInit() { + this.analyzers$ = this.analysisService.getByPath(this.endpoint, this.path).pipe( + map(reports => { + const res = []; + if (this.allowNone) { + res.push({ title: 'None' }); + } + if (reports) { + reports.forEach(r => { + const c = { ...r }; + const title = c.type.substr(0, 1).toUpperCase() + c.type.substr(1); + const age = moment(c.created).fromNow(true); + c.title = `${title} (${age})`; + res.push(c); + }); + } + this.reportCount.next(res.length); + return res; + }), + tap(reports => { + if (!this.autoSelected && this.autoSelect && reports.length > 0) { + this.onSelected(reports[0]); + } + }) + ) + } + + + // Selection changed + public onSelected(d) { + this.selection = d; + if (!d.id) { + this.selected.emit(null); + } else { + this.selected.next(d); + } + } + + public refreshReports($event: MouseEvent) { + this.analysisService.getByPath(this.endpoint, this.path, true) + $event.preventDefault(); + $event.cancelBubble = true; + } + + ngOnDestroy() { + safeUnsubscribe(...this.subs) + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html new file mode 100644 index 0000000000..c527914e5a --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.html @@ -0,0 +1 @@ + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts new file mode 100644 index 0000000000..ed39ab0acf --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnalysisReportViewerComponent } from './analysis-report-viewer.component'; +import { KubernetesBaseTestModules } from '../kubernetes.testing.module'; + +describe('AnalysisReportViewerComponent', () => { + let component: AnalysisReportViewerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AnalysisReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalysisReportViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts new file mode 100644 index 0000000000..735ff9b6ce --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/analysis-report-viewer.component.ts @@ -0,0 +1,76 @@ +import { + Component, + ComponentFactoryResolver, + ComponentRef, + Input, + OnDestroy, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +import { AnalysisReport } from '../store/kube.types'; +import { KubeScoreReportViewerComponent } from './kube-score-report-viewer/kube-score-report-viewer.component'; +import { PopeyeReportViewerComponent } from './popeye-report-viewer/popeye-report-viewer.component'; + +export interface IReportViewer { + report: AnalysisReport; +} + +@Component({ + selector: 'app-analysis-report-viewer', + templateUrl: './analysis-report-viewer.component.html', + styleUrls: ['./analysis-report-viewer.component.scss'] +}) +export class AnalysisReportViewerComponent implements OnDestroy { + + // Component reference for the dynamically created auth form + @ViewChild('reportViewer', { read: ViewContainerRef, static: true }) + public container: ViewContainerRef; + private reportComponentRef: ComponentRef; + + private id: string; + + @Input('report') + set report(report: AnalysisReport) { + if (report === null || report.id === this.id) { + return; + } + this.id = report.id; + this.updateReport(report); + } + + constructor(private resolver: ComponentFactoryResolver) { } + + updateReport(report) { + switch (report.format) { + case 'popeye': + this.createComponent(PopeyeReportViewerComponent, report); + break; + case 'kubescore': + this.createComponent(KubeScoreReportViewerComponent, report); + break; + } + } + + // Dynamically create the component for the report type type + createComponent(component: Type, report: AnalysisReport) { + if (!component || !this.container) { + return; + } + + if (this.reportComponentRef) { + this.reportComponentRef.destroy(); + } + const factory = this.resolver.resolveComponentFactory(component); + this.reportComponentRef = this.container.createComponent(factory); + // this.reportComponentRef.instance.setReport(report); + this.reportComponentRef.instance.report = report; + } + + ngOnDestroy() { + if (this.reportComponentRef) { + this.reportComponentRef.destroy(); + } + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html new file mode 100644 index 0000000000..a5dfac6b45 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.html @@ -0,0 +1,18 @@ +
+
{{ group._name }}
+
+
+
+ check_circle + info + warning + error + help_outline +
+
{{ check.Check.Name }}
+
+
+
{{ comment.Summary }}
+
+
+
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss new file mode 100644 index 0000000000..6c76b78b25 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.scss @@ -0,0 +1,23 @@ +.report { + &__group { + margin-bottom: 10px; + } + &__group-name { + font-weight: bold; + padding: 4px 0; + } + &__check { + align-items: center; + display: flex; + flex-direction: row; + margin-left: 10px; + } + &__icon { + margin-right: 4px; + } + &__comment { + display: list-item; + list-style: square; + margin-left: 58px; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts new file mode 100644 index 0000000000..a93f22f618 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../core/md.module'; + +import { KubeScoreReportViewerComponent } from './kube-score-report-viewer.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; + +describe('KubeScoreReportViewerComponent', () => { + let component: KubeScoreReportViewerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubeScoreReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeScoreReportViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts new file mode 100644 index 0000000000..0a53ea6921 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit } from '@angular/core'; + +import { IReportViewer } from '../analysis-report-viewer.component'; + +@Component({ + selector: 'app-kube-score-report-viewer', + templateUrl: './kube-score-report-viewer.component.html', + styleUrls: ['./kube-score-report-viewer.component.scss'] +}) +export class KubeScoreReportViewerComponent implements OnInit, IReportViewer { + + /* + Kube Score grading + + See: https://github.com/zegl/kube-score/blob/eca7bda47f5b3c523a0f41945cb1adda0a4e2e2e/scorecard/scorecard.go + GradeCritical Grade = 1 + GradeWarning Grade = 5 + GradeAlmostOK Grade = 7 + GradeAllOK Grade = 10 + */ + + report: any; + processed: any; + + constructor() { } + + ngOnInit() { + this.processed = []; + // Turn the report into an array + if (this.report) { + Object.keys(this.report.report).forEach(key => { + const filtered = this.filter(this.report.report[key]); + if (filtered.length > 0) { + this.processed.push({ + ...this.report.report[key], + _checks: filtered, + _name: key, + }); + } + }); + } + } + + public filter(report) { + const filtered = []; + report.Checks.forEach(r => { + if (r.Grade !== 10 && !r.Skipped) { + filtered.push(r); + } + }); + return filtered; + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html new file mode 100644 index 0000000000..6b198581cf --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.html @@ -0,0 +1,60 @@ +
+
+
Report
+
+
Score
+
{{processed.popeye.score }}
+
+
+
Grade
+
{{processed.popeye.grade }}
+
+
+ +
+
+
+
{{ section.sanitizer }}
+
+
OK
+
{{section.tally.ok }}
+
+
+
Info
+
{{section.tally.info }}
+
+
+
Warning
+
{{section.tally.warning }}
+
+
+
Error
+
{{section.tally.error }}
+
+
+
Score
+
{{section.tally.score }}
+
+
+ + + + + + + +
{{ group.name }} +
+
+ check_circle + info + warning + error + help_outline +
+ {{ issue.message }} +
+
+
+
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss new file mode 100644 index 0000000000..f70ee0c2c3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.scss @@ -0,0 +1,43 @@ +.report { + &__report-header { + align-items: center; + display: flex; + margin-bottom: 8px; + } + &__header { + align-items: center; + display: flex; + } + &__title { + flex: 1; + } + &__stat { + display: flex; + flex-direction: column; + padding: 5px 12px; + &>div:first-child { + opacity: 0.8; + } + } + &__score { + flex: 0; + font-size: 20px; + } + &__grade { + flex: 0; + font-size: 20px; + } + &__table { + margin-left: 20px; + } + &__issue { + align-items: center; + display: flex; + } + &__icon { + padding-right: 4px; + } + &__table-name { + vertical-align: top; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts new file mode 100644 index 0000000000..ccbe5c5081 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../core/md.module'; + +import { PopeyeReportViewerComponent } from './popeye-report-viewer.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; + +describe('PopeyeReportViewerComponent', () => { + let component: PopeyeReportViewerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PopeyeReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PopeyeReportViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts new file mode 100644 index 0000000000..1537046212 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from '@angular/core'; + +import { IReportViewer } from '../analysis-report-viewer.component'; + +@Component({ + selector: 'app-popeye-report-viewer', + templateUrl: './popeye-report-viewer.component.html', + styleUrls: ['./popeye-report-viewer.component.scss'] +}) +export class PopeyeReportViewerComponent implements OnInit, IReportViewer { + + report: any; + processed: any; + + ngOnInit() { + this.processed = this.apply(this.report); + } + + private apply(response) { + if (response) { + // In order to supplement the sanitizers with extra properties need to create new obj (see spread below and `reduce`) + response = { + ...response, + report: { + ...response.report, + popeye: { + ...response.report.popeye + } + } + } + // Make the response easier to render + response.report.popeye.sanitizers = response.report.popeye.sanitizers.reduce((ss, oldS) => { + const s = { ...oldS } + const groups = []; + let totalIssues = 0; + if (s.issues) { + Object.keys(s.issues).forEach(key => { + const issues = s.issues[key]; + totalIssues += issues.length; + if (issues.length > 0) { + groups.push({ + name: key, + issues + }); + } + }); + s.hide = totalIssues === 0; + } else { + s.hide = true; + } + s.groups = groups; + ss.push(s); + return ss; + }, []); + + return response.report; + } + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html new file mode 100644 index 0000000000..0e7a5b9605 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.html @@ -0,0 +1,5 @@ + +
+ +
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss new file mode 100644 index 0000000000..d7d483462b --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.scss @@ -0,0 +1,14 @@ +.alert { + &__info { + align-items: center; + display: flex; + margin: 4px 20px; + } + &__icon { + margin-right: 8px; + } + &__group { + font-weight: bold; + padding: 4px 0; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts new file mode 100644 index 0000000000..f78f26e839 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResourceAlertPreviewComponent } from './resource-alert-preview.component'; +import { ResourceAlertViewComponent } from './resource-alert-view/resource-alert-view.component'; +import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service'; +import { KubernetesBaseTestModules } from '../../kubernetes.testing.module'; + +describe('ResourceAlertPreviewComponent', () => { + let component: ResourceAlertPreviewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ResourceAlertPreviewComponent, ResourceAlertViewComponent ], + imports: [ + KubernetesBaseTestModules, + ], + providers: [ + SidePanelService, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResourceAlertPreviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts new file mode 100644 index 0000000000..5abfac176e --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-preview.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { PreviewableComponent } from 'frontend/packages/core/src/shared/previewable-component'; + +@Component({ + selector: 'app-resource-alert-preview', + templateUrl: './resource-alert-preview.component.html', + styleUrls: ['./resource-alert-preview.component.scss'] +}) +export class ResourceAlertPreviewComponent implements PreviewableComponent { + + title: string; + + resource: any; + alerts: any; + + constructor() { } + + setProps(props: { [key: string]: any; }): void { + this.resource = props.resource; + this.title = `${this.resource.kind} Alerts`; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html new file mode 100644 index 0000000000..f9193d84c4 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.html @@ -0,0 +1,16 @@ +
+
+
{{group.name}}
+
+
+
+ info + warning + error + help_outline +
+ {{ alert.message }} +
+
+
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss new file mode 100644 index 0000000000..d7d483462b --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.scss @@ -0,0 +1,14 @@ +.alert { + &__info { + align-items: center; + display: flex; + margin: 4px 20px; + } + &__icon { + margin-right: 8px; + } + &__group { + font-weight: bold; + padding: 4px 0; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts new file mode 100644 index 0000000000..0ffbf5f5ff --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../../core/md.module'; + +import { ResourceAlertViewComponent } from './resource-alert-view.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service'; + +describe('ResourceAlertViewComponent', () => { + let component: ResourceAlertViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ResourceAlertViewComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResourceAlertViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts new file mode 100644 index 0000000000..15368b31e4 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-resource-alert-view', + templateUrl: './resource-alert-view.component.html', + styleUrls: ['./resource-alert-view.component.scss'] +}) +export class ResourceAlertViewComponent { + + alertInfo; + + @Input() + set alerts(data: any) { + if (data) { + const alerts = data.alerts ? data.alerts : data; + this.alertInfo = this.normalize(alerts); + } + } + + @Input() showHeader = true; + + normalize(data) { + // Normalize the alerts into groups + const normalized = {}; + data.forEach(item => { + const path = item.namespace ? `${item.namespace}/${item.name}` : item.name; + if (!normalized[path]) { + normalized[path] = []; + } + normalized[path].push({ + ...item, + path + }); + }); + + const arr = []; + Object.keys(normalized).forEach(group => { + arr.push({ + name: group, + alerts: normalized[group] + }); + }); + return arr; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-catalog.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-catalog.ts index 816ab0bd47..e41dfd779d 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-catalog.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-catalog.ts @@ -4,6 +4,7 @@ import { } from '../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IFavoriteMetadata } from '../../../../store/src/types/user-favorites.types'; import { + AnalysisReportsActionBuilders, KubeDashboardActionBuilders, KubeDeploymentActionBuilders, KubeNamespaceActionBuilders, @@ -13,6 +14,7 @@ import { KubeStatefulSetsActionBuilders, } from './store/action-builders/kube.action-builders'; import { + AnalysisReport, KubernetesDeployment, KubernetesNamespace, KubernetesNode, @@ -34,6 +36,7 @@ export class KubeEntityCatalog { public namespace: StratosCatalogEntity; public service: StratosCatalogEntity public dashboard: StratosCatalogEntity; + public analysisReport: StratosCatalogEntity; } /** diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-factory.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-factory.ts index 1fed3c1b20..02637d2f5a 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-factory.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-factory.ts @@ -22,6 +22,7 @@ export const kubernetesServicesEntityType = 'kubernetesService'; export const kubernetesStatefulSetsEntityType = 'kubernetesStatefulSet'; export const kubernetesDeploymentsEntityType = 'kubernetesDeployment'; export const kubernetesDashboardEntityType = 'kubernetesDashboard'; +export const analysisReportEntityType = 'analysisReport'; export const getKubeAppId = (object: KubernetesApp) => object.name; @@ -104,6 +105,13 @@ entityCache[kubernetesDashboardEntityType] = new KubernetesEntitySchema( { idAttribute: getGuidFromKubeDashboardObj } ); +// Analysis Reports - should not be bound to an endpoint +entityCache[analysisReportEntityType] = new KubernetesEntitySchema( + analysisReportEntityType, + {}, + { idAttribute: 'id' } +); + entityCache[metricEntityType] = new KubernetesEntitySchema(metricEntityType); export function addKubernetesEntitySchema(key: string, newSchema: EntitySchema) { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-generator.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-generator.ts index 1d8dc13dac..814f8a3e1f 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-generator.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-entity-generator.ts @@ -24,6 +24,7 @@ import { KubernetesGKEAuthFormComponent } from './auth-forms/kubernetes-gke-auth import { KubeConfigRegistrationComponent } from './kube-config-registration/kube-config-registration.component'; import { kubeEntityCatalog } from './kubernetes-entity-catalog'; import { + analysisReportEntityType, KUBERNETES_ENDPOINT_TYPE, kubernetesDashboardEntityType, kubernetesDeploymentsEntityType, @@ -35,6 +36,8 @@ import { kubernetesStatefulSetsEntityType, } from './kubernetes-entity-factory'; import { + AnalysisReportsActionBuilders, + analysisReportsActionBuilders, KubeDashboardActionBuilders, kubeDashboardActionBuilders, KubeDeploymentActionBuilders, @@ -186,6 +189,7 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { generateNamespacesEntity(endpointDefinition), generateServicesEntity(endpointDefinition), generateDashboardEntity(endpointDefinition), + generateAnalysisReportsEntity(endpointDefinition), generateMetricEntity(endpointDefinition), ...generateWorkloadsEntities(endpointDefinition) ]; @@ -283,6 +287,18 @@ function generateDashboardEntity(endpointDefinition: StratosEndpointExtensionDef return kubeEntityCatalog.dashboard; } +function generateAnalysisReportsEntity(endpointDefinition: StratosEndpointExtensionDefinition) { + const definition = { + type: analysisReportEntityType, + schema: kubernetesEntityFactory(analysisReportEntityType), + endpoint: endpointDefinition + }; + kubeEntityCatalog.analysisReport = new StratosCatalogEntity(definition, { + actionBuilders: analysisReportsActionBuilders + }); + return kubeEntityCatalog.analysisReport +} + function generateMetricEntity(endpointDefinition: StratosEndpointExtensionDefinition) { const definition: IStratosEntityDefinition = { type: metricEntityType, diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html new file mode 100644 index 0000000000..650e5a18aa --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts new file mode 100644 index 0000000000..5e5fdfaf98 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.spec.ts @@ -0,0 +1,46 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../core/md.module'; + +import { KubernetesNamespaceAnalysisReportComponent } from './kubernetes-namespace-analysis-report.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { + AnalysisReportSelectorComponent +} from './../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; +import { AnalysisReportViewerComponent } from './../../analysis-report-viewer/analysis-report-viewer.component'; +import { KubernetesNamespaceService } from '../../services/kubernetes-namespace.service'; +import { TabNavService } from 'frontend/packages/core/tab-nav.service'; + +describe('KubernetesNamespaceAnalysisReportComponent', () => { + let component: KubernetesNamespaceAnalysisReportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubernetesNamespaceAnalysisReportComponent, AnalysisReportSelectorComponent, AnalysisReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + KubernetesNamespaceService, + TabNavService, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubernetesNamespaceAnalysisReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts new file mode 100644 index 0000000000..cdaa70db35 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { Subject } from 'rxjs'; + +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { KubernetesNamespaceService } from '../../services/kubernetes-namespace.service'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { AnalysisReport } from '../../store/kube.types'; + +@Component({ + selector: 'app-kubernetes-namespace-analysis-report-tab', + templateUrl: './kubernetes-namespace-analysis-report.component.html', + styleUrls: ['./kubernetes-namespace-analysis-report.component.scss'], + providers: [ + KubernetesAnalysisService + ] +}) +export class KubernetesNamespaceAnalysisReportComponent { + + public report$ = new Subject(); + + path: string; + + currentReport = null; + + endpointID: string; + + noReportsAvailable = false; + + constructor( + public analyzerService: KubernetesAnalysisService, + public endpointService: KubernetesEndpointService, + public kubeNamespaceService: KubernetesNamespaceService, + ) { + this.endpointID = this.endpointService.kubeGuid; + this.path = `${this.kubeNamespaceService.namespaceName}`; + this.report$.next(null); + } + + public analysisChanged(report) { + if (report.id !== this.currentReport) { + this.currentReport = report.id; + this.analyzerService.getByID(this.endpointID, report.id).subscribe(r => this.report$.next(r)); + } + } + + public onReportCount(count: number) { + this.noReportsAvailable = count === 0; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts index 3847972a17..45ab5eb4a9 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-namespace/kubernetes-namespace.component.ts @@ -8,6 +8,7 @@ import { BaseKubeGuid } from '../kubernetes-page.types'; import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; import { KubernetesNamespaceService } from '../services/kubernetes-namespace.service'; import { KubernetesService } from '../services/kubernetes.service'; +import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service'; @Component({ selector: 'app-kubernetes-namespace', @@ -27,21 +28,20 @@ import { KubernetesService } from '../services/kubernetes.service'; }, KubernetesService, KubernetesEndpointService, - KubernetesNamespaceService + KubernetesNamespaceService, + KubernetesAnalysisService, ] }) export class KubernetesNamespaceComponent { - tabLinks = [ - { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, - { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' } - ]; + tabLinks = []; public breadcrumbs$: Observable; constructor( public kubeEndpointService: KubernetesEndpointService, - public kubeNamespaceService: KubernetesNamespaceService + public kubeNamespaceService: KubernetesNamespaceService, + public analysisService: KubernetesAnalysisService, ) { this.breadcrumbs$ = kubeEndpointService.endpoint$.pipe( map(endpoint => ([{ @@ -51,5 +51,11 @@ export class KubernetesNamespaceComponent { }]) ) ); + + this.tabLinks = [ + { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, + { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' }, + { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ }, + ]; } } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html index cb780e3041..c3ebbdd9e1 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.html @@ -13,7 +13,7 @@ {{ resource.age }}
- +
{{ label.name }}
{{ label.value }}
@@ -27,7 +27,10 @@
- + + + +
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts index 5df03aea30..75fb79c994 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.spec.ts @@ -5,6 +5,7 @@ import { SidePanelService } from '../../../shared/services/side-panel.service'; import { KubeBaseGuidMock, KubernetesBaseTestModules } from '../kubernetes.testing.module'; import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; import { KubernetesResourceViewerComponent } from './kubernetes-resource-viewer.component'; +import { ResourceAlertViewComponent } from './../analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component'; describe('KubernetesResourceViewerComponent', () => { let component: KubernetesResourceViewerComponent; @@ -12,7 +13,7 @@ describe('KubernetesResourceViewerComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [KubernetesResourceViewerComponent], + declarations: [KubernetesResourceViewerComponent, KubernetesResourceViewerComponent, ResourceAlertViewComponent], imports: KubernetesBaseTestModules, providers: [ KubernetesEndpointService, diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts index 871f4e9a17..4672958d66 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-resource-viewer/kubernetes-resource-viewer.component.ts @@ -6,10 +6,11 @@ import { filter, first, map, publishReplay, refCount, switchMap } from 'rxjs/ope import { EndpointsService } from '../../../../../core/src/core/endpoints.service'; import { PreviewableComponent } from '../../../../../core/src/shared/previewable-component'; import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; -import { BasicKubeAPIResource, KubeAPIResource } from '../store/kube.types'; +import { BasicKubeAPIResource, KubeAPIResource, KubeStatus } from '../store/kube.types'; export interface KubernetesResourceViewerConfig { title: string; + analysis?: any; resource$: Observable; resourceKind: string; } @@ -44,32 +45,42 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent { public hasPodMetrics$: Observable; public podRouterLink$: Observable; + private analysis; + public alerts; + setProps(props: KubernetesResourceViewerConfig) { this.title = props.title; + this.analysis = props.analysis; this.resource$ = props.resource$.pipe( - map((item: any) => {// KubeAPIResource + filter(item => !!item), + map((item: (KubeAPIResource | KubeStatus)) => { const resource: KubernetesResourceViewerResource = {} as KubernetesResourceViewerResource; const newItem = {} as any; resource.raw = item; - Object.keys(item || []).forEach(k => { - if (k !== 'endpointId' && k !== 'releaseTitle' && k !== 'expandedStatus') { + if (k !== 'endpointId' && k !== 'releaseTitle' && k !== 'expandedStatus' && k !== '_metadata') { newItem[k] = item[k]; } }); resource.jsonView = newItem; - resource.age = moment(item.metadata.creationTimestamp).fromNow(true); - resource.creationTimestamp = item.metadata.creationTimestamp; - - resource.labels = []; - Object.keys(item.metadata.labels || []).forEach(labelName => { - resource.labels.push({ - name: labelName, - value: item.metadata.labels[labelName] + + const fallback = item['_metadata'] || {}; + + const ts = item.metadata ? item.metadata.creationTimestamp : fallback.creationTimestamp; + resource.age = moment(ts).fromNow(true); + resource.creationTimestamp = ts; + + if (item.metadata && item.metadata.labels) { + resource.labels = []; + Object.keys(item.metadata.labels || []).forEach(labelName => { + resource.labels.push({ + name: labelName, + value: item.metadata.labels[labelName] + }); }); - }); + } if (item.metadata && item.metadata.annotations) { resource.annotations = []; @@ -81,8 +92,13 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent { }); } - resource.kind = item.kind || props.resourceKind; - resource.apiVersion = item.apiVersion || this.getVersionFromSelfLink(item.metadata.selfLink); + resource.kind = item['kind'] || fallback.kind || props.resourceKind; + resource.apiVersion = item['apiVersion'] || fallback.apiVersion || this.getVersionFromSelfLink(item.metadata['selfLink']); + + // Apply analysis if there is one - if this is a k8s resource (i.e. not a container) + if (item.metadata) { + this.applyAnalysis(resource); + } return resource; }), publishReplay(1), @@ -123,4 +139,14 @@ export class KubernetesResourceViewerComponent implements PreviewableComponent { return this.kubeEndpointService.kubeGuid || res.endpointId || res.metadata.kubeId; } + private applyAnalysis(resource) { + let id = (resource.kind || 'pod').toLowerCase(); + id = `${id}/${resource.raw.metadata.namespace}/${resource.raw.metadata.name}`; + if (this.analysis && this.analysis.alerts[id]) { + this.alerts = this.analysis.alerts[id]; + } else { + this.alerts = null; + } + } + } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts index a24a892eb2..fcd9c0ba5f 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes-tab-base/kubernetes-tab-base.component.ts @@ -7,6 +7,7 @@ import { FavoritesConfigMapper } from '../../../../../store/src/favorite-config- import { UserFavoriteEndpoint } from '../../../../../store/src/types/user-favorites.types'; import { BaseKubeGuid } from '../kubernetes-page.types'; import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; +import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service'; import { KubernetesService } from '../services/kubernetes.service'; @Component({ @@ -27,16 +28,12 @@ import { KubernetesService } from '../services/kubernetes.service'; }, KubernetesService, KubernetesEndpointService, + KubernetesAnalysisService, ] }) export class KubernetesTabBaseComponent implements OnInit { - tabLinks = [ - { link: 'summary', label: 'Summary', icon: 'kubernetes', iconFont: 'stratos-icons' }, - { link: 'nodes', label: 'Nodes', icon: 'node', iconFont: 'stratos-icons' }, - { link: 'namespaces', label: 'Namespaces', icon: 'namespace', iconFont: 'stratos-icons' }, - { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, - ]; + tabLinks = []; public isFetching$: Observable; public favorite$: Observable; @@ -44,7 +41,19 @@ export class KubernetesTabBaseComponent implements OnInit { constructor( public kubeEndpointService: KubernetesEndpointService, - public favoritesConfigMapper: FavoritesConfigMapper) { } + public favoritesConfigMapper: FavoritesConfigMapper, + public analysisService: KubernetesAnalysisService, + ) { + this.tabLinks = [ + { link: 'summary', label: 'Summary', icon: 'kubernetes', iconFont: 'stratos-icons' }, + { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ }, + { link: '-', label: 'Cluster' }, + { link: 'nodes', label: 'Nodes', icon: 'node', iconFont: 'stratos-icons' }, + { link: 'namespaces', label: 'Namespaces', icon: 'namespace', iconFont: 'stratos-icons' }, + { link: '-', label: 'Resources' }, + { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, + ]; + } ngOnInit() { this.isFetching$ = this.kubeEndpointService.endpoint$.pipe( diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.module.ts index faa30bd980..9d5c1db436 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.module.ts @@ -4,10 +4,31 @@ import { NgxChartsModule } from '@swimlane/ngx-charts'; import { CoreModule } from '../../../../core/src/core/core.module'; import { SharedModule } from '../../../../core/src/shared/shared.module'; +import { + AnalysisReportRunnerComponent, +} from './analysis-report-viewer/analysis-report-runner/analysis-report-runner.component'; +import { + AnalysisReportSelectorComponent, +} from './analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; +import { AnalysisReportViewerComponent } from './analysis-report-viewer/analysis-report-viewer.component'; +import { + KubeScoreReportViewerComponent, +} from './analysis-report-viewer/kube-score-report-viewer/kube-score-report-viewer.component'; +import { PopeyeReportViewerComponent } from './analysis-report-viewer/popeye-report-viewer/popeye-report-viewer.component'; +import { + ResourceAlertPreviewComponent, +} from './analysis-report-viewer/resource-alert-preview/resource-alert-preview.component'; +import { + ResourceAlertViewComponent, +} from './analysis-report-viewer/resource-alert-preview/resource-alert-view/resource-alert-view.component'; +import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; import { KubedashConfigurationComponent, } from './kubernetes-dashboard/kubedash-configuration/kubedash-configuration.component'; import { KubernetesDashboardTabComponent } from './kubernetes-dashboard/kubernetes-dashboard.component'; +import { + KubernetesNamespaceAnalysisReportComponent, +} from './kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component'; import { KubernetesNamespacePodsComponent, } from './kubernetes-namespace/kubernetes-namespace-pods/kubernetes-namespace-pods.component'; @@ -32,6 +53,7 @@ import { KubernetesResourceViewerComponent } from './kubernetes-resource-viewer/ import { KubernetesTabBaseComponent } from './kubernetes-tab-base/kubernetes-tab-base.component'; import { KubernetesRoutingModule } from './kubernetes.routing'; import { KubernetesComponent } from './kubernetes/kubernetes.component'; +import { AnalysisStatusCellComponent } from './list-types/analysis-status-cell/analysis-status-cell.component'; import { KubernetesLabelsCellComponent } from './list-types/kubernetes-labels-cell/kubernetes-labels-cell.component'; import { KubeNamespacePodCountComponent, @@ -87,14 +109,22 @@ import { PodMetricsComponent } from './pod-metrics/pod-metrics.component'; import { KubernetesEndpointService } from './services/kubernetes-endpoint.service'; import { KubernetesNodeService } from './services/kubernetes-node.service'; import { KubernetesService } from './services/kubernetes.service'; +import { + AnalysisInfoCardComponent, +} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component'; +import { + KubernetesAnalysisInfoComponent, +} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component'; +import { + KubernetesAnalysisReportComponent, +} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component'; +import { KubernetesAnalysisTabComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component'; import { KubernetesNamespacesTabComponent } from './tabs/kubernetes-namespaces-tab/kubernetes-namespaces-tab.component'; import { KubernetesNodesTabComponent } from './tabs/kubernetes-nodes-tab/kubernetes-nodes-tab.component'; import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernetes-pods-tab.component'; import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component'; -import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; /* tslint:disable:max-line-length */ - /* tslint:enable */ @NgModule({ @@ -115,6 +145,7 @@ import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; KubernetesNamespacesTabComponent, KubernetesDashboardTabComponent, KubernetesSummaryTabComponent, + KubernetesAnalysisTabComponent, PodMetricsComponent, KubernetesNodeLinkComponent, KubernetesNodeIpsComponent, @@ -149,6 +180,18 @@ import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; KubeServiceCardComponent, KubedashConfigurationComponent, KubernetesPodContainersComponent, + KubernetesAnalysisReportComponent, + KubernetesAnalysisInfoComponent, + AnalysisInfoCardComponent, + AnalysisReportViewerComponent, + PopeyeReportViewerComponent, + AnalysisReportSelectorComponent, + AnalysisReportRunnerComponent, + ResourceAlertPreviewComponent, + ResourceAlertViewComponent, + KubeScoreReportViewerComponent, + AnalysisStatusCellComponent, + KubernetesNamespaceAnalysisReportComponent, ], providers: [ KubernetesService, @@ -173,9 +216,22 @@ import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; KubeServiceCardComponent, KubernetesResourceViewerComponent, KubernetesPodContainersComponent, + PopeyeReportViewerComponent, + KubeScoreReportViewerComponent, + AnalysisReportSelectorComponent, + ResourceAlertPreviewComponent, + AnalysisStatusCellComponent, ], exports: [ - KubernetesResourceViewerComponent + KubernetesResourceViewerComponent, + AnalysisReportViewerComponent, + PopeyeReportViewerComponent, + KubeScoreReportViewerComponent, + AnalysisReportSelectorComponent, + AnalysisReportRunnerComponent, + ResourceAlertPreviewComponent, + ResourceAlertViewComponent, + AnalysisStatusCellComponent, ] }) export class KubernetesModule { } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.routing.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.routing.ts index c7681161ce..35c15386f8 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.routing.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.routing.ts @@ -1,3 +1,4 @@ +import { KubernetesNamespaceAnalysisReportComponent } from './kubernetes-namespace/kubernetes-namespace-analysis-report/kubernetes-namespace-analysis-report.component'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; @@ -24,6 +25,11 @@ import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernete import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component'; import { KubedashConfigurationComponent } from './kubernetes-dashboard/kubedash-configuration/kubedash-configuration.component'; import { KubeConsoleComponent } from './kube-terminal/kube-console.component'; +import { KubernetesAnalysisTabComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component'; +import { KubernetesAnalysisReportComponent } from './tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component'; +import { + KubernetesAnalysisInfoComponent +} from './tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component'; const kubernetes: Routes = [{ path: '', @@ -81,7 +87,10 @@ const kubernetes: Routes = [{ path: 'services', component: KubernetesNamespaceServicesComponent }, - + { + path: 'analysis', + component: KubernetesNamespaceAnalysisReportComponent + } ] }, { @@ -109,6 +118,18 @@ const kubernetes: Routes = [{ path: 'pods', component: KubernetesPodsTabComponent }, + { + path: 'analysis', + component: KubernetesAnalysisTabComponent + }, + { + path: 'analysis/report/:id', + component: KubernetesAnalysisReportComponent + }, + { + path: 'analysis/info', + component: KubernetesAnalysisInfoComponent + }, ] }, { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.store.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.store.module.ts index f0a76a9017..4b37169198 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.store.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/kubernetes.store.module.ts @@ -1,12 +1,14 @@ import { NgModule } from '@angular/core'; import { EffectsModule } from '@ngrx/effects'; +import { AnalysisEffects } from './store/analysis.effects'; import { KubernetesEffects } from './store/kubernetes.effects'; @NgModule({ imports: [ EffectsModule.forFeature([ - KubernetesEffects + AnalysisEffects, + KubernetesEffects, ]) ] }) diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-config.service.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-config.service.ts new file mode 100644 index 0000000000..9e68483e80 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-config.service.ts @@ -0,0 +1,123 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { ITableColumn } from 'frontend/packages/core/src/shared/components/list/list-table/table.types'; +import { + IListAction, + IListConfig, + IListMultiFilterConfig, + ListViewTypes, +} from 'frontend/packages/core/src/shared/components/list/list.component.types'; +import * as moment from 'moment'; +import { of } from 'rxjs'; + +import { ListView } from '../../../../../store/src/actions/list.actions'; +import { AppState } from '../../../../../store/src/app-state'; +import { defaultHelmKubeListPageSize } from '../../kubernetes/list-types/kube-helm-list-types'; +import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; +import { KubernetesAnalysisService } from '../services/kubernetes.analysis.service'; +import { AnalysisReport } from '../store/kube.types'; +import { AnalysisReportsDataSource } from './analysis-reports-list-source'; +import { AnalysisStatusCellComponent } from './analysis-status-cell/analysis-status-cell.component'; + +@Injectable() +export class AnalysisReportsListConfig implements IListConfig { + AppsDataSource: AnalysisReportsDataSource; + isLocal = true; + multiFilterConfigs: IListMultiFilterConfig[]; + + guid: string; + + columns: Array> = [ + { + columnId: 'name', headerCell: () => 'Name', + cellDefinition: { + getValue: (row: AnalysisReport) => row.name, + getLink: row => row.status === 'completed' ? `/kubernetes/${this.guid}/analysis/report/${row.id}` : null + }, + sort: { + type: 'sort', + orderKey: 'name', + field: 'name' + }, + cellFlex: '2', + }, + { + columnId: 'type', + headerCell: () => 'Type', + cellDefinition: { + getValue: (row: AnalysisReport) => row.type.charAt(0).toUpperCase() + row.type.substring(1) + }, + sort: { + type: 'sort', + orderKey: 'type', + field: 'type' + }, + cellFlex: '1' + }, + { + columnId: 'age', + headerCell: () => 'Age', + cellDefinition: { + getValue: (row: AnalysisReport) => { + return moment(row.created).fromNow(true); + } + }, + sort: { + type: 'sort', + orderKey: 'age', + field: 'created' + }, + cellFlex: '1' + }, + { + columnId: 'status', + headerCell: () => 'Status', + cellComponent: AnalysisStatusCellComponent, + sort: { + type: 'sort', + orderKey: 'status', + field: 'status' + }, + cellFlex: '1' + } + ]; + + pageSizeOptions = defaultHelmKubeListPageSize; + viewType = ListViewTypes.TABLE_ONLY; + defaultView = 'table' as ListView; + + enableTextFilter = true; + text = { + filter: 'Filter by Name', + noEntries: 'There are no Analysis Reports' + }; + + constructor( + store: Store, + kubeEndpointService: KubernetesEndpointService, + private analysisService: KubernetesAnalysisService, + ngZone: NgZone, + ) { + this.guid = kubeEndpointService.baseKube.guid; + this.AppsDataSource = new AnalysisReportsDataSource(store, this, kubeEndpointService, ngZone); + } + + private listActionDelete: IListAction = { + action: (item) => this.analysisService.delete(item.endpoint, item), + label: 'Delete', + icon: 'delete', + description: ``, + createEnabled: row$ => of(true) + }; + + private singleActions = [ + this.listActionDelete, + ]; + + getGlobalActions = () => []; + getMultiActions = () => []; + getSingleActions = () => this.singleActions; + getColumns = () => this.columns; + getDataSource = () => this.AppsDataSource; + getMultiFiltersConfigs = () => []; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-source.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-source.ts new file mode 100644 index 0000000000..6ccd5e3d98 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-reports-list-source.ts @@ -0,0 +1,63 @@ +import { NgZone } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { safeUnsubscribe } from 'frontend/packages/core/src/core/utils.service'; +import { ListDataSource } from 'frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source'; +import { IListConfig } from 'frontend/packages/core/src/shared/components/list/list.component.types'; +import { interval, Subscription } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { AppState } from '../../../../../store/src/app-state'; +import { isFetchingPage } from '../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; +import { kubeEntityCatalog } from '../kubernetes-entity-catalog'; +import { KubernetesEndpointService } from '../services/kubernetes-endpoint.service'; +import { GetAnalysisReports } from '../store/anaylsis.actions'; +import { AnalysisReport } from '../store/kube.types'; + +export class AnalysisReportsDataSource extends ListDataSource { + + + private analysisAction: GetAnalysisReports; + private pollInterval: Subscription; + + constructor( + store: Store, + listConfig: IListConfig, + endpointService: KubernetesEndpointService, + ngZone: NgZone, + ) { + const action = kubeEntityCatalog.analysisReport.actions.getMultiple(endpointService.baseKube.guid); + super({ + store, + action, + schema: action.entity[0], + getRowUniqueId: (entity: AnalysisReport) => action.entity[0].getId(entity), + paginationKey: action.paginationKey, + isLocal: true, + transformEntities: [{ type: 'filter', field: 'name' }], + listConfig, + }); + this.analysisAction = action; + + this.startPoll(store, ngZone); + } + + destroy() { + safeUnsubscribe(this.pollInterval); + } + + private startPoll(store: Store, ngZone: NgZone) { + ngZone.runOutsideAngular(() => this.pollInterval = interval(5000).subscribe(() => this.poll(store, ngZone))); + } + private poll(store: Store, ngZone: NgZone) { + kubeEntityCatalog.analysisReport.store.getPaginationMonitor(this.analysisAction.kubeGuid).pagination$.pipe( + first(), + map(isFetchingPage) + ).subscribe(isFetchingPage => { + if (!isFetchingPage) { + ngZone.run(() => { + store.dispatch(this.analysisAction); + }); + } + }) + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html new file mode 100644 index 0000000000..6ff4563a31 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.html @@ -0,0 +1,8 @@ +
+ Running +
+
Completed
+
+
Error
+ warning +
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss new file mode 100644 index 0000000000..27dcfcf08b --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.scss @@ -0,0 +1,22 @@ +.status { + &__running { + align-items: center; + display: flex; + + &> mat-progress-spinner { + margin-right: 8px; + } + } + &__error { + align-items: center; + display: flex; + + mat-icon { + cursor: help; + font-size: 20px; + height: 20px; + margin-left: 8px; + width: 20px; + } + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts new file mode 100644 index 0000000000..3c1fb10915 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.spec.ts @@ -0,0 +1,32 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MDAppModule } from './../../../../core/md.module'; +import { AnalysisStatusCellComponent } from './analysis-status-cell.component'; +import { + AnalysisReportSelectorComponent +} from './../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; + +describe('AnalysisStatusCellComponent', () => { + let component: AnalysisStatusCellComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AnalysisStatusCellComponent, AnalysisReportSelectorComponent ], + imports: [ + MDAppModule, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalysisStatusCellComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts new file mode 100644 index 0000000000..acc74ee6d6 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/analysis-status-cell/analysis-status-cell.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TableCellCustom } from 'frontend/packages/core/src/shared/components/list/list.types'; + +@Component({ + selector: 'app-analysis-status-cell', + templateUrl: './analysis-status-cell.component.html', + styleUrls: ['./analysis-status-cell.component.scss'] +}) +export class AnalysisStatusCellComponent extends TableCellCustom { + + constructor() { + super(); + this.row = {}; + } + + } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts index e70ac26fdf..68f0f28e01 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/list-types/kubernetes-labels-cell/kubernetes-labels-cell.component.spec.ts @@ -1,8 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BaseTestModules } from '../../../../../../core/test-framework/core-test.helper'; -import { KubernetesStatus } from '../../store/kube.types'; import { KubernetesLabelsCellComponent } from './kubernetes-labels-cell.component'; +import { KubernetesStatus } from '../../store/kube.types'; describe('KubernetesLabelsCellComponent', () => { let component: KubernetesLabelsCellComponent; diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/analysis-report.types.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/analysis-report.types.ts new file mode 100644 index 0000000000..620640c6c9 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/analysis-report.types.ts @@ -0,0 +1,22 @@ +export enum ResourceAlertLevel { + OK = 0, + Info, + Warning, + Error, + Unknown, +} + +// We re-map an analysis reprot into a map of resource alerts that is better for us +// to overlay in the UI to show issues from reports +export interface ResourceAlert { + apiVersion?: string; + kind: string; + message: string; + namespace: string; + name: string; + level: ResourceAlertLevel; +} + +export interface ResourceAlertMap { + [key: string]: ResourceAlert[]; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.analysis.service.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.analysis.service.ts new file mode 100644 index 0000000000..14749dc1c8 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.analysis.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { combineLatest, Observable } from 'rxjs'; +import { filter, first, map, pairwise, startWith, tap } from 'rxjs/operators'; + +import { SnackBarService } from '../../../../../core/src/shared/services/snackbar.service'; +import { ResetPaginationOfType } from '../../../../../store/src/actions/pagination.actions'; +import { AppState } from '../../../../../store/src/app-state'; +import { ListActionState, RequestInfoState } from '../../../../../store/src/reducers/api-request-reducer/types'; +import { kubeEntityCatalog } from '../kubernetes-entity-catalog'; +import { GetAnalysisReports } from '../store/anaylsis.actions'; +import { AnalysisReport } from '../store/kube.types'; +import { getHelmReleaseDetailsFromGuid } from '../workloads/store/workloads-entity-factory'; +import { KubernetesEndpointService } from './kubernetes-endpoint.service'; + +export interface KubernetesAnalysisType { + name: string; + id: string; + namespaceAware: boolean; + iconUrl?: string; + descriptionUrl?: string; +} + +@Injectable() +export class KubernetesAnalysisService { + kubeGuid: string; + + public analyzers$: Observable; + public namespaceAnalyzers$: Observable; + + public enabled$: Observable; + public hideAnalysis$: Observable; + + private action: GetAnalysisReports; + + constructor( + public kubeEndpointService: KubernetesEndpointService, + public activatedRoute: ActivatedRoute, + public store: Store, + private snackbarService: SnackBarService + ) { + this.kubeGuid = kubeEndpointService.kubeGuid || getHelmReleaseDetailsFromGuid(activatedRoute.snapshot.params.guid).endpointId; + + // Is the backend plugin available? + this.enabled$ = this.store.select('auth').pipe( + map(auth => auth.sessionData.plugins && auth.sessionData.plugins.analysis) + ); + + this.hideAnalysis$ = this.enabled$.pipe( + map(enabled => !enabled), + startWith(true), + ) + + const allEngines = { + popeye: + { + name: 'PopEye', + id: 'popeye', + namespaceAware: true, + // iconUrl: '/core/assets/custom/popeye.png', + // iconWidth: '80', + descriptionUrl: '/core/assets/custom/popeye.md' + }, + 'kube-score': + { + name: 'Kube Score', + id: 'kube-score', + namespaceAware: true, + // iconUrl: '/core/assets/custom/kubescore.png', + // iconWidth: '120', + descriptionUrl: '/core/assets/custom/kubescore.md' + } + // { + // name: 'Sonobuoy', + // id: 'sonobuoy', + // namespaceAware: false, + // iconUrl: '/core/assets/custom/sonobuoy.png', + // iconWidth: '70', + // descriptionUrl: '/core/assets/custom/sonobuoy.md' + // } + }; + + // Determine which analyzers are enabled + this.analyzers$ = this.store.select('auth').pipe( + filter(auth => !!auth.sessionData['plugin-config']), + map(auth => auth.sessionData['plugin-config'].analysisEngines), + map(engines => engines.split(',').map(e => allEngines[e.trim()]).filter(e => !!e)) + ); + + this.namespaceAnalyzers$ = combineLatest( + this.analyzers$, + this.enabled$ + ).pipe( + map(([a, enabled]) => { + if (!enabled) { + return null; + } + return a.filter(v => v.namespaceAware); + }) + ); + + this.action = kubeEntityCatalog.analysisReport.actions.getMultiple(this.kubeGuid) + } + + public delete(endpointID: string, item: { id: string }) { + return kubeEntityCatalog.analysisReport.api.delete(endpointID, item.id); + } + + public refresh() { + this.store.dispatch(new ResetPaginationOfType(this.action)); + } + + public run(id: string, endpointID: string, namespace?: string, app?: string): Observable { + const obs$ = kubeEntityCatalog.analysisReport.api.run(endpointID, id, namespace, app).pipe( + pairwise(), + filter(([oldE, newE]) => oldE.creating && !newE.creating), + map(([, newE]) => newE), + first() + ) + obs$.subscribe(() => { + const type = id.charAt(0).toUpperCase() + id.substring(1); + let msg; + if (app) { + msg = `${type} analysis started for workload '${app}'`; + } else if (namespace) { + msg = `${type} analysis started for namespace '${namespace}'`; + } else { + msg = `${type} analysis started for the Kubernetes cluster`; + } + this.snackbarService.showReturn(msg, ['kubernetes', endpointID, 'analysis'], 'View', 5000); + this.refresh(); + }); + return obs$; + } + + public getByID(endpoint: string, id: string, refresh = false): Observable { + if (refresh) { + kubeEntityCatalog.analysisReport.api.getById(endpoint, id) + } + + const entityService = kubeEntityCatalog.analysisReport.store.getById.getEntityService(endpoint, id); + return entityService.waitForEntity$.pipe( + map(e => e.entity), + tap(entity => { + if (!refresh && !entity.report) { + kubeEntityCatalog.analysisReport.api.getById(endpoint, id); + refresh = true; + } + }), + filter(entity => !!entity.report) + ); + } + + public getByPath(endpointID: string, path: string, refresh = false): Observable { + if (refresh) { + kubeEntityCatalog.analysisReport.api.getByPath(endpointID, path) + } + return kubeEntityCatalog.analysisReport.store.getByPath.getPaginationService(endpointID, path).entities$.pipe( + filter(entities => !!entities) + ); + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.service.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.service.ts index 678b9eceed..09c03d154e 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubernetes.service.ts @@ -2,11 +2,11 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map, shareReplay } from 'rxjs/operators'; -import { endpointEntityType, stratosEntityFactory } from '../../../../../store/src/helpers/stratos-entity-factory'; import { PaginationMonitor } from '../../../../../store/src/monitors/pagination-monitor'; -import { PaginationMonitorFactory } from '../../../../../store/src/monitors/pagination-monitor.factory'; +import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog'; import { APIResource, EntityInfo } from '../../../../../store/src/types/api.types'; -import { endpointListKey, EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { KUBERNETES_ENDPOINT_TYPE } from '../kubernetes-entity-factory'; @Injectable() export class KubernetesService { @@ -14,18 +14,11 @@ export class KubernetesService { kubeEndpointsMonitor: PaginationMonitor; waitForAppEntity$: Observable>; - constructor( - private paginationMonitorFactory: PaginationMonitorFactory - ) { - // TODO: RC update with stratos entity catalog - this.kubeEndpointsMonitor = this.paginationMonitorFactory.create( - endpointListKey, - stratosEntityFactory(endpointEntityType), - true - ); + constructor() { + this.kubeEndpointsMonitor = stratosEntityCatalog.endpoint.store.getAll.getPaginationMonitor() this.kubeEndpoints$ = this.kubeEndpointsMonitor.currentPage$.pipe( - map(endpoints => endpoints.filter(e => e.cnsi_type === 'k8s')), + map(endpoints => endpoints.filter(e => e.cnsi_type === KUBERNETES_ENDPOINT_TYPE)), shareReplay(1) ); } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubescore-report.helper.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubescore-report.helper.ts new file mode 100644 index 0000000000..b07c0e7abf --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/kubescore-report.helper.ts @@ -0,0 +1,57 @@ +import { ResourceAlert, ResourceAlertLevel, ResourceAlertMap } from './analysis-report.types'; + +export class KubeScoreReportHelper { + + constructor(public report: any) {} + + public map() { + if (!this.report.report) { + return; + } + + const kubescore = this.report.report; + // Go through the report and re-map + const result = {} as ResourceAlertMap; + + Object.keys(kubescore).forEach(key => { + const item = kubescore[key]; + let id = item.TypeMeta.kind.toLowerCase(); + id = `${id}/${item.ObjectMeta.namespace}/${item.ObjectMeta.name}`; + + item.Checks.forEach(check => { + if (check.Grade !== 10 && !check.Skipped) { + // Add an alert for each comment + check.Comments.forEach(comment => { + // Include this comment + const alert = { + kind: item.TypeMeta.kind.toLowerCase(), + namespace: item.ObjectMeta.namespace, + name: item.ObjectMeta.name, + message: comment.Summary, + level: this.convertMessageLevel(check.Grade) + } as ResourceAlert; + if (!result[id]) { + result[id] = [] as ResourceAlert[]; + } + result[id].push(alert); + }); + } + }); + }); + this.report.alerts = result; + } + private convertMessageLevel(level: number): ResourceAlertLevel { + switch (level) { + case 10: + return ResourceAlertLevel.OK; + case 7: + return ResourceAlertLevel.Info; + case 5: + return ResourceAlertLevel.Warning; + case 1: + return ResourceAlertLevel.Error; + default: + return ResourceAlertLevel.Unknown; + } + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/popeye-report.helper.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/popeye-report.helper.ts new file mode 100644 index 0000000000..12de4c61c1 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/popeye-report.helper.ts @@ -0,0 +1,69 @@ +import { ResourceAlert, ResourceAlertLevel, ResourceAlertMap } from './analysis-report.types'; + +export class PopeyeReportHelper { + + constructor(public report: any) { } + + // Map the report to the alert format + public map() { + if (!this.report.report || !this.report.report.popeye) { + return; + } + + const popeye = this.report.report.popeye; + // Go through the report and re-map + const result = {} as ResourceAlertMap; + popeye.sanitizers.forEach(s => { + // We just care about issues + const resourceType = s.sanitizer; + if (s.issues) { + Object.keys(s.issues).forEach(resourcePath => { + const issues = s.issues[resourcePath]; + issues.forEach(issue => { + // Level must be greater than 0 (OK) + if (issue.level > 0) { + let namespace; + let name; + if (resourcePath.indexOf('/') !== -1) { + // Has a namespace + namespace = resourcePath.split('/')[0]; + name = resourcePath.split('/')[1]; + } else { + name = resourcePath; + namespace = ''; + } + const alert = { + kind: resourceType, + namespace, + name, + message: issue.message, + level: this.convertMessageLevel(issue.level) + } as ResourceAlert; + const id = `${resourceType}/${resourcePath}`; + if (!result[id]) { + result[id] = [] as ResourceAlert[]; + } + result[id].push(alert); + } + }); + }); + } + }); + + this.report.alerts = result; + } + private convertMessageLevel(level: number): ResourceAlertLevel { + switch (level) { + case 0: + return ResourceAlertLevel.OK; + case 1: + return ResourceAlertLevel.Info; + case 2: + return ResourceAlertLevel.Warning; + case 3: + return ResourceAlertLevel.Error; + default: + return ResourceAlertLevel.Unknown; + } + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/route.helper.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/route.helper.ts new file mode 100644 index 0000000000..ccbb8f7bc1 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/services/route.helper.ts @@ -0,0 +1,11 @@ +import { ActivatedRoute } from '@angular/router'; + +export function getParentURL(route: ActivatedRoute, removeLastParts = 1): string { + const reducer = (a: string, v) => { + const p = v.url.join('/'); + return p.length > 0 ? `${a}/${p}` : a; + }; + let res = route.snapshot.pathFromRoot.reduce(reducer, '').split('/'); + res.splice(-removeLastParts, removeLastParts); + return res.join('/'); +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/action-builders/kube.action-builders.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/action-builders/kube.action-builders.ts index 58ad75827c..362c418497 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/action-builders/kube.action-builders.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/action-builders/kube.action-builders.ts @@ -2,6 +2,13 @@ import { OrchestratedActionBuilders, } from '../../../../../../store/src/entity-catalog/action-orchestrator/action-orchestrator'; import { GetHelmReleasePods, GetHelmReleaseServices } from '../../workloads/store/workloads.actions'; +import { + DeleteAnalysisReport, + GetAnalysisReportById, + GetAnalysisReports, + GetAnalysisReportsByPath, + RunAnalysisReport, +} from '../anaylsis.actions'; import { CreateKubernetesNamespace, GeKubernetesDeployments, @@ -145,3 +152,35 @@ export interface KubeDashboardActionBuilders extends OrchestratedActionBuilders export const kubeDashboardActionBuilders: KubeDashboardActionBuilders = { get: (kubeGuid: string) => new GetKubernetesDashboard(kubeGuid) } + +export interface AnalysisReportsActionBuilders extends OrchestratedActionBuilders { + getMultiple: ( + kubeGuid: string + ) => GetAnalysisReports; + getById: ( + kubeGuid: string, + id: string, + ) => GetAnalysisReportById; + getByPath: ( + kubeGuid: string, + path: string, + ) => GetAnalysisReportsByPath; + delete: ( + kubeGuid: string, + id: string, + ) => DeleteAnalysisReport; + run: ( + kubeGuid: string, + id: string, + namespace?: string, + app?: string + ) => RunAnalysisReport; +} + +export const analysisReportsActionBuilders: AnalysisReportsActionBuilders = { + getMultiple: (kubeGuid: string) => new GetAnalysisReports(kubeGuid), + getById: (kubeGuid: string, id: string) => new GetAnalysisReportById(kubeGuid, id), + getByPath: (kubeGuid: string, path: string) => new GetAnalysisReportsByPath(kubeGuid, path), + delete: (kubeGuid: string, id: string) => new DeleteAnalysisReport(kubeGuid, id), + run: (kubeGuid: string, id: string, namespace?: string, app?: string) => new RunAnalysisReport(kubeGuid, id, namespace, app) +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/analysis.effects.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/analysis.effects.ts new file mode 100644 index 0000000000..4f8a7d24c2 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/analysis.effects.ts @@ -0,0 +1,264 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { catchError, flatMap, mergeMap } from 'rxjs/operators'; + +import { environment } from '../../../../../core/src/environments/environment'; +import { AppState } from '../../../../../store/src/app-state'; +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; +import { ApiRequestTypes } from '../../../../../store/src/reducers/api-request-reducer/request-helpers'; +import { NormalizedResponse } from '../../../../../store/src/types/api.types'; +import { + StartRequestAction, + WrapperRequestActionFailed, + WrapperRequestActionSuccess, +} from '../../../../../store/src/types/request.types'; +import { KubeScoreReportHelper } from '../services/kubescore-report.helper'; +import { PopeyeReportHelper } from '../services/popeye-report.helper'; +import { + DELETE_ANALYSIS_REPORT_TYPES, + DeleteAnalysisReport, + GET_ANALYSIS_REPORT_BY_ID_TYPES, + GET_ANALYSIS_REPORTS_BY_PATH_TYPES, + GET_ANALYSIS_REPORTS_TYPES, + GetAnalysisReportById, + GetAnalysisReports, + GetAnalysisReportsByPath, + RUN_ANALYSIS_REPORT_TYPES, + RunAnalysisReport, +} from './anaylsis.actions'; +import { AnalysisReport } from './kube.types'; + +@Injectable() +export class AnalysisEffects { + proxyAPIVersion = environment.proxyAPIVersion; + + constructor( + private http: HttpClient, + private actions$: Actions, + private store: Store, + ) { } + + @Effect() + fetchAnalysisReports$ = this.actions$.pipe( + ofType(GET_ANALYSIS_REPORTS_TYPES[0]), + flatMap(action => { + this.store.dispatch(new StartRequestAction(action)); + const headers = new HttpHeaders({}); + const requestArgs = { + headers + }; + const url = `/pp/${this.proxyAPIVersion}/analysis/reports/${action.kubeGuid}`; + const entityKey = entityCatalog.getEntityKey(action); + return this.http.get(url, requestArgs).pipe( + mergeMap(response => { + const res: NormalizedResponse = { + entities: { [entityKey]: {} }, + result: [] + }; + const items: any = response as Array; + items.forEach(item => { + const id = item.id; + res.entities[entityKey][id] = item; + res.result.push(id); + }); + return [new WrapperRequestActionSuccess(res, action)]; + }), + catchError(error => [ + new WrapperRequestActionFailed(error.message, action, 'fetch', { + endpointIds: [action.kubeGuid], + url: error.url || url, + eventCode: error.status ? error.status + '' : '500', + message: 'Kubernetes Analysis Report request error', + error + }) + ]) + ); + }) + ); + + @Effect() + fetchAnalysisReportById$ = this.actions$.pipe( + ofType(GET_ANALYSIS_REPORT_BY_ID_TYPES[0]), + flatMap(action => { + this.store.dispatch(new StartRequestAction(action)); + + const url = `/pp/${this.proxyAPIVersion}/analysis/reports/${action.kubeGuid}/${action.guid}`; + const headers = new HttpHeaders({}); + const requestArgs = { + headers, + }; + const entityKey = entityCatalog.getEntityKey(action); + + return this.http.get(url, requestArgs).pipe( + mergeMap(response => { + this.processReport(response); + + const res: NormalizedResponse = { + entities: { + [entityKey]: { + [action.guid]: response + } + }, + result: [action.guid] + }; + return [new WrapperRequestActionSuccess(res, action)]; + }), + catchError(error => [ + new WrapperRequestActionFailed(error.message, action, 'fetch', { + endpointIds: [action.kubeGuid], + url: error.url || url, + eventCode: error.status ? error.status + '' : '500', + message: 'Kubernetes Analysis Report request error', + error + }) + ]) + ); + }) + ); + + @Effect() + fetchAnalysisReportByPath$ = this.actions$.pipe( + ofType(GET_ANALYSIS_REPORTS_BY_PATH_TYPES[0]), + flatMap(action => { + this.store.dispatch(new StartRequestAction(action)); + + const url = `/pp/${this.proxyAPIVersion}/analysis/completed/${action.kubeGuid}/${action.path}`; + const headers = new HttpHeaders({}); + const requestArgs = { + headers, + }; + const schema = action.entity[0]; + const entityKey = entityCatalog.getEntityKey(action); + return this.http.get(url, requestArgs).pipe( + mergeMap((response: AnalysisReport[]) => { + const res: NormalizedResponse = { + entities: { + [entityKey]: {} + }, + result: [] + }; + response.forEach(report => { + const guid = schema.getId(report); + res.entities[entityKey][guid] = report; + res.result.push(guid); + }) + return [new WrapperRequestActionSuccess(res, action)]; + }), + catchError(error => [ + new WrapperRequestActionFailed(error.message, action, 'fetch', { + endpointIds: [action.kubeGuid], + url: error.url || url, + eventCode: error.status ? error.status + '' : '500', + message: 'Kubernetes Analysis Report request error', + error + }) + ]) + ); + }) + ); + + @Effect() + deleteAnalysisReport$ = this.actions$.pipe( + ofType(DELETE_ANALYSIS_REPORT_TYPES[0]), + flatMap(action => { + const type: ApiRequestTypes = 'delete'; + + this.store.dispatch(new StartRequestAction(action, type)); + + const url = `/pp/${this.proxyAPIVersion}/analysis/reports`; + const headers = new HttpHeaders({}); + const requestArgs = { + headers, + body: [action.guid] + }; + + return this.http.delete(url, requestArgs).pipe( + mergeMap(() => { + const res: NormalizedResponse = { + entities: { [entityCatalog.getEntityKey(action)]: {} }, + result: [] + }; + return [new WrapperRequestActionSuccess(res, action, type)]; + }), + catchError(error => [ + new WrapperRequestActionFailed(error.message, action, type, { + endpointIds: [action.kubeGuid], + url: error.url || url, + eventCode: error.status ? error.status + '' : '500', + message: 'Kubernetes Analysis Report request error', + error + }) + ]) + ); + }) + ); + + @Effect() + runAnalysisReport$ = this.actions$.pipe( + ofType(RUN_ANALYSIS_REPORT_TYPES[0]), + flatMap(action => { + const type: ApiRequestTypes = 'create'; + + this.store.dispatch(new StartRequestAction(action, type)); + + const { namespace, app } = action; + const body = { + namespace, + app, + }; + + // Start an Analysis + const url = `/pp/${this.proxyAPIVersion}/analysis/run/${action.guid}/${action.kubeGuid}`; + const headers = new HttpHeaders({}); + const requestArgs = { + headers, + }; + + return this.http.post(url, body, requestArgs).pipe( + mergeMap((response: AnalysisReport) => { + const res: NormalizedResponse = { + entities: { [entityCatalog.getEntityKey(action)]: { [response.id]: response } }, + result: [response.id] + }; + return [new WrapperRequestActionSuccess(res, action, type)]; + }), + catchError(error => [ + new WrapperRequestActionFailed(error.message, action, type, { + endpointIds: [action.kubeGuid], + url: error.url || url, + eventCode: error.status ? error.status + '' : '500', + message: 'Kubernetes Analysis Report request error', + error + }) + ]) + ); + + }) + ); + + + private processReport(report: any) { + // Check the path of the report + if (report.path.split('/').length !== 2) { + return; + } + + switch (report.format) { + case 'popeye': + const helper = new PopeyeReportHelper(report); + helper.map(); + break; + case 'kubescore': + const kubeScoreHelper = new KubeScoreReportHelper(report); + kubeScoreHelper.map(); + break; + default: + console.warn('Do not know how to handle this report type: ', report.format); + break; + } + } + + +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/anaylsis.actions.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/anaylsis.actions.ts new file mode 100644 index 0000000000..e9da8d7a27 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/anaylsis.actions.ts @@ -0,0 +1,69 @@ +import { getActions } from '../../../../../store/src/actions/action.helper'; +import { PaginatedAction } from '../../../../../store/src/types/pagination.types'; +import { analysisReportEntityType, KUBERNETES_ENDPOINT_TYPE, kubernetesEntityFactory } from '../kubernetes-entity-factory'; +import { KubeAction, KubePaginationAction, KubeSingleEntityAction } from './kubernetes.actions'; + +export const GET_ANALYSIS_REPORTS_TYPES = getActions('ANALYSIS', 'Get reports'); +export const GET_ANALYSIS_REPORT_BY_ID_TYPES = getActions('ANALYSIS', 'Get report by id'); +export const GET_ANALYSIS_REPORTS_BY_PATH_TYPES = getActions('ANALYSIS', 'Get report by path'); +export const DELETE_ANALYSIS_REPORT_TYPES = getActions('ANALYSIS', 'Delete report'); +export const RUN_ANALYSIS_REPORT_TYPES = getActions('ANALYSIS', 'Run'); + +abstract class AnalysisAction implements KubeAction { + constructor(public kubeGuid: string, public actions: string[]) { + this.type = this.actions[0]; + } + endpointType = KUBERNETES_ENDPOINT_TYPE; + entityType = analysisReportEntityType; + entity = [kubernetesEntityFactory(analysisReportEntityType)]; + type: string; +} +abstract class AnalysisPaginationAction extends AnalysisAction implements KubePaginationAction, PaginatedAction { + flattenPagination = true; + constructor(kubeGuid: string, actionTypes: string[], public paginationKey: string) { + super(kubeGuid, actionTypes); + } +} +abstract class AnalysisSingleEntityAction extends AnalysisAction implements KubeSingleEntityAction { + constructor(kubeGuid: string, actionTypes: string[], public guid: string) { + super(kubeGuid, actionTypes); + } +} + + +/** + * Get the analysis reports for the given endpoint ID + */ +export class GetAnalysisReports extends AnalysisPaginationAction { + constructor(public kubeGuid: string) { + super(kubeGuid, GET_ANALYSIS_REPORTS_TYPES, kubeGuid) + } + initialParams = { + 'order-direction': 'asc', + 'order-direction-field': 'age', + }; +} + +export class GetAnalysisReportById extends AnalysisSingleEntityAction { + constructor(kubeGuid: string, id: string) { + super(kubeGuid, GET_ANALYSIS_REPORT_BY_ID_TYPES, id); + } +} + +export class GetAnalysisReportsByPath extends AnalysisPaginationAction { + constructor(kubeGuid: string, public path: string) { + super(kubeGuid, GET_ANALYSIS_REPORTS_BY_PATH_TYPES, path); + } +} + +export class DeleteAnalysisReport extends AnalysisSingleEntityAction { + constructor(kubeGuid: string, id: string) { + super(kubeGuid, DELETE_ANALYSIS_REPORT_TYPES, id); + } +} + +export class RunAnalysisReport extends AnalysisSingleEntityAction { + constructor(kubeGuid: string, id: string, public namespace?: string, public app?: string) { + super(kubeGuid, RUN_ANALYSIS_REPORT_TYPES, id); + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.getIds.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.getIds.ts index f08ff16fb3..898062b926 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.getIds.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.getIds.ts @@ -14,7 +14,7 @@ const deliminate = (...args: string[]) => args.join('_:_'); const debugMissingKubeId = (entity: BasicKubeAPIResource, func: (...args: string[]) => string, ...args: string[]) => { if (!environment.production && (!entity.metadata || !entity.metadata.kubeId)) { - console.log(`Kube entity does not have a kubeId, this is probably a bug: `, entity); + console.warn(`Kube entity does not have a kubeId, this is probably a bug: `, entity); } return func(...args); } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.types.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.types.ts index b6b2721e86..3a4a5e60b9 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.types.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kube.types.ts @@ -484,3 +484,30 @@ export interface HostPath { export interface Item { key: string; } + +export interface KubeStatus { + kind: string; + apiVersion: string; + metadata: Metadata; + status: string; + message: string; + reason: string; + details: {} + code: number +} + +// Analysis Reports + +export interface AnalysisReport { + id: string; + endpoint: string; + type: string; + name: string; + path: string; + created: Date; + read: boolean; + status: string; + duration: number; + report?: any; + title?: string; +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.actions.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.actions.ts index cec88d6f34..b35b07a92b 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.actions.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.actions.ts @@ -81,7 +81,6 @@ export const GET_KUBE_DASHBOARD = '[KUBERNETES Endpoint] Get K8S Dashboard Info' export const GET_KUBE_DASHBOARD_SUCCESS = '[KUBERNETES Endpoint] Get Dashboard Success'; export const GET_KUBE_DASHBOARD_FAILURE = '[KUBERNETES Endpoint] Get Dashboard Failure'; - const defaultSortParams = { 'order-direction': 'desc' as SortDirection, 'order-direction-field': 'name' @@ -371,5 +370,3 @@ export class FetchKubernetesChartMetricsAction extends MetricsChartAction { ); } } - - diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.effects.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.effects.ts index 5f47b4a890..e839283471 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.effects.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/store/kubernetes.effects.ts @@ -127,7 +127,7 @@ export class KubernetesEffects { endpointIds: [action.kubeGuid], url: error.url || url, eventCode: error.status ? error.status + '' : '500', - message: 'Kubernetes API request error', + message: 'Kubernetes Dashboard request error', error }) ])); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html new file mode 100644 index 0000000000..a695a022d0 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss new file mode 100644 index 0000000000..0f224e7f96 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.scss @@ -0,0 +1,21 @@ +.info { + + &__card { + display: flex; + } + + &__card-icon { + flex: 0 0 140px; + width: 140px; + &>img { + max-width: 120px; + } + margin-right: 10px; + text-align: center; + } + + &__card-text { + flex: 1; + } + +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts new file mode 100644 index 0000000000..88894b763d --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.spec.ts @@ -0,0 +1,29 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AnalysisInfoCardComponent } from './analysis-info-card.component'; + +describe('AnalysisInfoCardComponent', () => { + let component: AnalysisInfoCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AnalysisInfoCardComponent ], + imports: [ + HttpClientTestingModule, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AnalysisInfoCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.theme.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.theme.scss new file mode 100644 index 0000000000..06983eb900 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.theme.scss @@ -0,0 +1,21 @@ + +@mixin kube-analysis-card-theme($theme, $app-theme) { + + .info__card { + + P { + font-size: 14px; + line-height: 1.24em; + } + + h2 { + font-size: 18px; + padding: 0; + } + + h2 { + font-size: 16px; + padding: 0; + } + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts new file mode 100644 index 0000000000..56fcba9997 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/analysis-info-card/analysis-info-card.component.ts @@ -0,0 +1,55 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, Input } from '@angular/core'; +import * as markdown from 'marked'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +@Component({ + selector: 'app-analysis-info-card', + templateUrl: './analysis-info-card.component.html', + styleUrls: ['./analysis-info-card.component.scss'] +}) +export class AnalysisInfoCardComponent { + + public loading = true; + public content$: Observable; + private renderer = new markdown.Renderer(); + + public mAanalyzer = {}; + + @Input() set analyzer(analyzer: any) { + if (analyzer && analyzer.descriptionUrl) { + this.content$ = this.getDescription(analyzer.descriptionUrl); + } + this.mAanalyzer = analyzer; + } + + get analyzer() { + return this.mAanalyzer; + } + + constructor(private http: HttpClient) { + this.renderer.link = (href, title, text) => `${text}`; + this.renderer.code = (text: string) => `${text}`; + } + + private getDescription(url): Observable { + return this.http.get(url, { responseType: 'text' }).pipe( + map(resp => { + this.loading = false; + return markdown(resp, { + renderer: this.renderer + }); + }), + catchError((error) => { + this.loading = false; + if (error.status === 404) { + return of('

Unable to load description for this Analyzer

'); + } else { + return of('

An error occurred retrieving description for this Analyzer

'); + } + } + )); + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html new file mode 100644 index 0000000000..b88ffd84a2 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.html @@ -0,0 +1,7 @@ + +
+
+ +
+
+
diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss new file mode 100644 index 0000000000..53951f99ae --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.scss @@ -0,0 +1,5 @@ +.info__title { + padding: 0; + margin: 0 0 30px 0; + font-size: 22px; +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts new file mode 100644 index 0000000000..0a35b86689 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.spec.ts @@ -0,0 +1,38 @@ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesAnalysisInfoComponent } from './kubernetes-analysis-info.component'; +import { AnalysisInfoCardComponent } from './analysis-info-card/analysis-info-card.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service'; + +describe('KubernetesAnalysisInfoComponent', () => { + let component: KubernetesAnalysisInfoComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubernetesAnalysisInfoComponent, AnalysisInfoCardComponent ], + imports: [ + KubernetesBaseTestModules, + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubernetesAnalysisInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts new file mode 100644 index 0000000000..76cce96ccc --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-info/kubernetes-analysis-info.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { PreviewableComponent } from 'frontend/packages/core/src/shared/previewable-component'; +import { Observable } from 'rxjs'; + +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; + + +@Component({ + selector: 'app-kubernetes-analysis-info', + templateUrl: './kubernetes-analysis-info.component.html', + styleUrls: ['./kubernetes-analysis-info.component.scss'], + providers: [ + KubernetesAnalysisService + ] +}) +export class KubernetesAnalysisInfoComponent implements PreviewableComponent { + + analyzers$: Observable; + + setProps(props: { [key: string]: any; }) { + this.analyzers$ = props.analyzers$; + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html new file mode 100644 index 0000000000..25926fc569 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss new file mode 100644 index 0000000000..f70ee0c2c3 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.scss @@ -0,0 +1,43 @@ +.report { + &__report-header { + align-items: center; + display: flex; + margin-bottom: 8px; + } + &__header { + align-items: center; + display: flex; + } + &__title { + flex: 1; + } + &__stat { + display: flex; + flex-direction: column; + padding: 5px 12px; + &>div:first-child { + opacity: 0.8; + } + } + &__score { + flex: 0; + font-size: 20px; + } + &__grade { + flex: 0; + font-size: 20px; + } + &__table { + margin-left: 20px; + } + &__issue { + align-items: center; + display: flex; + } + &__icon { + padding-right: 4px; + } + &__table-name { + vertical-align: top; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts new file mode 100644 index 0000000000..f1f2d4fd8c --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.spec.ts @@ -0,0 +1,32 @@ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesAnalysisReportComponent } from './kubernetes-analysis-report.component'; +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { AnalysisReportViewerComponent } from './../../../analysis-report-viewer/analysis-report-viewer.component'; + +describe('KubernetesAnalysisReportComponent', () => { + let component: KubernetesAnalysisReportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubernetesAnalysisReportComponent, AnalysisReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, +// MDAppModule + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubernetesAnalysisReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss new file mode 100644 index 0000000000..4ea5002e16 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.theme.scss @@ -0,0 +1,13 @@ + +@mixin kube-analysis-report-theme($theme, $app-theme) { + $backgrounds: map-get($theme, background); + $background: mat-color($backgrounds, card); + $background-color: map-get($app-theme, app-background-color); + $darker-background-color: darken($background-color, 4%); + .report__header { + background-color: $darker-background-color; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding-left: 10px; + } +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts new file mode 100644 index 0000000000..4ca7a3fc19 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-report/kubernetes-analysis-report.component.ts @@ -0,0 +1,80 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IHeaderBreadcrumbLink } from 'frontend/packages/core/src/shared/components/page-header/page-header.types'; +import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; +import { catchError, first, map, startWith } from 'rxjs/operators'; + +import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service'; +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; +import { getParentURL } from '../../../services/route.helper'; + +@Component({ + selector: 'app-kubernetes-analysis-report', + templateUrl: './kubernetes-analysis-report.component.html', + styleUrls: ['./kubernetes-analysis-report.component.scss'] +}) +export class KubernetesAnalysisReportComponent implements OnInit { + + report$: Observable; + private errorMsg = new Subject(); + errorMsg$ = this.errorMsg.pipe(startWith('')); + isLoading$: Observable; + + endpointID: string; + id: string; + + private breadcrumbsSubject: BehaviorSubject; + public breadcrumbs$: Observable; + + constructor( + private analysisService: KubernetesAnalysisService, + private route: ActivatedRoute, + private kubeEndpointService: KubernetesEndpointService, + ) { + this.id = route.snapshot.params.id; + + this.breadcrumbsSubject = new BehaviorSubject(undefined); + this.breadcrumbs$ = this.breadcrumbsSubject.asObservable(); + this.breadcrumbsSubject.next([ + { value: 'Analysis', routerLink: getParentURL(route, 2) }, + { value: 'Report' }, + ]); + } + + ngOnInit() { + this.report$ = this.analysisService.getByID(this.kubeEndpointService.baseKube.guid, this.id).pipe( + map((response: any) => { + if (!response.type) { + this.error(); + return false; + } + this.errorMsg.next(''); + return response; + }), + catchError((e, c) => { + this.error(); + return of(false); + }) + ); + + this.isLoading$ = this.report$.pipe( + map(() => false), + startWith(true) + ); + + // When the report has loaded, update the name in the breadcrumbs + this.report$.pipe(first()).subscribe(report => { + this.breadcrumbsSubject.next([ + { value: 'Analysis', routerLink: getParentURL(this.route, 2) }, + { value: report.name }, + ]); + }); + } + + error() { + const msg = { firstLine: 'Failed to load Analysis Report' }; + this.errorMsg.next(msg); + } +} + + diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html new file mode 100644 index 0000000000..42ebd5c5d8 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts new file mode 100644 index 0000000000..b13da9b832 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.spec.ts @@ -0,0 +1,42 @@ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MDAppModule } from './../../../../core/md.module'; + +import { KubernetesAnalysisTabComponent } from './kubernetes-analysis-tab.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { AnalysisReportViewerComponent } from './../../analysis-report-viewer/analysis-report-viewer.component'; +import { TabNavService } from 'frontend/packages/core/tab-nav.service'; + +describe('KubernetesAnalysisTabComponent', () => { + let component: KubernetesAnalysisTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ KubernetesAnalysisTabComponent, AnalysisReportViewerComponent ], + imports: [ + KubernetesBaseTestModules, + MDAppModule + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + TabNavService, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubernetesAnalysisTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts new file mode 100644 index 0000000000..f0db6bbd91 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/tabs/kubernetes-analysis-tab/kubernetes-analysis-tab.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { ListConfig } from 'frontend/packages/core/src/shared/components/list/list.component.types'; + +import { AnalysisReportsListConfig } from '../../list-types/analysis-reports-list-config.service'; +import { KubernetesEndpointService } from '../../services/kubernetes-endpoint.service'; +import { KubernetesAnalysisService } from '../../services/kubernetes.analysis.service'; + +@Component({ + selector: 'app-kubernetes-analysis-tab', + templateUrl: './kubernetes-analysis-tab.component.html', + styleUrls: ['./kubernetes-analysis-tab.component.scss'], + providers: [ + KubernetesAnalysisService, + { + provide: ListConfig, + useClass: AnalysisReportsListConfig, + } + ] +}) +export class KubernetesAnalysisTabComponent { + + constructor(public kubeEndpointService: KubernetesEndpointService) { } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts index 4777afe99b..96996ee411 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.spec.ts @@ -1,8 +1,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabNavService } from 'frontend/packages/core/tab-nav.service'; -import { HelmReleaseProviders, KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { HelmReleaseProviders, KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../kubernetes.testing.module'; import { HelmReleaseTabBaseComponent } from './helm-release-tab-base.component'; +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../services/kubernetes-endpoint.service'; describe('HelmReleaseTabBaseComponent', () => { @@ -15,7 +17,10 @@ describe('HelmReleaseTabBaseComponent', () => { declarations: [HelmReleaseTabBaseComponent], providers: [ ...HelmReleaseProviders, - TabNavService + TabNavService, + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, ] }) .compileComponents(); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts index 9a838e889f..b84d708c7c 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/helm-release-tab-base/helm-release-tab-base.component.ts @@ -7,12 +7,14 @@ import { catchError, map, share, switchMap } from 'rxjs/operators'; import { LoggerService } from '../../../../../../../core/src/core/logger.service'; import { IPageSideNavTab } from '../../../../../../../core/src/features/dashboard/page-side-nav/page-side-nav.component'; +import { SessionService } from '../../../../../../../core/src/shared/services/session.service'; import { SnackBarService } from '../../../../../../../core/src/shared/services/snackbar.service'; import { AppState } from '../../../../../../../store/src/app-state'; import { entityCatalog } from '../../../../../../../store/src/entity-catalog/entity-catalog'; import { EntityRequestAction, WrapperRequestActionSuccess } from '../../../../../../../store/src/types/request.types'; import { kubeEntityCatalog } from '../../../kubernetes-entity-catalog'; import { KubernetesPodExpandedStatusHelper } from '../../../services/kubernetes-expanded-state'; +import { KubernetesAnalysisService } from '../../../services/kubernetes.analysis.service'; import { KubernetesPod, KubeService } from '../../../store/kube.types'; import { KubePaginationAction } from '../../../store/kubernetes.actions'; import { HelmReleaseGraph, HelmReleaseGuid, HelmReleasePod, HelmReleaseService } from '../../workload.types'; @@ -26,6 +28,7 @@ import { HelmReleaseHelperService } from '../tabs/helm-release-helper.service'; styleUrls: ['./helm-release-tab-base.component.scss'], providers: [ HelmReleaseHelperService, + KubernetesAnalysisService, { provide: HelmReleaseGuid, useFactory: (activatedRoute: ActivatedRoute) => ({ @@ -43,8 +46,6 @@ export class HelmReleaseTabBaseComponent implements OnDestroy { private sub: Subscription; - // private connection: Connection; - public breadcrumbs = [{ breadcrumbs: [ { value: 'Workloads', routerLink: '/workloads' } @@ -53,23 +54,28 @@ export class HelmReleaseTabBaseComponent implements OnDestroy { public title = ''; - tabLinks: IPageSideNavTab[] = [ - { link: 'summary', label: 'Summary', icon: 'helm', iconFont: 'stratos-icons' }, - { link: 'notes', label: 'Notes', icon: 'subject' }, - { link: 'values', label: 'Values', icon: 'list' }, - { link: '-', label: 'Resources' }, - // { link: 'graph', label: 'Overview', icon: 'share' }, - { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, - { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' } - ]; + tabLinks: IPageSideNavTab[]; + constructor( public helmReleaseHelper: HelmReleaseHelperService, private store: Store, private logService: LoggerService, - private snackbarService: SnackBarService + private analysisService: KubernetesAnalysisService, + private snackbarService: SnackBarService, + sessionService: SessionService ) { this.title = this.helmReleaseHelper.releaseTitle; + this.tabLinks = [ + { link: 'summary', label: 'Summary', icon: 'helm', iconFont: 'stratos-icons' }, + { link: 'notes', label: 'Notes', icon: 'subject' }, + { link: 'values', label: 'Values', icon: 'list' }, + { link: 'analysis', label: 'Analysis', icon: 'assignment', hidden$: this.analysisService.hideAnalysis$ }, + { link: '-', label: 'Resources' }, + { link: 'graph', label: 'Overview', icon: 'share', hidden$: sessionService.isTechPreview().pipe(map(tp => !tp)) }, + { link: 'pods', label: 'Pods', icon: 'pod', iconFont: 'stratos-icons' }, + { link: 'services', label: 'Services', icon: 'service', iconFont: 'stratos-icons' } + ]; const releaseRef = this.helmReleaseHelper.guidAsUrlFragment(); const host = window.location.host; @@ -132,21 +138,23 @@ export class HelmReleaseTabBaseComponent implements OnDestroy { const releaseServicesAction = kubeEntityCatalog.service.actions.getInWorkload( this.helmReleaseHelper.releaseTitle, this.helmReleaseHelper.endpointGuid, - ) + ); this.populateList(releaseServicesAction, svcs); } - const resources = { ...manifest }; - resources.endpointId = this.helmReleaseHelper.endpointGuid; - resources.releaseTitle = this.helmReleaseHelper.releaseTitle; + // const resources = { ...manifest }; + // kind === 'Resources' is an array, really they should go into a pagination section + messageObj.endpointId = this.helmReleaseHelper.endpointGuid; + messageObj.releaseTitle = this.helmReleaseHelper.releaseTitle; + const releaseResourceAction = workloadsEntityCatalog.resource.actions.get( - resources.releaseTitle, - resources.endpointId, + this.helmReleaseHelper.releaseTitle, + this.helmReleaseHelper.endpointGuid, ); - this.addResource(releaseResourceAction, resources); + this.addResource(releaseResourceAction, messageObj); } else if (messageObj.kind === 'ManifestErrors') { if (messageObj.data) { - this.snackbarService.show('Errors were found when parsing this workload. Not all resources may be shown', 'Dismiss') + this.snackbarService.show('Errors were found when parsing this workload. Not all resources may be shown', 'Dismiss'); } } } @@ -181,7 +189,7 @@ export class HelmReleaseTabBaseComponent implements OnDestroy { newResource.metadata.kubeId = action.kubeGuid; // The service entity from manifest is missing this, but apply here to ensure any others are caught newResource.metadata.namespace = this.helmReleaseHelper.namespace; - const entityId = action.entity[0].getId(resource) + const entityId = action.entity[0].getId(resource); newResources[entityId] = newResource; }); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/icon-helper.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/icon-helper.ts new file mode 100644 index 0000000000..ea2d9e6df0 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/icon-helper.ts @@ -0,0 +1,77 @@ +export function getIcon(kind: string) { + const rkind = kind || 'Pod'; + if (iconMappings[rkind]) { + return iconMappings[rkind]; + } else { + return iconMappings.default; + } +} + +const iconMappings = { + Namespace: { + name: 'namespace', + font: 'stratos-icons' + }, + Container: { + name: 'container', + font: 'stratos-icons' + }, + ClusterRole: { + name: 'cluster_role', + font: 'stratos-icons' + }, + ClusterRoleBinding: { + name: 'cluster_role_binding', + font: 'stratos-icons' + }, + Deployment: { + name: 'deployment', + font: 'stratos-icons' + }, + ReplicaSet: { + name: 'replica_set', + font: 'stratos-icons' + }, + Pod: { + name: 'pod', + font: 'stratos-icons' + }, + Service: { + name: 'service', + font: 'stratos-icons' + }, + Role: { + name: 'assignment_ind', + font: 'Material Icons', + fontSet: 'material-icons' + }, + RoleBinding: { + name: 'role_binding', + font: 'stratos-icons' + }, + StatefulSet: { + name: 'stateful_set', + font: 'stratos-icons' + }, + Ingress: { + name: 'ingress', + font: 'stratos-icons' + }, + ConfigMap: { + name: 'config_map', + font: 'stratos-icons' + }, + Secret: { + name: 'config_map', + font: 'stratos-icons' + }, + ServiceAccount: { + name: 'lock', + font: 'Material Icons', + fontSet: 'material-icons' + }, + default: { + name: 'collocation', + font: 'stratos-icons' + } +}; \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html new file mode 100644 index 0000000000..15301e10ff --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.html @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts new file mode 100644 index 0000000000..a2c29af062 --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HelmReleaseAnalysisTabComponent } from './helm-release-analysis-tab.component'; +import { AnalysisReportSelectorComponent } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; +import { KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../../kubernetes.testing.module'; +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service'; +import { AnalysisReportViewerComponent } from '../../../../analysis-report-viewer/analysis-report-viewer.component'; +import { HelmReleaseProviders } from '../../../../kubernetes.testing.module'; +import { TabNavService } from 'frontend/packages/core/tab-nav.service'; + +describe('HelmReleaseAnalysisTabComponent', () => { + let component: HelmReleaseAnalysisTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HelmReleaseAnalysisTabComponent, AnalysisReportSelectorComponent, AnalysisReportViewerComponent], + imports: [ + KubernetesBaseTestModules, + ], + providers: [ + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + HelmReleaseProviders, + TabNavService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HelmReleaseAnalysisTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts new file mode 100644 index 0000000000..beb9436e4c --- /dev/null +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { Subject } from 'rxjs'; + +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { AnalysisReport } from '../../../../store/kube.types'; +import { HelmReleaseHelperService } from '../helm-release-helper.service'; + +@Component({ + selector: 'app-helm-release-analysis-tab', + templateUrl: './helm-release-analysis-tab.component.html', + styleUrls: ['./helm-release-analysis-tab.component.scss'] +}) +export class HelmReleaseAnalysisTabComponent { + + public report$ = new Subject(); + + path: string; + + currentReport = null; + + noReportsAvailable = false; + + constructor( + public analaysisService: KubernetesAnalysisService, + public helmReleaseHelper: HelmReleaseHelperService + ) { + this.path = `${this.helmReleaseHelper.namespace}/${this.helmReleaseHelper.releaseTitle}`; + } + + public analysisChanged(report) { + if (report.id !== this.currentReport) { + this.currentReport = report.id; + this.analaysisService.getByID(this.helmReleaseHelper.endpointGuid, report.id).subscribe(r => this.report$.next(r)); + } + } + + + public onReportCount(count: number) { + this.noReportsAvailable = count === 0; + } + +} diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts index a7dd8aaa99..13b0507c23 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-helper.service.ts @@ -10,7 +10,7 @@ import { HelmReleaseChartData, HelmReleaseGraph, HelmReleaseGuid, - HelmReleaseResource, + HelmReleaseResources, } from '../../workload.types'; import { workloadsEntityCatalog } from '../../workloads-entity-catalog'; @@ -69,7 +69,7 @@ export class HelmReleaseHelperService { ); } - public fetchReleaseResources(): Observable { + public fetchReleaseResources(): Observable { // Get helm release const action = workloadsEntityCatalog.resource.actions.get(this.releaseTitle, this.endpointGuid) return workloadsEntityCatalog.resource.store.getEntityMonitor( diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html index 6dd8d86bda..1070d1925c 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.html @@ -3,23 +3,52 @@ all_out Fit - + + + + + - + - - /> - + + + {{ node.data.icon.name }} + {{node.label}} + {{node.data.kind}} + + + + + - \ No newline at end of file + +
+
+ Loading resources + + +
+
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.scss b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.scss index ec3bfddc2f..beb8c49098 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.scss +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.scss @@ -3,3 +3,9 @@ height: 100%; width: 100%; } + +@keyframes dash { + to { + stroke-dashoffset: 1000; + } +} \ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.spec.ts index 238acbf3c5..99e5683d26 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.spec.ts @@ -5,6 +5,10 @@ import { TabNavService } from 'frontend/packages/core/tab-nav.service'; import { HelmReleaseProviders, KubernetesBaseTestModules } from '../../../../kubernetes.testing.module'; import { HelmReleaseResourceGraphComponent } from './helm-release-resource-graph.component'; +import { AnalysisReportSelectorComponent } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service'; +import { KubeBaseGuidMock } from './../../../../kubernetes.testing.module'; describe('HelmReleaseResourceGraphComponent', () => { let component: HelmReleaseResourceGraphComponent; @@ -16,11 +20,14 @@ describe('HelmReleaseResourceGraphComponent', () => { ...KubernetesBaseTestModules, NgxGraphModule ], - declarations: [HelmReleaseResourceGraphComponent], + declarations: [HelmReleaseResourceGraphComponent, AnalysisReportSelectorComponent], providers: [ ...HelmReleaseProviders, SidePanelService, TabNavService, + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, ] }) .compileComponents(); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.ts index d47848393b..9e73f79e75 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-resource-graph/helm-release-resource-graph.component.ts @@ -1,13 +1,22 @@ import { Component, ComponentFactoryResolver, OnDestroy, OnInit } from '@angular/core'; -import { Edge, Node } from '@swimlane/ngx-graph'; +import { Edge } from '@swimlane/ngx-graph'; import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { filter, first, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, first, map, publishReplay, refCount, startWith } from 'rxjs/operators'; import { KubernetesResourceViewerComponent, } from '../../../../kubernetes-resource-viewer/kubernetes-resource-viewer.component'; -import { KubeAPIResource } from '../../../../store/kube.types'; +import { ResourceAlert, ResourceAlertLevel } from '../../../../services/analysis-report.types'; +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { + HelmReleaseGraphLink, + HelmReleaseGraphNode, + HelmReleaseGraphNodeData, + HelmReleaseResource, + HelmReleaseResources, +} from '../../../workload.types'; +import { getIcon } from '../../icon-helper'; import { HelmReleaseHelperService } from '../helm-release-helper.service'; @@ -22,6 +31,26 @@ const layouts = [ 'colaForceDirected' ]; +interface CustomHelmReleaseGraphNode extends Omit { + data: CustomHelmReleaseGraphNodeData +} + +interface CustomHelmReleaseGraphNode { + id: string; + label: string; + data: CustomHelmReleaseGraphNodeData +} + +interface CustomHelmReleaseGraphNodeData extends HelmReleaseGraphNodeData { + missing: boolean, + dash: number, + fill: string, + text: string, + icon: any, + alerts: [], + alertSummary: {} +} + @Component({ selector: 'app-helm-release-resource-graph', templateUrl: './helm-release-resource-graph.component.html', @@ -31,7 +60,7 @@ export class HelmReleaseResourceGraphComponent implements OnInit, OnDestroy { // see: https://swimlane.github.io/ngx-graph/#/#quick-start - public nodes: Node[] = []; + public nodes: CustomHelmReleaseGraphNode[] = []; public links: Edge[] = []; update$: BehaviorSubject = new BehaviorSubject(false); @@ -44,31 +73,62 @@ export class HelmReleaseResourceGraphComponent implements OnInit, OnDestroy { private graph: Subscription; + private didInitialFit = false; + + public path: string; + + private analysisReportUpdated = new Subject(); + private analysisReportUpdated$ = this.analysisReportUpdated.pipe( + startWith(null), + distinctUntilChanged(), + publishReplay(1), + refCount() + ); + constructor( private componentFactoryResolver: ComponentFactoryResolver, private helper: HelmReleaseHelperService, - private previewPanel: SidePanelService) { } + public analyzerService: KubernetesAnalysisService, + private previewPanel: SidePanelService) { + this.path = `${this.helper.namespace}/${this.helper.releaseTitle}`; + } ngOnInit() { // Listen for the graph - this.graph = this.helper.fetchReleaseGraph().subscribe(g => { - const newNodes = []; - Object.values(g.nodes).forEach((node: any) => { + this.graph = combineLatest( + this.helper.fetchReleaseGraph(), + this.analysisReportUpdated$ + ).subscribe(([g, report]) => { + const newNodes: CustomHelmReleaseGraphNode[] = []; + Object.values(g.nodes).forEach((node: HelmReleaseGraphNode) => { const colors = this.getColor(node.data.status); - newNodes.push({ + const icon = getIcon(node.data.kind); + const missing = node.data.status === 'missing'; + + const newNode: CustomHelmReleaseGraphNode = { id: node.id, label: node.label, data: { ...node.data, + missing: node.data.status === 'missing', + dash: missing ? 6 : 0, fill: colors.bg, - text: colors.fg + text: colors.fg, + icon: icon, + alerts: null, + alertSummary: {} }, - }); + }; + + // Does this node have any alerts? + this.applyAlertToNode(newNode, report) + + newNodes.push(newNode); }); this.nodes = newNodes; - const newLinks = []; + const newLinks: HelmReleaseGraphLink[] = []; Object.values(g.links).forEach((link: any) => { newLinks.push({ id: link.id, @@ -79,9 +139,48 @@ export class HelmReleaseResourceGraphComponent implements OnInit, OnDestroy { }); this.links = newLinks; this.update$.next(true); + + if (!this.didInitialFit) { + this.didInitialFit = true; + setTimeout(() => this.fitGraph(), 10); + } }); } + private applyAlertToNode(newNode, report) { + if (report && report.alerts) { + Object.values(report.alerts).forEach((group: ResourceAlert[]) => { + group.forEach(alert => { + if ( + newNode.data.kind.toLowerCase() === alert.kind && + newNode.data.metadata.name === alert.name + // namespace is undefined, however the only resources we have should be from the correct context + ) { + newNode.data.alerts = newNode.data.alerts || []; + newNode.data.alerts.push(alert); + newNode.data.alertSummary = newNode.data.alertSummary || {}; + if (alert.level > newNode.data.alertSummary.level || !newNode.data.alertSummary.level) { + newNode.data.alertSummary.color = this.alertLevelToColor(alert.level); + newNode.data.alertSummary.level = alert.level; + } + } + }); + }); + } + } + + private alertLevelToColor(level: ResourceAlertLevel) { + // These colours need to come from theme - #420 + switch (level) { + case ResourceAlertLevel.Info: + return '#42a5f5'; + case ResourceAlertLevel.Warning: + return '#ff9800'; + case ResourceAlertLevel.Error: + return '#f44336'; + } + } + ngOnDestroy() { if (this.graph) { this.graph.unsubscribe(); @@ -89,15 +188,20 @@ export class HelmReleaseResourceGraphComponent implements OnInit, OnDestroy { } // Open side panel when node is clicked - public onNodeClick(node: any) { - this.previewPanel.show( - KubernetesResourceViewerComponent, - { - title: 'Helm Release Resource Preview', - resource$: this.getResource(node) - }, - this.componentFactoryResolver - ); + public onNodeClick(node: CustomHelmReleaseGraphNode) { + this.analysisReportUpdated$.pipe(first()).subscribe(analysis => { + this.previewPanel.show( + KubernetesResourceViewerComponent, + { + title: 'Helm Release Resource Preview', + resource$: this.getResource(node), + analysis, + resourceKind: node.data.kind + }, + this.componentFactoryResolver + ); + }) + } public fitGraph() { @@ -138,12 +242,31 @@ export class HelmReleaseResourceGraphComponent implements OnInit, OnDestroy { } } - private getResource(node: any): Observable { + private getResource(node: CustomHelmReleaseGraphNode): Observable { return this.helper.fetchReleaseResources().pipe( filter(r => !!r), - map((r: any[]) => Object.values(r).find((res: any) => res.metadata.name === node.label && res.metadata.kind === node.kind)), + // tap(r => { + // console.log(node); + // console.log(r); + // }), + map((r: HelmReleaseResources) => Object.values(r.data).find((res) => { + // if (!res.metadata) { + // console.log(node, res); + // } + return res.metadata.name === node.label && res.kind === node.data.kind; + })), first(), ); } + public analysisChanged(report) { + if (report === null) { + this.analysisReportUpdated.next(null); + } else { + this.analyzerService.getByID(this.helper.endpointGuid, report.id).subscribe(results => { + this.analysisReportUpdated.next(results); + }); + } + } + } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html index f69faf6ea9..0bfbe0568a 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.html @@ -1,8 +1,15 @@ - + + + + + + @@ -59,7 +66,8 @@
+ [alerts]="res.alerts" (showAlerts)="showAlerts($event, res)" + iconFont="{{ res.icon.fontSet || res.icon.font }}" value="{{ res.count }}">
@@ -76,12 +84,11 @@
- Loading Resources + Loading resources
-
\ No newline at end of file diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts index 60540500e7..ebe873f1d2 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.spec.ts @@ -1,8 +1,12 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabNavService } from 'frontend/packages/core/tab-nav.service'; -import { HelmReleaseProviders, KubernetesBaseTestModules } from '../../../../kubernetes.testing.module'; +import { HelmReleaseProviders, KubernetesBaseTestModules, KubeBaseGuidMock } from '../../../../kubernetes.testing.module'; import { HelmReleaseSummaryTabComponent } from './helm-release-summary-tab.component'; +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { KubernetesEndpointService } from '../../../../services/kubernetes-endpoint.service'; +import { AnalysisReportSelectorComponent } from './../../../../analysis-report-viewer/analysis-report-selector/analysis-report-selector.component'; +import { SidePanelService } from './../../../../../../shared/services/side-panel.service'; describe('HelmReleaseSummaryTabComponent', () => { let component: HelmReleaseSummaryTabComponent; @@ -13,10 +17,14 @@ describe('HelmReleaseSummaryTabComponent', () => { imports: [ ...KubernetesBaseTestModules ], - declarations: [HelmReleaseSummaryTabComponent], + declarations: [HelmReleaseSummaryTabComponent, AnalysisReportSelectorComponent], providers: [ ...HelmReleaseProviders, - TabNavService + KubernetesAnalysisService, + KubernetesEndpointService, + KubeBaseGuidMock, + TabNavService, + SidePanelService ] }) .compileComponents(); diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts index a302a6fd2a..3b3d8ce7c8 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/release/tabs/helm-release-summary-tab/helm-release-summary-tab.component.ts @@ -1,25 +1,32 @@ import { HttpClient } from '@angular/common/http'; -import { Component, OnDestroy } from '@angular/core'; +import { Component, ComponentFactoryResolver, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; import { LoggerService } from 'frontend/packages/core/src/core/logger.service'; import { ConfirmationDialogConfig } from 'frontend/packages/core/src/shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from 'frontend/packages/core/src/shared/components/confirmation-dialog.service'; +import { SidePanelService } from 'frontend/packages/core/src/shared/services/side-panel.service'; import { ClearPaginationOfType } from 'frontend/packages/store/src/actions/pagination.actions'; import { RouterNav } from 'frontend/packages/store/src/actions/router.actions'; import { AppState } from 'frontend/packages/store/src/app-state'; -import { combineLatest, Observable, ReplaySubject } from 'rxjs'; +import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs'; import { distinctUntilChanged, filter, first, map, publishReplay, refCount, startWith } from 'rxjs/operators'; import { SnackBarService } from '../../../../../../../../core/src/shared/services/snackbar.service'; import { endpointsEntityRequestDataSelector } from '../../../../../../../../store/src/selectors/endpoint.selectors'; -import { HelmReleaseChartData, HelmReleaseResource } from '../../../workload.types'; +import { + ResourceAlertPreviewComponent, +} from '../../../../analysis-report-viewer/resource-alert-preview/resource-alert-preview.component'; +import { KubernetesAnalysisService } from '../../../../services/kubernetes.analysis.service'; +import { HelmReleaseChartData } from '../../../workload.types'; import { workloadsEntityCatalog } from '../../../workloads-entity-catalog'; +import { getIcon } from '../../icon-helper'; import { HelmReleaseHelperService } from '../helm-release-helper.service'; +import { ResourceAlert } from './../../../../services/analysis-report.types'; @Component({ selector: 'app-helm-release-summary-tab', templateUrl: './helm-release-summary-tab.component.html', - styleUrls: ['./helm-release-summary-tab.component.scss'] + styleUrls: ['./helm-release-summary-tab.component.scss'], }) export class HelmReleaseSummaryTabComponent implements OnDestroy { // Confirmation dialogs @@ -38,6 +45,8 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { private successChartColor = '#4DD3A7'; private completedChartColour = '#7aa3e5'; + public path: string; + public podChartColors = [ { name: 'Running', @@ -60,88 +69,30 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { } ]; - - public iconMappings = { - Namespace: { - name: 'namespace', - font: 'stratos-icons' - }, - Container: { - name: 'container', - font: 'stratos-icons' - }, - ClusterRole: { - name: 'cluster_role', - font: 'stratos-icons' - }, - ClusterRoleBinding: { - name: 'cluster_role_binding', - font: 'stratos-icons' - }, - Deployment: { - name: 'deployment', - font: 'stratos-icons' - }, - ReplicaSet: { - name: 'replica_set', - font: 'stratos-icons' - }, - Pod: { - name: 'pod', - font: 'stratos-icons' - }, - Service: { - name: 'service', - font: 'stratos-icons' - }, - Role: { - name: 'assignment_ind' - }, - RoleBinding: { - name: 'role_binding', - font: 'stratos-icons' - }, - StatefulSet: { - name: 'stateful_set', - font: 'stratos-icons' - }, - Ingress: { - name: 'ingress', - font: 'stratos-icons' - }, - ConfigMap: { - name: 'config_map', - font: 'stratos-icons' - }, - Secret: { - name: 'config_map', - font: 'stratos-icons' - }, - ServiceAccount: { - name: 'lock' - }, - default: { - name: 'collocation', - font: 'stratos-icons' - } - }; - // Blue: #00B2E2 // Yellow: #FFC107 private deleted = false; public chartData$: Observable; - public resources$: Observable; + public resources$: Observable; + + // Cached analysis report + private analysisReport; + + private analysisReportUpdated = new Subject(); + private analysisReportUpdated$ = this.analysisReportUpdated.pipe(startWith(null), distinctUntilChanged()); constructor( + private componentFactoryResolver: ComponentFactoryResolver, public helmReleaseHelper: HelmReleaseHelperService, private store: Store, private confirmDialog: ConfirmationDialogService, private httpClient: HttpClient, private logService: LoggerService, - private snackbarService: SnackBarService + private snackbarService: SnackBarService, + public analyzerService: KubernetesAnalysisService, + private previewPanel: SidePanelService, ) { - this.isBusy$ = combineLatest([ this.helmReleaseHelper.isFetching$, this.busyDeletingSubject.asObservable().pipe( @@ -152,6 +103,8 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { startWith(true) ); + this.path = `${this.helmReleaseHelper.namespace}/${this.helmReleaseHelper.releaseTitle}`; + this.chartData$ = this.helmReleaseHelper.fetchReleaseChartStats().pipe( distinctUntilChanged(), map(chartData => ({ @@ -162,8 +115,11 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { ) ); - this.resources$ = this.helmReleaseHelper.fetchReleaseGraph().pipe( - map((graph: any) => { + this.resources$ = combineLatest( + this.helmReleaseHelper.fetchReleaseGraph(), + this.analysisReportUpdated$ + ).pipe( + map(([graph,]) => { const resources = {}; // Collect the resources Object.values(graph.nodes).forEach((node: any) => { @@ -173,18 +129,20 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { label: `${node.data.kind}s`, count: 0, statuses: [], - icon: this.getIcon(node.data.kind) + icon: getIcon(node.data.kind) }; } resources[node.data.kind].count++; resources[node.data.kind].statuses.push(node.data.status); }); + this.applyAnalysis(resources, this.analysisReport); return Object.values(resources).sort((a: any, b: any) => a.kind.localeCompare(b.kind)); }), publishReplay(1), refCount() ); + this.hasResources$ = combineLatest([ this.chartData$, this.resources$ @@ -206,14 +164,25 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { }, 'Delete' ); + + this.hasAllResources$ = combineLatest([ + this.resources$, + this.hasResources$ + ]).pipe( + map(([resources, hasSome]) => hasSome && resources && resources.length > 0) + ); } - private getIcon(kind: string) { - const rkind = kind || 'Pod'; - if (this.iconMappings[rkind]) { - return this.iconMappings[rkind]; + public analysisChanged(report) { + if (report === null) { + // No report selected + this.analysisReport = null; + this.analysisReportUpdated.next(''); } else { - return this.iconMappings.default; + this.analyzerService.getByID(this.helmReleaseHelper.endpointGuid, report.id).subscribe(results => { + this.analysisReport = results; + this.analysisReportUpdated.next(report.id); + }); } } @@ -283,4 +252,35 @@ export class HelmReleaseSummaryTabComponent implements OnDestroy { first() ); } + + private applyAnalysis(resources, report) { + // Clear out existing alerts for all resources + Object.values(resources).forEach((resource: any) => resource.alerts = []); + + if (report && Object.keys(resources).length > 0) { + Object.values(report.alerts).forEach((group: ResourceAlert[]) => { + group.forEach(alert => { + // Can we find a corresponding group in the resources? + const res = Object.keys(resources).find((i) => i.toLowerCase() === alert.kind); + if (res) { + const resItem = resources[res]; + if (resItem) { + resItem.alerts.push(alert); + } + } + }); + }); + } + } + + public showAlerts(alerts, resource) { + this.previewPanel.show( + ResourceAlertPreviewComponent, + { + resource, + alerts, + }, + this.componentFactoryResolver + ); + } } diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts index abce799544..9605546f9d 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-factory.ts @@ -1,6 +1,6 @@ import { EntitySchema } from '../../../../../../store/src/helpers/entity-schema'; import { addKubernetesEntitySchema, KubernetesEntitySchema } from '../../kubernetes-entity-factory'; -import { HelmRelease, HelmReleaseGraph, HelmReleaseResource } from '../workload.types'; +import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from '../workload.types'; export const helmReleaseEntityKey = 'helmRelease'; export const helmReleasePodEntityType = 'helmReleasePod'; @@ -22,7 +22,7 @@ export const getHelmReleaseIdByObj = (entity: HelmRelease) => getHelmReleaseId(e export const getHelmReleaseGraphId = (endpointId: string, releaseTitle: string) => `${endpointId}${separator}${releaseTitle}`; export const getHelmReleaseGraphIdByObj = (entity: HelmReleaseGraph) => getHelmReleaseGraphId(entity.endpointId, entity.releaseTitle); export const getHelmReleaseResourceId = (endpointId: string, releaseTitle: string) => `${endpointId}${separator}${releaseTitle}`; -export const getHelmReleaseResourceIdByObj = (entity: HelmReleaseResource) => getHelmReleaseResourceId(entity.endpointId, entity.releaseTitle); +export const getHelmReleaseResourceIdByObj = (entity: HelmReleaseResources) => getHelmReleaseResourceId(entity.endpointId, entity.releaseTitle); const entityCache: { [key: string]: EntitySchema diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts index f4a0939afa..3a8f8a0181 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/store/workloads-entity-generator.ts @@ -5,7 +5,7 @@ import { import { StratosEndpointExtensionDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; import { IFavoriteMetadata } from '../../../../../../store/src/types/user-favorites.types'; import { kubernetesEntityFactory } from '../../kubernetes-entity-factory'; -import { HelmRelease, HelmReleaseGraph, HelmReleaseResource } from '../workload.types'; +import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from '../workload.types'; import { workloadsEntityCatalog } from '../workloads-entity-catalog'; import { WorkloadGraphBuilders, @@ -62,7 +62,7 @@ function generateReleaseResourceEntity(endpointDefinition: StratosEndpointExtens schema: kubernetesEntityFactory(helmReleaseResourceEntityType), endpoint: endpointDefinition }; - workloadsEntityCatalog.resource = new StratosCatalogEntity( + workloadsEntityCatalog.resource = new StratosCatalogEntity( definition, { actionBuilders: workloadResourceBuilders diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts index f91220bad4..bf5b8e708b 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workload.types.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { KubernetesPod, KubeService } from '../store/kube.types'; +import { KubeAPIResource, KubernetesPod, KubeService, KubeStatus } from '../store/kube.types'; export interface HelmRelease { endpointId: string; @@ -36,11 +36,43 @@ export interface HelmReleasePod extends HelmReleaseEntity, KubernetesPod { } export interface HelmReleaseService extends HelmReleaseEntity, KubeService { } export interface HelmReleaseGraph extends HelmReleaseEntity { - nodes: {}; - links: {}; + nodes: { [key: string]: HelmReleaseGraphNode }; + links: { [key: string]: HelmReleaseGraphLink }; } -export type HelmReleaseResource = any; +export interface HelmReleaseGraphNode { + id: string; + label: string; + data: HelmReleaseGraphNodeData +} + +export interface HelmReleaseGraphNodeData { + kind: string, + status: string, + metadata: { + name: string, + namespace: string + } +} + +export interface HelmReleaseGraphLink { + id: string; + label?: string; + source: string; + target: string; +} + +export interface HelmReleaseResources extends HelmReleaseEntity { + data: HelmReleaseResource[], + kind: string +}; + +export interface HelmReleaseKubeAPIResource extends KubeAPIResource { + apiVersion: string; + kind: string; +} + +export type HelmReleaseResource = HelmReleaseKubeAPIResource | KubeStatus; @Injectable() export class HelmReleaseGuid { diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts index 06d3a33c04..6492abd0b9 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads-entity-catalog.ts @@ -1,7 +1,7 @@ import { StratosCatalogEntity } from '../../../../../store/src/entity-catalog/entity-catalog-entity/entity-catalog-entity'; import { IFavoriteMetadata } from '../../../../../store/src/types/user-favorites.types'; import { WorkloadGraphBuilders, WorkloadReleaseBuilders, WorkloadResourceBuilders } from './store/workload-action-builders'; -import { HelmRelease, HelmReleaseGraph, HelmReleaseResource } from './workload.types'; +import { HelmRelease, HelmReleaseGraph, HelmReleaseResources } from './workload.types'; /** * A strongly typed collection of Workload Catalog Entities. @@ -10,7 +10,7 @@ import { HelmRelease, HelmReleaseGraph, HelmReleaseResource } from './workload.t export class WorkloadsEntityCatalog { release: StratosCatalogEntity; graph: StratosCatalogEntity - resource: StratosCatalogEntity + resource: StratosCatalogEntity } /** diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts index 5fb855b409..aaed18524c 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.module.ts @@ -18,6 +18,7 @@ import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-value import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component'; import { WorkloadsStoreModule } from './store/workloads.store.module'; import { WorkloadsRouting } from './workloads.routing'; +import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; @NgModule({ imports: [ @@ -39,6 +40,7 @@ import { WorkloadsRouting } from './workloads.routing'; HelmReleaseServicesTabComponent, HelmReleaseResourceGraphComponent, HelmReleaseCardComponent, + HelmReleaseAnalysisTabComponent, ], entryComponents: [ HelmReleaseCardComponent diff --git a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts index 8f3a4100a7..dc687ecd76 100644 --- a/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts +++ b/src/frontend/packages/suse-extensions/src/custom/kubernetes/workloads/workloads.routing.ts @@ -12,6 +12,7 @@ import { HelmReleaseServicesTabComponent } from './release/tabs/helm-release-ser import { HelmReleaseSummaryTabComponent } from './release/tabs/helm-release-summary-tab/helm-release-summary-tab.component'; import { HelmReleaseValuesTabComponent } from './release/tabs/helm-release-values-tab/helm-release-values-tab.component'; import { HelmReleasesTabComponent } from './releases-tab/releases-tab.component'; +import { HelmReleaseAnalysisTabComponent } from './release/tabs/helm-release-analysis-tab/helm-release-analysis-tab.component'; const routes: Routes = [ { @@ -36,7 +37,8 @@ const routes: Routes = [ { path: 'values', component: HelmReleaseValuesTabComponent }, { path: 'pods', component: HelmReleasePodsTabComponent }, { path: 'services', component: HelmReleaseServicesTabComponent }, - { path: 'graph', component: HelmReleaseResourceGraphComponent } + { path: 'graph', component: HelmReleaseResourceGraphComponent }, + { path: 'analysis', component: HelmReleaseAnalysisTabComponent }, ] }, ] diff --git a/src/frontend/packages/suse-theme/assets/core/custom/kubescore.md b/src/frontend/packages/suse-theme/assets/core/custom/kubescore.md new file mode 100644 index 0000000000..a4a592e740 --- /dev/null +++ b/src/frontend/packages/suse-theme/assets/core/custom/kubescore.md @@ -0,0 +1,7 @@ +## Kube-Score + +Kube-score is a tool that performs static code analysis of your Kubernetes object definitions. + +The output is a list of recommendations of what you can improve to make your application more secure and resilient. + +[https://github.com/zegl/kube-score](https://github.com/zegl/kube-score) \ No newline at end of file diff --git a/src/frontend/packages/suse-theme/assets/core/custom/kubescore.png b/src/frontend/packages/suse-theme/assets/core/custom/kubescore.png new file mode 100644 index 0000000000..ee6b8f82f0 Binary files /dev/null and b/src/frontend/packages/suse-theme/assets/core/custom/kubescore.png differ diff --git a/src/frontend/packages/suse-theme/assets/core/custom/popeye.md b/src/frontend/packages/suse-theme/assets/core/custom/popeye.md new file mode 100644 index 0000000000..98f871ba7c --- /dev/null +++ b/src/frontend/packages/suse-theme/assets/core/custom/popeye.md @@ -0,0 +1,7 @@ +## Popeye - A Kubernetes Cluster Sanitizer + +Popeye is a utility that scans live Kubernetes cluster and reports potential issues with deployed resources and configurations. It sanitizes your cluster based on what's deployed and not what's sitting on disk. By scanning your cluster, it detects misconfigurations and ensure best practices are in place thus preventing potential future headaches. It aims at reducing the cognitive overload one faces when operating a Kubernetes cluster in the wild. Furthermore, if your cluster employs a metric-server, it reports potential resources over/under allocations and attempts to warn you should your cluster run out of capacity. + +Popeye is a readonly tool, it does not alter any of your Kubernetes resources in any way! + +[https://github.com/derailed/popeye](https://github.com/derailed/popeye) \ No newline at end of file diff --git a/src/frontend/packages/suse-theme/assets/core/custom/popeye.png b/src/frontend/packages/suse-theme/assets/core/custom/popeye.png new file mode 100644 index 0000000000..8d07a32b96 Binary files /dev/null and b/src/frontend/packages/suse-theme/assets/core/custom/popeye.png differ diff --git a/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.md b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.md new file mode 100644 index 0000000000..4e7e951cbc --- /dev/null +++ b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.md @@ -0,0 +1,5 @@ +## Sonobuoy - Validate your Kubernetes configuration + +Sonobuoy is a diagnostic tool that makes it easier to understand the state of a Kubernetes cluster by running a choice of configuration tests in an accessible and non-destructive manner. + +[https://sonobuoy.io/](https://sonobuoy.io/) \ No newline at end of file diff --git a/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.png b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.png new file mode 100644 index 0000000000..2fc164c2c5 Binary files /dev/null and b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.png differ diff --git a/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.svg b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.svg new file mode 100644 index 0000000000..96eee04212 --- /dev/null +++ b/src/frontend/packages/suse-theme/assets/core/custom/sonobuoy.svg @@ -0,0 +1,81 @@ + +image/svg+xml \ No newline at end of file diff --git a/src/frontend/packages/theme/_helper.scss b/src/frontend/packages/theme/_helper.scss index 7b9015f992..70cb9f9eda 100644 --- a/src/frontend/packages/theme/_helper.scss +++ b/src/frontend/packages/theme/_helper.scss @@ -115,6 +115,6 @@ $oss-dark-theme: mat-dark-theme($oss-dark-primary, $oss-dark-accent, $oss-dark-w $warn: map-get($theme, warn); $primary: map-get($theme, primary); $white: #fff; // Use default palette for status - @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); + @return (success: map-get($mat-green, 500), info: map-get($mat-blue, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); } } diff --git a/src/jetstream/config.example b/src/jetstream/config.example index 69d9110c8b..417f5f8144 100644 --- a/src/jetstream/config.example +++ b/src/jetstream/config.example @@ -75,5 +75,8 @@ INVITE_USER_CLIENT_SECRET= # Simplify development with FDB (value is port of FDB server: 27016 for FDB, 27017 for MongoDB) #FDB_LOCAL_DEV=27017 +# Analysis services API +#ANALYSIS_SERVICES_API= + # Download link when installing the Kubernetes Dashboard in a targetted Kube Endpoint -# STRATOS_KUBERNETES_DASHBOARD_IMAGE= \ No newline at end of file +# STRATOS_KUBERNETES_DASHBOARD_IMAGE= diff --git a/src/jetstream/go.mod b/src/jetstream/go.mod index 59494917e6..989a9fc1b8 100644 --- a/src/jetstream/go.mod +++ b/src/jetstream/go.mod @@ -36,6 +36,7 @@ require ( github.com/fatih/color v1.7.0 // indirect github.com/go-sql-driver/mysql v1.5.0 github.com/google/go-querystring v1.0.0 // indirect + github.com/google/martian v2.1.0+incompatible github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect github.com/gorilla/context v1.1.1 github.com/gorilla/securecookie v1.1.1 diff --git a/src/jetstream/go.sum b/src/jetstream/go.sum index df1d085124..ca7f76c6fb 100644 --- a/src/jetstream/go.sum +++ b/src/jetstream/go.sum @@ -56,6 +56,7 @@ github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiU github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= @@ -130,10 +131,13 @@ github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.15+incompatible h1:+9RjdC18gMxNQVvSiXvObLu29mOFmkgdsB4cRTlV+EE= github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea h1:n2Ltr3SrfQlf/9nOna1DoGKxLx3qTSI8Ttl6Xrqp6mw= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cppforlife/go-patch v0.2.0 h1:Y14MnCQjDlbw7WXT4k+u6DPAA9XnygN4BfrSpI/19RU= github.com/cppforlife/go-patch v0.2.0/go.mod h1:67a7aIi94FHDZdoeGSJRRFDp66l9MhaAG1yGxpUoFD8= @@ -357,6 +361,7 @@ github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:Fecb github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= @@ -478,6 +483,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= @@ -509,6 +515,7 @@ github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830 h1:yvQ/2Pupw60ON8TYEIGGTAI77yZsWYkiOeHFZWkwlCk= github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -634,8 +641,11 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK go.mongodb.org/mongo-driver v1.1.3 h1:++7u8r9adKhGR+I79NfEtYrk2ktjenErXM99PSufIoI= go.mongodb.org/mongo-driver v1.1.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569 h1:nSQar3Y0E3VQF/VdZ8PTAilaXpER+d7ypdABCrpwMdg= go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df h1:shvkWr0NAZkg4nPuE3XrKP0VuBPijjk3TfX6Y6acFNg= go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15 h1:Z2sc4+v0JHV6Mn4kX1f2a5nruNjmV+Th32sugE8zwz8= go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -778,6 +788,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/square/go-jose.v1 v1.1.2 h1:/5jmADZB+RiKtZGr4HxsEFOEfbfsjTKsVnqpThUpE30= gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA= @@ -822,12 +833,14 @@ k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-201910010437 k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20191001043732-d647ddbd755f/go.mod h1:f1tFT2pOqPzfckbG1GjHIzy3G+T2LW7rchcruNoLaiM= k8s.io/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20191001043732-d647ddbd755f h1:X3br+JCtf40mnzQsKAnHnezd1CvCENgG5uLJTbAspZ4= k8s.io/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20191001043732-d647ddbd755f/go.mod h1:PNw+FbGH4/s3zK9V3rAeMiHTbQz2CU/yqAkfQ2UgLVs= +k8s.io/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20191001043732-d647ddbd755f h1:QIhu1g7jmiv/90qGiPiCOTHFYEcrL0HA5P/6G/pt7zM= k8s.io/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20191001043732-d647ddbd755f/go.mod h1:WmFoxjELD2xtWb77Yj9RPibT5ACkQYEW9lPQtNkGtbE= k8s.io/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20191001043732-d647ddbd755f h1:6CkT409OUoX4ZiP++1N3id3PCcOoktBvclNsDKPKrfc= k8s.io/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20191001043732-d647ddbd755f/go.mod h1:nBogvbgjMgo7AeVA6CuqVO13LVIfmlQ11t6xzAJdBN8= k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20191001043732-d647ddbd755f h1:ksJC2cpBqkCP8bzmfDYXr65JRpt9JmANvaKIR3qggt4= k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20191001043732-d647ddbd755f/go.mod h1:GiGfbsjtP4tOW6zgpL8/vCUoyXAV5+9X2onLursPi08= k8s.io/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20191001043732-d647ddbd755f/go.mod h1:L8deZCu6NpzgKzY91TOGKJ1JtAoHd8WyJ/HdoxqZCGo= +k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20191001043732-d647ddbd755f h1:fwZSUxpQ99UBEkIhHbzY2pE3SPU9Zn4yZkMSolEt6Jw= k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20191001043732-d647ddbd755f/go.mod h1:spPP+vRNS8EsnNNIhFCZTTuRO3XhV1WoF18HJySoZn8= k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20191001043732-d647ddbd755f h1:vH4+rTRLDI8z9dQCZ6cJcIi3RMGZ6JwJWyLbrSNHBCE= k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20191001043732-d647ddbd755f/go.mod h1:ellVfoCz8MlDjTnkqsTkU5svJOIjcK3XNx/onmixgDk= @@ -845,6 +858,7 @@ rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/src/jetstream/load_plugins.go b/src/jetstream/load_plugins.go index 188aef7b7c..861eb8c81b 100644 --- a/src/jetstream/load_plugins.go +++ b/src/jetstream/load_plugins.go @@ -7,12 +7,14 @@ import ( "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cfappssh" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cloudfoundry" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cloudfoundryhosting" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/kubernetes" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/metrics" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/monocular" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userfavorites" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userinfo" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/userinvite" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" log "github.com/sirupsen/logrus" ) @@ -39,6 +41,7 @@ func (pp *portalProxy) loadPlugins() { {"monocular", monocular.Init}, {"userfavorites", userfavorites.Init}, {"autoscaler", autoscaler.Init}, + {"analysis", analysis.Init}, {"backup", backup.Init}, } { plugin, err := p.Init(pp) diff --git a/src/jetstream/plugins/analysis/20200210105400_Analysis.go b/src/jetstream/plugins/analysis/20200210105400_Analysis.go new file mode 100644 index 0000000000..f27d6bf873 --- /dev/null +++ b/src/jetstream/plugins/analysis/20200210105400_Analysis.go @@ -0,0 +1,43 @@ +package analysis + +import ( + "database/sql" + + "bitbucket.org/liamstask/goose/lib/goose" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" +) + +func init() { + datastore.RegisterMigration(20200210105400, "Analysis", func(txn *sql.Tx, conf *goose.DBConf) error { + + createAnalysisTabls := "CREATE TABLE IF NOT EXISTS analysis (" + createAnalysisTabls += "id VARCHAR(255) NOT NULL," + createAnalysisTabls += "endpoint VARCHAR(36) NOT NULL," + createAnalysisTabls += "endpoint_type VARCHAR(36) NOT NULL," + createAnalysisTabls += "name VARCHAR(255) NOT NULL," + createAnalysisTabls += "user VARCHAR(36) NOT NULL," + createAnalysisTabls += "path VARCHAR(255) NOT NULL," + createAnalysisTabls += "type VARCHAR(64) NOT NULL," + createAnalysisTabls += "format VARCHAR(64) NOT NULL," + createAnalysisTabls += "created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP," + createAnalysisTabls += "acknowledged BOOLEAN NOT NULL DEFAULT FALSE," + createAnalysisTabls += "status VARCHAR(16) NOT NULL," + createAnalysisTabls += "duration INT NOT NULL DEFAULT 0," + createAnalysisTabls += "result VARCHAR(255) NOT NULL," + createAnalysisTabls += "PRIMARY KEY (id) );" + + _, err := txn.Exec(createAnalysisTabls) + if err != nil { + return err + } + + // createIndex := "CREATE INDEX charts_id ON charts (id);" + // _, err = txn.Exec(createIndex) + // if err != nil { + // return err + // } + + return nil + }) +} diff --git a/src/jetstream/plugins/analysis/container/Dockerfile b/src/jetstream/plugins/analysis/container/Dockerfile new file mode 100644 index 0000000000..b9863a323a --- /dev/null +++ b/src/jetstream/plugins/analysis/container/Dockerfile @@ -0,0 +1,61 @@ +FROM splatform/stratos-bk-build-base:leap15_1 as builder + +# Build the API Server for the analysis engines + +RUN mkdir -p /home/stratos/go/src +WORKDIR /home/stratos/go/src +COPY --chown=stratos:users . /home/stratos/go/src +ARG VERSION=1.0.0 +RUN GO111MODULE=on go build -o stratos-analyzers -ldflags -X=main.appVersion=${VERSION} + +# Download the Analysis tools +WORKDIR /home/stratos/analysis +WORKDIR /home/stratos/tmp +USER root + +# Analyzers ==================================================================================================================== + + +# Popeye +ARG POPEYE_VERSION=0.6.2 +# Download archive - popeye executable is in main dir - move it to the analysis folder +RUN wget https://github.com/derailed/popeye/releases/download/v${POPEYE_VERSION}/popeye_${POPEYE_VERSION}_Linux_x86_64.tar.gz && \ + tar -xvf popeye_${POPEYE_VERSION}_Linux_x86_64.tar.gz && \ + mv popeye ../analysis + +# Kube-score +ARG KUBESCORE_VERSION=1.5.0 +RUN wget https://github.com/zegl/kube-score/releases/download/v${KUBESCORE_VERSION}/kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz && \ + tar -xvf kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz && \ + mv kube-score ../analysis + +# Sonobuoy +# ARG SONOBUOY_VERSION=0.17.2 +# RUN wget https://github.com/vmware-tanzu/sonobuoy/releases/download/v${SONOBUOY_VERSION}/sonobuoy_${SONOBUOY_VERSION}_linux_amd64.tar.gz && \ +# tar -xvf sonobuoy_${SONOBUOY_VERSION}_linux_amd64.tar.gz && \ +# mv sonobuoy ../analysis + +# Need kubectl for Kubescore - TODO: Use correct version depending on cluster +ARG KUBECTL_VERSION=1.16.2 +RUN wget https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \ + chmod +x kubectl && \ + mv kubectl ../analysis + +# klar +# ARG KLAR_VERSION=2.4.0 +# RUN wget https://github.com/optiopay/klar/releases/download/v${KLAR_VERSION}/klar-${KLAR_VERSION}-linux-amd64 && \ +# mv klar-${KLAR_VERSION}-linux-amd64 klar && \ +# chmod +x klar && \ +# mv klar ../analysis + +# Final Container ============================================================================================================= + +FROM splatform/stratos-bk-base:leap15_1 + +# Copy tools to the /usr/bin folder so that they are in the path +COPY --from=builder /home/stratos/analysis /usr/bin +COPY --from=builder /home/stratos/go/src/stratos-analyzers /stratos-analyzers +COPY ./scripts /scripts +RUN mkdir /reports + +CMD ["/stratos-analyzers"] diff --git a/src/jetstream/plugins/analysis/container/go.mod b/src/jetstream/plugins/analysis/container/go.mod new file mode 100644 index 0000000000..d9101c6c6c --- /dev/null +++ b/src/jetstream/plugins/analysis/container/go.mod @@ -0,0 +1,12 @@ +module analyzers + +go 1.13 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/labstack/echo v3.3.10+incompatible + github.com/labstack/gommon v0.3.0 // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/valyala/fasttemplate v1.1.0 // indirect + golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect +) diff --git a/src/jetstream/plugins/analysis/container/go.sum b/src/jetstream/plugins/analysis/container/go.sum new file mode 100644 index 0000000000..ae076adf3a --- /dev/null +++ b/src/jetstream/plugins/analysis/container/go.sum @@ -0,0 +1,48 @@ +github.com/cloudfoundry-incubator/stratos v2.0.0-beta-001+incompatible h1:UUxNbLjhv2cfymub5yNN1tjjqYkteHBBagb4jcbXEIQ= +github.com/cloudfoundry-incubator/stratos/src/jetstream v0.0.0-20200222120421-390cf0f6670b h1:52Py09Cmdnyxr750Tj5InffbWJpCDTWie0RCbxxoUAA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= +github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/jetstream/plugins/analysis/container/kubescore.go b/src/jetstream/plugins/analysis/container/kubescore.go new file mode 100644 index 0000000000..959e15964b --- /dev/null +++ b/src/jetstream/plugins/analysis/container/kubescore.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" +) + +func runKubeScore(job *AnalysisJob) error { + + log.Debug("Running kube-score job") + + job.Busy = true + job.Type = "kubescore" + job.Format = "kubescore" + setJobNameAndPath(job, "Kube-score") + + scriptPath := filepath.Join(getScriptFolder(), "kubescore-runner.sh") + args := []string{scriptPath, job.KubeConfigPath, job.Config.Namespace} + + log.Infof("Running kube score job: %s", job.Path) + + go func() { + // Use our custom script which is a wrapper around kubescore + cmd := exec.Command("bash", args...) + cmd.Dir = job.Folder + cmd.Env = make([]string, 0) + cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", job.KubeConfigPath)) + + start := time.Now() + out, err := cmd.Output() + end := time.Now() + + log.Infof("Completed kube score job: %s", job.Path) + + // Remove any config files when done + job.RemoveTempFiles() + + job.Duration = int(end.Sub(start).Seconds()) + + if err != nil { + // There was an error + // Remove the folder + os.Remove(job.Folder) + job.Status = "error" + } else { + reportFile := filepath.Join(job.Folder, "report.log") + ioutil.WriteFile(reportFile, out, os.ModePerm) + job.Status = "completed" + } + }() + + return nil +} diff --git a/src/jetstream/plugins/analysis/container/main.go b/src/jetstream/plugins/analysis/container/main.go new file mode 100644 index 0000000000..533ec83786 --- /dev/null +++ b/src/jetstream/plugins/analysis/container/main.go @@ -0,0 +1,171 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" + log "github.com/sirupsen/logrus" +) + +const ( + defaultPort = 8090 + defaultAddress = "0.0.0.0" + reportsDirEnvVar = "ANALYSIS_REPORTS_DIR" + scriptsDirEnvVar = "ANALYSIS_SCRIPTS_DIR" +) + +type Analyzer struct { + reportsDir string + jobs map[string]*AnalysisJob +} + +func main() { + log.SetFormatter(&log.TextFormatter{ForceColors: true, FullTimestamp: true, TimestampFormat: time.UnixDate}) + + log.SetOutput(os.Stdout) + + log.Info("========================================") + log.Info("=== Stratos Analysis API Server ===") + log.Info("========================================") + log.Info("") + log.Info("Initialization started.") + + analyzer := Analyzer{} + analyzer.jobs = make(map[string]*AnalysisJob) + + analyzer.Start() +} + +func (a *Analyzer) Start() { + + // Reports folder + + // Init reports directory + if reportsDir, ok := os.LookupEnv(reportsDirEnvVar); ok { + dir, err := filepath.Abs(reportsDir) + if err != nil { + log.Fatal("Can not get absolute path for reports folder") + } + a.reportsDir = dir + } else { + a.reportsDir = filepath.Join(os.TempDir(), "stratos-analysis") + } + log.Infof("Using reports folder: %s", a.reportsDir) + + // Make the directory if it does not exit + if _, err := os.Stat(a.reportsDir); os.IsNotExist(err) { + if os.MkdirAll(a.reportsDir, os.ModePerm) != nil { + log.Fatal("Could not create folder for analysis reports") + } + } + + // Start a simple web server + e := echo.New() + e.HideBanner = true + e.HidePort = true + customLoggerConfig := middleware.LoggerConfig{ + Format: `Request: [${time_rfc3339}] Remote-IP:"${remote_ip}" ` + + `Method:"${method}" Path:"${path}" Status:${status} Latency:${latency_human} ` + + `Bytes-In:${bytes_in} Bytes-Out:${bytes_out}` + "\n", + } + e.Use(middleware.LoggerWithConfig(customLoggerConfig)) + e.Use(middleware.Recover()) + + a.registerRoutes(e) + + var engineErr error + address := fmt.Sprintf("%s:%d", defaultAddress, defaultPort) + log.Infof("Starting HTTP Server at address: %s", address) + engineErr = e.Start(address) + + if engineErr != nil { + engineErrStr := fmt.Sprintf("%s", engineErr) + if !strings.Contains(engineErrStr, "Server closed") { + log.Warnf("Failed to start HTTP/S server: %+v", engineErr) + } + } +} + +func (a *Analyzer) registerRoutes(e *echo.Echo) { + api := e.Group("/api") + api.Use(setSecureCacheContentMiddleware) + + // Liveness check + api.GET("/v1/ping", a.ping) + // Run the given analyzer + api.POST("/v1/run/:analyzer", a.run) + // Get status + api.POST("/v1/status", a.status) + // Get a report + api.GET("/v1/report/:user/:endpoint/:id/:file", a.report) + // Delete a report + api.DELETE("/v1/report/:user/:endpoint/:id", a.delete) + // Delete all reports for an endpoint + api.DELETE("/v1/report/:endpoint", a.deleteEndpoint) +} + +func setSecureCacheContentMiddleware(h echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + c.Response().Header().Set("cache-control", "no-store") + c.Response().Header().Set("pragma", "no-cache") + return h(c) + } +} + +// Set the name of the job +func setJobNameAndPath(job *AnalysisJob, title string) { + job.Name = fmt.Sprintf("%s cluster analysis", title) + job.Path = "" + + log.Info("setJobNameAndPath") + log.Infof("%+v", job.Config) + + if job.Config != nil { + if len(job.Config.Namespace) > 0 { + if len(job.Config.App) > 0 { + job.Name = fmt.Sprintf("%s workload analysis: %s in %s", title, job.Config.App, job.Config.Namespace) + job.Path = fmt.Sprintf("%s/%s", job.Config.Namespace, job.Config.App) + } else { + job.Name = fmt.Sprintf("%s namespace analysis: %s", title, job.Config.Namespace) + job.Path = job.Config.Namespace + } + } + } +} + +func getScriptFolder() string { + fallbackPath, err := os.Getwd() + if err != nil { + fallbackPath = "." + } + + // Look first at the env var, then at a relative path to the executable + if dir, ok := os.LookupEnv(scriptsDirEnvVar); ok { + return dir + } + + // Relative to the executable + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + log.Error("Could not get folder of the running program") + return fallbackPath + } + + scripts := filepath.Join(dir, "scripts") + if _, err := os.Stat(scripts); !os.IsNotExist(err) { + return scripts + } + + scripts = filepath.Join(dir, "plugins±", "analysis", "container", "scripts") + if _, err := os.Stat(scripts); !os.IsNotExist(err) { + return scripts + } + + log.Error("Unable to locate scripts folder") + return fallbackPath +} diff --git a/src/jetstream/plugins/analysis/container/popeye.go b/src/jetstream/plugins/analysis/container/popeye.go new file mode 100644 index 0000000000..209224a92e --- /dev/null +++ b/src/jetstream/plugins/analysis/container/popeye.go @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/json" + "errors" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" +) + +type popEyeSummary struct { + Score int `json:"score"` + Grade string `json:"grade"` +} + +type popEyeResult struct { + PopEye popEyeSummary `json:"popeye"` +} + +func runPopeye(job *AnalysisJob) error { + + log.Debug("Running popeye job") + + job.Busy = true + job.Type = "popeye" + job.Format = "popeye" + setJobNameAndPath(job, "Popeye") + + log.Infof("Running popeye job: %s", job.Path) + + args := []string{"--kubeconfig", job.KubeConfigPath, "-o", "json", "--insecure-skip-tls-verify"} + if len(job.Config.Namespace) > 0 { + args = append(args, "-n") + args = append(args, job.Config.Namespace) + } else { + args = append(args, "-A") + } + + go func() { + cmd := exec.Command("popeye", args...) + cmd.Dir = job.Folder + + start := time.Now() + out, err := cmd.Output() + end := time.Now() + job.EndTime = end + + job.Busy = false + + log.Infof("Completed kube score job: %s", job.Path) + + // Remove any config files when done + job.RemoveTempFiles() + + job.Duration = int(end.Sub(start).Seconds()) + + if err != nil { + // There was an error + // Remove the folder + os.Remove(job.Folder) + job.Status = "error" + } else { + reportFile := filepath.Join(job.Folder, "report.json") + ioutil.WriteFile(reportFile, out, os.ModePerm) + job.Status = "completed" + + // Parse the report + if summary, err := parsePopeyeReport(reportFile); err == nil { + job.Result = serializePopeyeReport(summary) + } + } + }() + + return nil +} + +func parsePopeyeReport(file string) (*popEyeSummary, error) { + jsonFile, err := os.Open(file) + if err != nil { + return nil, err + } + defer jsonFile.Close() + + data, err := ioutil.ReadAll(jsonFile) + if err != nil { + return nil, err + } + + result := popEyeResult{} + if err = json.Unmarshal(data, &result); err != nil { + return nil, errors.New("Failed to parse Popeye report") + } + + return &result.PopEye, nil +} + +func serializePopeyeReport(summary *popEyeSummary) string { + jsonString, err := json.Marshal(summary) + if err != nil { + return "" + } + + return string(jsonString) +} diff --git a/src/jetstream/plugins/analysis/container/routes.go b/src/jetstream/plugins/analysis/container/routes.go new file mode 100644 index 0000000000..3b3c846e2b --- /dev/null +++ b/src/jetstream/plugins/analysis/container/routes.go @@ -0,0 +1,90 @@ +package main + +import ( + "errors" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +// Ping endpoint +func (a *Analyzer) ping(ec echo.Context) error { + return nil +} + +// Get a given report +func (a *Analyzer) report(ec echo.Context) error { + + user := ec.Param("user") + endpoint := ec.Param("endpoint") + id := ec.Param("id") + name := ec.Param("file") + + // Name must end in json - we only serve json files + if !strings.HasSuffix(name, ".json") { + return errors.New("Can't serve that file") + } + + file := filepath.Join(a.reportsDir, user, endpoint, id, name) + _, err := os.Stat(file) + if os.IsNotExist(err) { + return echo.NewHTTPError(404, "No such file") + } + + return ec.File(file) +} + +// Delete a given report +func (a *Analyzer) delete(ec echo.Context) error { + log.Debug("delete report") + + user := ec.Param("user") + endpoint := ec.Param("endpoint") + id := ec.Param("id") + folder := filepath.Join(a.reportsDir, user, endpoint, id) + if err := os.RemoveAll(folder); err != nil { + log.Warnf("Could not delete Analysis report folder: %s", folder) + return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete report") + } + + return nil +} + +// Delete all reports for a given endpoint +func (a *Analyzer) deleteEndpoint(ec echo.Context) error { + log.Debug("delete reports for endpoint") + + endpoint := ec.Param("endpoint") + + // Iterate over all user folders + if items, err := ioutil.ReadDir(a.reportsDir); err == nil { + for _, item := range items { + if item.IsDir() { + // This is a user's folder - see if they have a folder for the endpoint + folder := filepath.Join(a.reportsDir, item.Name(), endpoint) + if folderExists(folder) { + if err := os.RemoveAll(folder); err != nil { + log.Warnf("Could not delete Analysis report endpoint folder: %s", folder) + } + } + } + } + } else { + return echo.NewHTTPError(http.StatusInternalServerError, "Error deleteing reports") + } + + return nil +} + +func folderExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return info.IsDir() +} diff --git a/src/jetstream/plugins/analysis/container/run.go b/src/jetstream/plugins/analysis/container/run.go new file mode 100644 index 0000000000..06310a41eb --- /dev/null +++ b/src/jetstream/plugins/analysis/container/run.go @@ -0,0 +1,130 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +const idHeaderName = "X-Stratos-Analaysis-ID" + +func (a *Analyzer) run(ec echo.Context) error { + err := a.doRun(ec) + if err != nil { + log.Error(err) + } + return err +} + +func (a *Analyzer) doRun(ec echo.Context) error { + + log.Debug("Run analyzer!") + + engine := ec.Param("analyzer") + if len(engine) == 0 { + log.Warn("No analyzer") + return errors.New("No analyzer specified") + } + + // ID is username/endpoint/id + id := ec.Request().Header.Get(idHeaderName) + if len(id) == 0 { + return errors.New("Mising ID header") + } + + folder := filepath.Join(a.reportsDir, id) + if os.MkdirAll(folder, os.ModePerm) != nil { + return errors.New("Could not create folder for analysis report") + } + + tempFiles := make([]string, 0) + reader, err := ec.Request().MultipartReader() + if err != nil { + log.Error("Could not parse request") + log.Error(err) + return errors.New("Failed to parse request payload") + } + + job := AnalysisJob{} + params := kubeAnalyzerConfig{} + + for { + part, err := reader.NextPart() + if err == io.EOF { + break + } + if err != nil { + log.Error("Unexpected error when retrieving a part of the message") + return errors.New("Unexpected error when retrieving a part of the message") + } + defer part.Close() + fileBytes, err := ioutil.ReadAll(part) + if err != nil { + log.Error("Failed to read content of the part") + return errors.New("Failed to read content of the part") + } + filename := part.Header.Get("Content-ID") + + // Decide what to do with the part + switch filename { + case "job": + if err = json.Unmarshal(fileBytes, &job); err != nil { + return fmt.Errorf("Can not parse Job: %v", err) + } + case "body": + if err = json.Unmarshal(fileBytes, ¶ms); err != nil { + return fmt.Errorf("Can not parse parameters: %v", err) + } + job.Config = ¶ms + default: + fullpath := filepath.Join(folder, filename) + if err = ioutil.WriteFile(fullpath, fileBytes, os.ModePerm); err != nil { + log.Error("Could not write data for: %s", filename) + return fmt.Errorf("Could not write file data for: %s", filename) + } + if filename == "kubeconfig" { + job.KubeConfigPath = fullpath + } + tempFiles = append(tempFiles, fullpath) + } + } + + if len(job.ID) == 0 { + return errors.New("Invalid Job metadata supplied") + } + + job.Folder = folder + job.TempFiles = tempFiles + + // Store the job so we track which jobs are running + a.jobs[job.ID] = &job + + job.Status = "running" + + switch engine { + case "popeye": + err = runPopeye(&job) + case "kube-score": + err = runKubeScore(&job) + // case "sonobuoy": + // runSonobuoy(dbStore, file, folder, report, requestBody) + default: + job.Status = "error" + return fmt.Errorf("Unkown analyzer: %s", engine) + } + + if err != nil { + job.Status = "error" + log.Error("Error running analyzer: %s", err) + } + + return ec.JSON(http.StatusOK, job) +} diff --git a/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh b/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh new file mode 100755 index 0000000000..2763b3008f --- /dev/null +++ b/src/jetstream/plugins/analysis/container/scripts/kubescore-runner.sh @@ -0,0 +1,16 @@ +ARGS="--all-namespaces" + +if [ -n "$2" ]; then + ARGS="-n ${2}" +fi + +# $1 is the kubeconfig file + +echo "Kubescore runner..." +echo "Running report..." + +kubectl api-resources --verbs=list --namespaced -o name \ + | xargs -n1 -I{} bash -c "kubectl get {} $ARGS -oyaml && echo ---" \ + | kube-score score -o json - > report.json + +exit 0 diff --git a/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh b/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh new file mode 100755 index 0000000000..8565beed6f --- /dev/null +++ b/src/jetstream/plugins/analysis/container/scripts/sonobuoy-runner.sh @@ -0,0 +1,19 @@ +# $1 is the kubeconfig file + +echo "Sonobuoy runner..." +env +echo "Args" +echo $@ + +echo "Running report..." + +# Run the report and wait +sonobuoy run --wait + +# Retrieve the report + +# Teardown sonobuoy + +# Unpack the report and copy the junit report to report.json at the top-level + +exit 0 diff --git a/src/jetstream/plugins/analysis/container/sonobuoy.go_ b/src/jetstream/plugins/analysis/container/sonobuoy.go_ new file mode 100644 index 0000000000..80be589427 --- /dev/null +++ b/src/jetstream/plugins/analysis/container/sonobuoy.go_ @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store" + log "github.com/sirupsen/logrus" +) + +func runSonobuoy(dbStore store.AnalysisStore, kubeconfig, folder string, report store.AnalysisRecord, body []byte) error { + path := "" + namespace := "" + options := &popeyeConfig{} + if err := json.Unmarshal(body, options); err == nil { + namespace = options.Namespace + path = namespace + + if len(options.App) > 0 { + path = fmt.Sprintf("%s/%s", path, options.App) + } + } + report.Name = "Sonobuoy cluster analysis" + report.Type = "sonobuoy" + report.Format = "junit" + + scriptPath := filepath.Join(getScriptFolder(), "sonobuoy-runner.sh") + args := []string{scriptPath, kubeconfig, namespace} + log.Error(scriptPath) + + report.Path = path + parts := len(strings.Split(path, "/")) + if parts == 2 { + report.Name = fmt.Sprintf("Sonobuoy workload analysis: %s in %s", options.App, namespace) + } else if parts == 1 && len(namespace) > 0 { + report.Name = fmt.Sprintf("Sonobuoy namespace analysis: %s", namespace) + } + + _, err := dbStore.Save(report) + if err != nil { + return err + } + + go func() { + // Use our custom script which is a wrapper around kubescore + cmd := exec.Command("bash", args...) + cmd.Dir = folder + cmd.Env = make([]string, 0) + cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", kubeconfig)) + log.Info(kubeconfig) + + start := time.Now() + out, err := cmd.Output() + end := time.Now() + + // Remove the config file when we are done + //os.Remove(kubeconfig) + + if err != nil { + // There was an error + // Remove the folder + os.Remove(folder) + log.Error(">>>>>>>>> ERROR <<<<<<<<<") + log.Error(string(out)) + log.Error(err) + report.Status = "error" + } else { + report.Status = "completed" + + // Parse the report + // if summary, err := parsePopeyeReport(reportFile); err == nil { + // report.Result = serializePopeyeReport(summary) + // } + + // Write stdout to log file + reportFile := filepath.Join(folder, "report.log") + ioutil.WriteFile(reportFile, out, os.ModePerm) + } + + report.Duration = int(end.Sub(start).Seconds()) + + dbStore.UpdateReport(report.UserID, &report) + }() + + return nil +} diff --git a/src/jetstream/plugins/analysis/container/status.go b/src/jetstream/plugins/analysis/container/status.go new file mode 100644 index 0000000000..52ad376502 --- /dev/null +++ b/src/jetstream/plugins/analysis/container/status.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/json" + "errors" + "io/ioutil" + + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +func (a *Analyzer) status(ec echo.Context) error { + err := a.doStatus(ec) + if err != nil { + log.Error(err) + } + return err +} + +func (a *Analyzer) doStatus(ec echo.Context) error { + log.Debug("Status") + req := ec.Request() + + // Body contains an array of IDs that the client thinks are running + // We send back updated status for each + + // Get the list of IDs + defer req.Body.Close() + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return errors.New("Could not read body") + } + + ids := make([]string, 0) + if err := json.Unmarshal(body, &ids); err != nil { + return errors.New("Failed to parse body") + } + + response := make(map[string]AnalysisJob) + for _, id := range ids { + if a.jobs[id] == nil { + // Client has a running job that we know nothing about - so must be an error + job := AnalysisJob{ + ID: id, + Status: "error", + } + response[id] = job + } else { + response[id] = *a.jobs[id] + } + } + + // Go through all of the jobs we have and increment the cleanup counter of those that are finished + // Assume after 5 requests to the status API that the caller has the info they need for the completed job + // and remove it + cleanup := make([]string, 0) + for id, job := range a.jobs { + // If the job has finished, increment the cleanup counter + // We will remove it from our cache once we are pretty sure Jetstream has the status + if !job.Busy { + job.CleanupCounter = job.CleanupCounter + 1 + if job.CleanupCounter > 5 { + cleanup = append(cleanup, id) + } + } + } + + for _, id := range cleanup { + delete(a.jobs, id) + } + + ec.JSON(200, response) + return nil +} diff --git a/src/jetstream/plugins/analysis/container/types.go b/src/jetstream/plugins/analysis/container/types.go new file mode 100644 index 0000000000..fb9de49c8c --- /dev/null +++ b/src/jetstream/plugins/analysis/container/types.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "os" + "time" + + log "github.com/sirupsen/logrus" +) + +type kubeAnalyzerConfig struct { + Namespace string `json:"namespace"` + App string `json:"app"` +} + +// AnalysisJob is the metadata format sent to and from the analyzer +type AnalysisJob struct { + ID string `json:"id"` + UserID string `json:"-"` + EndpointType string `json:"endpointType"` + EndpointID string `json:"endpoint"` + Type string `json:"type"` + Path string `json:"path"` + Format string `json:"format"` + Name string `json:"name"` + Status string `json:"status"` + Duration int `json:"duration"` + Result string `json:"-"` + Summary *json.RawMessage `json:"summary"` + Config *kubeAnalyzerConfig `json:"-"` + Folder string `json:"-"` + KubeConfigPath string `json:"-"` + TempFiles []string `json:"-"` + Busy bool `json:"-"` + EndTime time.Time `json:"-"` + CleanupCounter int `json:"-"` +} + +// RemoveTempFiles will remove any temporary files +func (job *AnalysisJob) RemoveTempFiles() { + log.Debug("Removing temporary files") + for _, name := range job.TempFiles { + err := os.Remove(name) + if err != nil { + log.Error("Could not delete file: %s", name) + } + } +} diff --git a/src/jetstream/plugins/analysis/list.go b/src/jetstream/plugins/analysis/list.go new file mode 100644 index 0000000000..c03f7af4fe --- /dev/null +++ b/src/jetstream/plugins/analysis/list.go @@ -0,0 +1,228 @@ +package analysis + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store" + + "github.com/labstack/echo" + + log "github.com/sirupsen/logrus" +) + +const mainReportFile = "report.json" + +// listReports will list the analysis repotrs that have run +func (c *Analysis) listReports(ec echo.Context) error { + log.Debug("listReports") + var p = c.portalProxy + + // Need to get a config object for the target endpoint + // endpointGUID := ec.Param("endpoint") + userID := ec.Get("user_id").(string) + endpointID := ec.Param("endpoint") + + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + reports, err := dbStore.List(userID, endpointID) + if err != nil { + return err + } + + for _, report := range reports { + populateSummary(report) + } + + return ec.JSON(200, reports) +} + +// getReportsByPath will list the completed analysis repotrs that have run for the specified path +func (c *Analysis) getReportsByPath(ec echo.Context) error { + log.Debug("getReportsByPath") + var p = c.portalProxy + + // Need to get a config object for the target endpoint + userID := ec.Get("user_id").(string) + endpointID := ec.Param("endpoint") + + pathPrefix := fmt.Sprintf("completed/%s/", endpointID) + index := strings.Index(ec.Request().RequestURI, pathPrefix) + if index < 0 { + return errors.New("Invalid request") + } + path := ec.Request().RequestURI[index+len(pathPrefix):] + + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + reports, err := dbStore.ListCompletedByPath(userID, endpointID, path) + if err != nil { + return err + } + + for _, report := range reports { + populateSummary(report) + } + + return ec.JSON(200, reports) +} + +func populateSummary(report *store.AnalysisRecord) { + if report.Status == "error" { + report.Error = report.Result + } else if len(report.Result) > 0 { + data := []byte(report.Result) + report.Summary = (*json.RawMessage)(&data) + } +} + +func (c *Analysis) getLatestReport(ec echo.Context) error { + log.Debug("getLatestReport") + var p = c.portalProxy + + // Need to get a config object for the target endpoint + userID := ec.Get("user_id").(string) + endpointID := ec.Param("endpoint") + + pathPrefix := fmt.Sprintf("latest/%s/", endpointID) + index := strings.Index(ec.Request().RequestURI, pathPrefix) + if index < 0 { + return errors.New("Invalid request") + } + path := ec.Request().RequestURI[index+len(pathPrefix):] + + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + report, err := dbStore.GetLatestCompleted(userID, endpointID, path) + if err != nil { + return echo.NewHTTPError(404, "No Analysis Report found") + } + + if ec.Request().Method == "HEAD" { + ec.Response().Status = 200 + return nil + } + + // Get the report contents from the analysis server + bytes, err := c.getReportFile(report.UserID, report.EndpointID, report.ID, mainReportFile) + if err != nil { + return err + } + + report.Report = (*json.RawMessage)(&bytes) + return ec.JSON(200, report) +} + +func (c *Analysis) getReport(ec echo.Context) error { + log.Debug("getReport") + var p = c.portalProxy + + // Need to get a config object for the target endpoint + userID := ec.Get("user_id").(string) + ID := ec.Param("id") + file := ec.Param("file") + if len(file) == 0 { + file = mainReportFile + } + + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + report, err := dbStore.Get(userID, ID) + if err != nil { + return err + } + + // Get the report contents from the analysis server + bytes, err := c.getReportFile(report.UserID, report.EndpointID, report.ID, file) + if err != nil { + return err + } + + report.Report = (*json.RawMessage)(&bytes) + return ec.JSON(200, report) +} + +func (c *Analysis) deleteReports(ec echo.Context) error { + log.Debug("deleteReports") + var p = c.portalProxy + + // Need to get a config object for the target endpoint + userID := ec.Get("user_id").(string) + + defer ec.Request().Body.Close() + body, err := ioutil.ReadAll(ec.Request().Body) + if err != nil { + return err + } + + var ids []string + ids = make([]string, 0) + if err = json.Unmarshal(body, &ids); err != nil { + return err + } + + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + for _, id := range ids { + // Look up the report to get the endpoint ID + if job, err := dbStore.Get(userID, id); err == nil { + deleteURL := fmt.Sprintf("%s/api/v1/report/%s/%s/%s", c.analysisServer, job.UserID, job.EndpointID, job.ID) + r, _ := http.NewRequest(http.MethodDelete, deleteURL, nil) + client := &http.Client{Timeout: 30 * time.Second} + rsp, err := client.Do(r) + if err != nil { + log.Warnf("Could not delete analysis report for: %s", job.ID) + } else if rsp.StatusCode != http.StatusOK { + log.Warnf("Could not delete analysis report for: %s", job.ID) + } + } + dbStore.Delete(userID, id) + } + + return ec.JSON(200, ids) +} + +func (c *Analysis) getReportFile(userID, endpointID, ID, name string) ([]byte, error) { + // Make request to get report + statusURL := fmt.Sprintf("%s/api/v1/report/%s/%s/%s/%s", c.analysisServer, userID, endpointID, ID, name) + r, _ := http.NewRequest(http.MethodGet, statusURL, nil) + client := &http.Client{Timeout: 30 * time.Second} + rsp, err := client.Do(r) + if err != nil { + return nil, fmt.Errorf("Failed getting report from Analyzer service: %v", err) + } else if rsp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Failed getting report from Analyzer service: %d", rsp.StatusCode) + } + + defer rsp.Body.Close() + response, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return nil, fmt.Errorf("Could not read response: %v", err) + } + + return response, nil +} diff --git a/src/jetstream/plugins/analysis/main.go b/src/jetstream/plugins/analysis/main.go new file mode 100644 index 0000000000..33dbc43917 --- /dev/null +++ b/src/jetstream/plugins/analysis/main.go @@ -0,0 +1,139 @@ +package analysis + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +const ( + analsyisServicesAPIEnvVar = "ANALYSIS_SERVICES_API" + + // Allow specific engines to be enabled + analysisEnginesAPIEnvVar = "ANALYSIS_ENGINES" + + // Names used to communicate settings info back to the front-end client + analysisEnabledPluginConfigSetting = "analysisEnabled" + analysisEnginesPluginConfigSetting = "analysisEngines" + + defaultEngines = "popeye" +) + +// Analysis - Plugin to allow analysers to run over an endpoint cluster +type Analysis struct { + portalProxy interfaces.PortalProxy + analysisServer string +} + +// Init creates a new Analysis +func Init(portalProxy interfaces.PortalProxy) (interfaces.StratosPlugin, error) { + store.InitRepositoryProvider(portalProxy.GetConfig().DatabaseProviderName) + return &Analysis{portalProxy: portalProxy}, nil +} + +// GetMiddlewarePlugin gets the middleware plugin for this plugin +func (analysis *Analysis) GetMiddlewarePlugin() (interfaces.MiddlewarePlugin, error) { + return nil, errors.New("Not implemented") +} + +// GetEndpointPlugin gets the endpoint plugin for this plugin +func (analysis *Analysis) GetEndpointPlugin() (interfaces.EndpointPlugin, error) { + return nil, errors.New("Not implemented") +} + +// GetRoutePlugin gets the route plugin for this plugin +func (analysis *Analysis) GetRoutePlugin() (interfaces.RoutePlugin, error) { + return analysis, nil +} + +// AddAdminGroupRoutes adds the admin routes for this plugin to the Echo server +func (analysis *Analysis) AddAdminGroupRoutes(echoGroup *echo.Group) { + // no-op +} + +// AddSessionGroupRoutes adds the session routes for this plugin to the Echo server +func (analysis *Analysis) AddSessionGroupRoutes(echoGroup *echo.Group) { + echoGroup.GET("/analysis/reports/:endpoint", analysis.listReports) + echoGroup.GET("/analysis/reports/:endpoint/:id", analysis.getReport) + echoGroup.GET("/analysis/reports/:endpoint/:id/:file", analysis.getReport) + + // Get completed reports for the given path + echoGroup.GET("/analysis/completed/:endpoint/*", analysis.getReportsByPath) + + // Get latest report + echoGroup.GET("/analysis/latest/:endpoint/*", analysis.getLatestReport) + echoGroup.HEAD("/analysis/latest/:endpoint/*", analysis.getLatestReport) + + echoGroup.DELETE("/analysis/reports", analysis.deleteReports) + + // Run report + echoGroup.POST("/analysis/run/:analyzer/:endpoint", analysis.runReport) +} + +// Init performs plugin initialization +func (analysis *Analysis) Init() error { + // Only enabled in tech preview + if !analysis.portalProxy.GetConfig().EnableTechPreview { + // This will set PluginsStatus[name] = false, which results in plugins[name] in the FE + return errors.New("Requires tech preview") + } + + // Check env var + if url, ok := analysis.portalProxy.Env().Lookup(analsyisServicesAPIEnvVar); ok { + analysis.analysisServer = url + + // Start background status check + analysis.initStatusCheck() + + if engines, ok := analysis.portalProxy.Env().Lookup(analysisEnginesAPIEnvVar); ok { + analysis.portalProxy.GetConfig().PluginConfig[analysisEnginesPluginConfigSetting] = engines + } else { + analysis.portalProxy.GetConfig().PluginConfig[analysisEnginesPluginConfigSetting] = defaultEngines + } + + return nil + } + + return errors.New("Analysis services API Server not configured") +} + +// OnEndpointNotification called when for endpoint events +func (analysis *Analysis) OnEndpointNotification(action interfaces.EndpointAction, endpoint *interfaces.CNSIRecord) { + if action == interfaces.EndpointUnregisterAction { + // An endpoint was unregistered, so remove all reports + dbStore, err := store.NewAnalysisDBStore(analysis.portalProxy.GetDatabaseConnection()) + if err == nil { + dbStore.DeleteForEndpoint(endpoint.GUID) + + // Now ask the analysis engine to to delete all files on disk + deleteURL := fmt.Sprintf("%s/api/v1/report/%s", analysis.analysisServer, endpoint.GUID) + r, _ := http.NewRequest(http.MethodDelete, deleteURL, nil) + client := &http.Client{Timeout: 30 * time.Second} + rsp, err := client.Do(r) + if err != nil { + log.Errorf("Failed deleting reports from Analyzer service: %v", err) + return + } + + if rsp.StatusCode != http.StatusOK { + log.Errorf("Failed deleting reports from Analyzer service: %d", rsp.StatusCode) + } + + if rsp.Body != nil { + defer rsp.Body.Close() + _, err = ioutil.ReadAll(rsp.Body) + if err != nil { + log.Errorf("Could not read response: %v", err) + } + } + } + } +} diff --git a/src/jetstream/plugins/analysis/run.go b/src/jetstream/plugins/analysis/run.go new file mode 100644 index 0000000000..7c64e569ed --- /dev/null +++ b/src/jetstream/plugins/analysis/run.go @@ -0,0 +1,188 @@ +package analysis + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "mime/multipart" + "net/http" + "net/textproto" + "time" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store" + + "github.com/labstack/echo" + uuid "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" +) + +type popeyeConfig struct { + Namespace string `json:"namespace"` + App string `json:"app"` +} + +type KubeConfigExporter interface { + GetKubeConfigForEndpointUser(endpointID, userID string) (string, error) +} + +const idHeaderName = "X-Stratos-Analaysis-ID" + +func (c *Analysis) runReport(ec echo.Context) error { + log.Debug("runReport") + + analyzer := ec.Param("analyzer") + endpointID := ec.Param("endpoint") + userID := ec.Get("user_id").(string) + + // Look up the endpoint for the user + var p = c.portalProxy + endpoint, err := p.GetCNSIRecord(endpointID) + if err != nil { + return errors.New("Could not get endpoint information") + } + + report := store.AnalysisRecord{ + ID: uuid.NewV4().String(), + EndpointID: endpointID, + EndpointType: endpoint.CNSIType, + UserID: userID, + Path: "", + Created: time.Now(), + Read: false, + Duration: 0, + Status: "pending", + Result: "", + } + + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return err + } + + report.Name = fmt.Sprintf("Analysis report %s", analyzer) + dbStore.Save((report)) + + err = c.doRunReport(ec, analyzer, endpointID, userID, dbStore, &report) + if err != nil { + report.Status = "error" + report.Result = err.Error() + dbStore.UpdateReport(userID, &report) + } + + return err + +} + +func (c *Analysis) doRunReport(ec echo.Context, analyzer, endpointID, userID string, dbStore store.AnalysisStore, report *store.AnalysisRecord) error { + + // Get Kube Config + k8s := c.portalProxy.GetPlugin("kubernetes") + if k8s == nil { + return errors.New("Could not find Kubernetes plugin") + } + + k8sConfig, ok := k8s.(KubeConfigExporter) + if !ok { + return errors.New("Could not find Kubernetes plugin interface") + } + + config, err := k8sConfig.GetKubeConfigForEndpointUser(endpointID, userID) + if err != nil { + return errors.New("Could not get Kube Config for the endpoint") + } + + id := fmt.Sprintf("%s/%s/%s", userID, endpointID, report.ID) + + // Create a multi-part form to send to the analyzer container + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + // Add kube config + metadataHeader := textproto.MIMEHeader{} + metadataHeader.Set("Content-Type", "application/yaml") + metadataHeader.Set("Content-ID", "kubeconfig") + part, _ := writer.CreatePart(metadataHeader) + part.Write([]byte(config)) + + requestBody := make([]byte, 0) + + // Read body + defer ec.Request().Body.Close() + if b, err := ioutil.ReadAll((ec.Request().Body)); err == nil { + requestBody = b + } + + // Content that was posted to us + postHeader := textproto.MIMEHeader{} + postHeader.Set("Content-Type", "application/json") + postHeader.Set("Content-ID", "body") + part, _ = writer.CreatePart(postHeader) + part.Write(requestBody) + + // Report config + reportHeader := textproto.MIMEHeader{} + reportHeader.Set("Content-Type", "application/json") + reportHeader.Set("Content-ID", "job") + part, _ = writer.CreatePart(reportHeader) + job, err := json.Marshal(report) + if err != nil { + return errors.New("Could not serialize job") + } + part.Write(job) + writer.Close() + + // Post this to the Analyzer API + contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary()) + uploadURL := fmt.Sprintf("%s/api/v1/run/%s", c.analysisServer, analyzer) + r, _ := http.NewRequest(http.MethodPost, uploadURL, bytes.NewReader(body.Bytes())) + r.Header.Set("Content-Type", contentType) + r.Header.Set(idHeaderName, id) + client := &http.Client{Timeout: 180 * time.Second} + rsp, err := client.Do(r) + if err != nil { + return errors.New("Analysis job failed - could not contact Analysis Server") + } + + if rsp.StatusCode != http.StatusOK { + log.Debugf("Request failed with response code: %d", rsp.StatusCode) + return fmt.Errorf("Analysis job failed with response code: %d", rsp.StatusCode) + } + + // Job submitted okay + // Updated job is in the response + + defer rsp.Body.Close() + response, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return errors.New("Could not read response") + } + + updatedJob := store.AnalysisRecord{} + if err = json.Unmarshal(response, &updatedJob); err != nil { + return errors.New("Could not read response - could not deserialize response") + } + + report.Duration = updatedJob.Duration + report.Status = updatedJob.Status + report.Name = updatedJob.Name + report.Format = updatedJob.Format + report.Type = updatedJob.Type + report.Path = updatedJob.Path + + log.Debug("OK => Job submitted okay") + log.Debug("=======================================================") + log.Debugf("%+v", report) + log.Debug("=======================================================") + + err = dbStore.UpdateReport(userID, report) + if err != nil { + return fmt.Errorf("Could not save report %s", err) + } + + log.Debug("All done - job saved") + + return ec.JSON(200, report) +} diff --git a/src/jetstream/plugins/analysis/status.go b/src/jetstream/plugins/analysis/status.go new file mode 100644 index 0000000000..5635589289 --- /dev/null +++ b/src/jetstream/plugins/analysis/status.go @@ -0,0 +1,109 @@ +package analysis + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/analysis/store" + + log "github.com/sirupsen/logrus" +) + +// Start a poller to check the status +func (c *Analysis) initStatusCheck() { + + log.Info("Analysis Plugin: Starting status check ...") + + // Just loop forever, checking the status of running jobs every 10s + go func() { + for { + time.Sleep(10 * time.Second) + err := c.checkStatus() + if err != nil { + log.Errorf("Error checking status: %v", err) + } + } + }() +} + +func (c *Analysis) checkStatus() error { + log.Debug("Checking status....") + p := c.portalProxy + // Create a record in the reports datastore + dbStore, err := store.NewAnalysisDBStore(p.GetDatabaseConnection()) + if err != nil { + return fmt.Errorf("Status Check: Can not get anaylsis store db: %v", err) + } + + // Get all running jobs + running, err := dbStore.ListRunning() + if err != nil { + return fmt.Errorf("Can not get list of running jobs: %v", err) + } + + if len(running) == 0 { + return nil + } + + ids := make([]string, 0) + for _, job := range running { + log.Debugf("Got running job: %s", job.ID) + ids = append(ids, job.ID) + } + + data, err := json.Marshal(ids) + if err != nil { + log.Errorf("Could not marshal IDs: %v", err) + return fmt.Errorf("Could not marshal IDs: %v", err) + } + + // Make request to status + statusURL := fmt.Sprintf("%s/api/v1/status", c.analysisServer) + r, _ := http.NewRequest(http.MethodPost, statusURL, bytes.NewReader(data)) + r.Header.Set("Content-Type", "application/json") + client := &http.Client{Timeout: 180 * time.Second} + rsp, err := client.Do(r) + if err != nil { + return fmt.Errorf("Failed getting status from Analyzer service: %v", err) + } + + if rsp.StatusCode != http.StatusOK { + return fmt.Errorf("Failed getting status from Analyzer service: %d", rsp.StatusCode) + } + + defer rsp.Body.Close() + response, err := ioutil.ReadAll(rsp.Body) + if err != nil { + log.Errorf("Could not read response: %v", err) + return fmt.Errorf("Could not read response: %v", err) + } + + // Turn into map of IDs to Jobs + statuses := make(map[string]store.AnalysisRecord) + + if err := json.Unmarshal(response, &statuses); err != nil { + return fmt.Errorf("Could not parse response: %v", err) + } + + for _, job := range running { + if status, ok := statuses[job.ID]; ok { + job.Duration = status.Duration + job.Status = status.Status + if err := dbStore.UpdateReport(job.UserID, job); err != nil { + log.Warnf("Unable to update status for job %s: %v", job.ID, err) + } + } else { + // The analysis server did not know about our job, os mark as error + job.Status = "error" + if err := dbStore.UpdateReport(job.UserID, job); err != nil { + log.Warnf("Unable to update status for job %s: %v", job.ID, err) + } + } + } + + return nil +} diff --git a/src/jetstream/plugins/analysis/store/analysis_store_db.go b/src/jetstream/plugins/analysis/store/analysis_store_db.go new file mode 100644 index 0000000000..481408fa6f --- /dev/null +++ b/src/jetstream/plugins/analysis/store/analysis_store_db.go @@ -0,0 +1,164 @@ +package store + +import ( + "database/sql" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" +) + +var ( + listReports = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE user = $1 AND endpoint = $2` + listCompletedReportsByPath = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'completed' AND user = $1 AND endpoint = $2 AND path = $3 ORDER BY created DESC` + getReport = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE user = $1 AND id=$2` + deleteReport = `DELETE FROM analysis WHERE user = $1 AND id = $2` + saveReport = `INSERT INTO analysis (id, user, endpoint_type, endpoint, name, path, type, format, created, acknowledged, status, duration, result) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)` + updateReport = `UPDATE analysis SET type = $1, format = $2, acknowledged = $3, status = $4, duration = $5, result = $6, name = $7, path = $8, result = $9 WHERE user = $10 AND id = $11` + getLatestReport = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'completed' AND user = $1 AND endpoint = $2 AND path = $3 ORDER BY created DESC` + listRunningReports = `SELECT id, endpoint_type, endpoint, user, name, path, type, format, created, acknowledged, status, duration, result FROM analysis WHERE status = 'running' ORDER BY created DESC` + deleteForEndpoint = `DELETE FROM analysis WHERE endpoint = $1` +) + +// InitRepositoryProvider - One time init for the given DB Provider +func InitRepositoryProvider(databaseProvider string) { + // Modify the database statements if needed, for the given database type + listReports = datastore.ModifySQLStatement(listReports, databaseProvider) + listCompletedReportsByPath = datastore.ModifySQLStatement(listCompletedReportsByPath, databaseProvider) + getReport = datastore.ModifySQLStatement(getReport, databaseProvider) + deleteReport = datastore.ModifySQLStatement(deleteReport, databaseProvider) + saveReport = datastore.ModifySQLStatement(saveReport, databaseProvider) + updateReport = datastore.ModifySQLStatement(updateReport, databaseProvider) + getLatestReport = datastore.ModifySQLStatement(getLatestReport, databaseProvider) + listRunningReports = datastore.ModifySQLStatement(listRunningReports, databaseProvider) + deleteForEndpoint = datastore.ModifySQLStatement(deleteForEndpoint, databaseProvider) +} + +// AnalysisDBStore is a DB-backed Analysis Reports repository +type AnalysisDBStore struct { + db *sql.DB +} + +// NewAnalysisDBStore will create a new instance of the AnalysisDBStore +func NewAnalysisDBStore(dcp *sql.DB) (AnalysisStore, error) { + return &AnalysisDBStore{db: dcp}, nil +} + +// List - Returns a list of all user Analysis Reports for the given endpoint +func (p *AnalysisDBStore) List(userGUID, endpointID string) ([]*AnalysisRecord, error) { + log.Debug("List") + rows, err := p.db.Query(listReports, userGUID, endpointID) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err) + } + defer rows.Close() + + return list(rows) +} + +func (p *AnalysisDBStore) ListCompletedByPath(userGUID, endpointID, path string) ([]*AnalysisRecord, error) { + log.Debug("ListCompletedByPath") + rows, err := p.db.Query(listCompletedReportsByPath, userGUID, endpointID, path) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err) + } + defer rows.Close() + + return list(rows) +} + +func (p *AnalysisDBStore) ListRunning() ([]*AnalysisRecord, error) { + log.Debug("ListRunning") + rows, err := p.db.Query(listRunningReports) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve Analysis Reports records: %v", err) + } + defer rows.Close() + + return list(rows) +} + +func list(rows *sql.Rows) ([]*AnalysisRecord, error) { + var reportList []*AnalysisRecord + reportList = make([]*AnalysisRecord, 0) + + for rows.Next() { + report := new(AnalysisRecord) + err := rows.Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result) + if err != nil { + return nil, fmt.Errorf("Unable to scan Analysis Reports records: %v", err) + } + reportList = append(reportList, report) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("Unable to List Analysis Reports records: %v", err) + } + + return reportList, nil +} + +// Get - Get a specific Analysis Report by ID +func (p *AnalysisDBStore) Get(userGUID, ID string) (*AnalysisRecord, error) { + log.Debug("Get") + + report := AnalysisRecord{} + err := p.db.QueryRow(getReport, userGUID, ID).Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result) + if err != nil { + msg := "Unable to Get Analysis Report record: %v" + log.Debugf(msg, err) + return nil, fmt.Errorf(msg, err) + } + + return &report, nil +} + +// GetLatestCompleted - Get latest report for the specified path +func (p *AnalysisDBStore) GetLatestCompleted(userGUID, endpointID, path string) (*AnalysisRecord, error) { + log.Debug("GetLatestCompleted") + + report := AnalysisRecord{} + err := p.db.QueryRow(getLatestReport, userGUID, endpointID, path).Scan(&report.ID, &report.EndpointType, &report.EndpointID, &report.UserID, &report.Name, &report.Path, &report.Type, &report.Format, &report.Created, &report.Read, &report.Status, &report.Duration, &report.Result) + if err != nil { + msg := "Unable to get laetst completed Analysis Report record: %v" + log.Debugf(msg, err) + return nil, fmt.Errorf(msg, err) + } + + return &report, nil +} + +// Delete will delete an Analysis Report from the datastore +func (p *AnalysisDBStore) Delete(userGUID string, id string) error { + if _, err := p.db.Exec(deleteReport, userGUID, id); err != nil { + return fmt.Errorf("Unable to delete Analysis Report record: %v", err) + } + + return nil +} + +// UpdateReport will update the dynamic fields of the Analysis Record in thedatastore +func (p *AnalysisDBStore) UpdateReport(userGUID string, report *AnalysisRecord) error { + if _, err := p.db.Exec(updateReport, report.Type, report.Format, report.Read, report.Status, report.Duration, report.Result, report.Name, report.Path, report.Result, userGUID, report.ID); err != nil { + return fmt.Errorf("Unable to update Analysis Report record: %v", err) + } + return nil +} + +// Save will persist an Analysis Report to the datastore +func (p *AnalysisDBStore) Save(report AnalysisRecord) (*AnalysisRecord, error) { + if _, err := p.db.Exec(saveReport, report.ID, report.UserID, report.EndpointType, report.EndpointID, report.Name, report.Path, report.Type, report.Format, report.Created, report.Read, &report.Status, &report.Duration, &report.Result); err != nil { + return nil, fmt.Errorf("Unable to save Analysis Report record: %v", err) + } + + return &report, nil +} + +// DeleteForEndpoint will remove all Analysis Reports for a given endpoint guid +func (p *AnalysisDBStore) DeleteForEndpoint(endpointID string) error { + if _, err := p.db.Exec(deleteForEndpoint, endpointID); err != nil { + return fmt.Errorf("Unable to delete reports for endpoint: %s %v", endpointID, err) + } + return nil +} diff --git a/src/jetstream/plugins/analysis/store/main.go b/src/jetstream/plugins/analysis/store/main.go new file mode 100644 index 0000000000..e9a14edac6 --- /dev/null +++ b/src/jetstream/plugins/analysis/store/main.go @@ -0,0 +1,39 @@ +package store + +import ( + "encoding/json" + "time" +) + +// AnalysisRecord represents an analysis that has been run +type AnalysisRecord struct { + ID string `json:"id"` + UserID string `json:"-"` + EndpointType string `json:"endpointType"` + EndpointID string `json:"endpoint"` + Type string `json:"type"` + Format string `json:"format"` + Name string `json:"name"` + Path string `json:"path"` + Created time.Time `json:"created"` + Read bool `json:"read"` + Status string `json:"status"` + Duration int `json:"duration"` + Result string `json:"-"` + Error string `json:"error"` + Summary *json.RawMessage `json:"summary"` + Report *json.RawMessage `json:"report,omitempty"` +} + +// AnalysisStore is the analysis repository +type AnalysisStore interface { + List(userGUID, endpointID string) ([]*AnalysisRecord, error) + Get(userGUID, id string) (*AnalysisRecord, error) + GetLatestCompleted(userGUID, endpointID, path string) (*AnalysisRecord, error) + ListCompletedByPath(userGUID, endpointID, path string) ([]*AnalysisRecord, error) + ListRunning() ([]*AnalysisRecord, error) + Delete(userGUID, id string) error + DeleteForEndpoint(endpointID string) error + Save(record AnalysisRecord) (*AnalysisRecord, error) + UpdateReport(userGUID string, report *AnalysisRecord) error +} diff --git a/src/jetstream/plugins/kubernetes/endpoint_config.go b/src/jetstream/plugins/kubernetes/endpoint_config.go index 8a7acd85af..8e7b983235 100644 --- a/src/jetstream/plugins/kubernetes/endpoint_config.go +++ b/src/jetstream/plugins/kubernetes/endpoint_config.go @@ -24,10 +24,8 @@ func (c *KubernetesSpecification) GetConfigForEndpoint(masterURL string, token i func (c *KubernetesSpecification) GetConfigForEndpointUser(endpointID, userID string) (*restclient.Config, error) { var p = c.portalProxy - cnsiRecord, err := p.GetCNSIRecord(endpointID) if err != nil { - //return sendSSHError("Could not get endpoint information") return nil, errors.New("Could not get endpoint information") } @@ -40,6 +38,23 @@ func (c *KubernetesSpecification) GetConfigForEndpointUser(endpointID, userID st return c.GetConfigForEndpoint(cnsiRecord.APIEndpoint.String(), tokenRec) } +func (c *KubernetesSpecification) GetKubeConfigForEndpointUser(endpointID, userID string) (string, error) { + + var p = c.portalProxy + cnsiRecord, err := p.GetCNSIRecord(endpointID) + if err != nil { + return "", errors.New("Could not get endpoint information") + } + + // Get token for this users + tokenRec, ok := p.GetCNSITokenRecord(endpointID, userID) + if !ok { + return "", errors.New("Could not get token") + } + + return c.GetKubeConfigForEndpoint(cnsiRecord.APIEndpoint.String(), tokenRec, "") +} + func (c *KubernetesSpecification) getKubeConfigForEndpoint(masterURL string, token interfaces.TokenRecord, namespace string) (*clientcmdapi.Config, error) { name := "cluster-0" diff --git a/src/jetstream/plugins/kubernetes/get_release.go b/src/jetstream/plugins/kubernetes/get_release.go index d072870557..e4627830ab 100644 --- a/src/jetstream/plugins/kubernetes/get_release.go +++ b/src/jetstream/plugins/kubernetes/get_release.go @@ -102,7 +102,7 @@ func (c *KubernetesSpecification) GetReleaseStatus(ec echo.Context) error { // this back incrementally // Parse the manifest - rel := helm.NewHelmRelease(res, endpointGUID, userID) + rel := helm.NewHelmRelease(res, endpointGUID, userID, c.portalProxy) graph := helm.NewHelmReleaseGraph(rel) diff --git a/src/jetstream/plugins/kubernetes/go.mod b/src/jetstream/plugins/kubernetes/go.mod index 288ec1d075..6e44da2a86 100644 --- a/src/jetstream/plugins/kubernetes/go.mod +++ b/src/jetstream/plugins/kubernetes/go.mod @@ -9,7 +9,6 @@ require ( github.com/aws/aws-sdk-go v1.17.5 github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect github.com/docker/docker v1.13.1 // indirect - github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect github.com/ghodss/yaml v1.0.0 github.com/gorilla/websocket v1.4.0 diff --git a/src/jetstream/plugins/kubernetes/go.sum b/src/jetstream/plugins/kubernetes/go.sum index d3b2e14ea0..3159ac83df 100644 --- a/src/jetstream/plugins/kubernetes/go.sum +++ b/src/jetstream/plugins/kubernetes/go.sum @@ -112,6 +112,7 @@ github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2Rz github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d h1:qdD+BtyCE1XXpDyhvn0yZVcZOLILdj9Cw4pKu0kQbPQ= github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= @@ -254,6 +255,7 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= @@ -292,6 +294,8 @@ github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZs github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= @@ -369,6 +373,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kubeapps/common v0.0.0-20181107174310-61d8eb6f11b4 h1:wdTBUArlqtBYGN2Dd4+zsaFxFH0m4iGCHToW10jPX0k= github.com/kubeapps/common v0.0.0-20181107174310-61d8eb6f11b4/go.mod h1:TsgmjeDpbftqhwPKInJ3v+l+xbHs4goiB6DFb2WqY9c= +github.com/kubeapps/common v0.0.0-20190508164739-10b110436c1a/go.mod h1:TsgmjeDpbftqhwPKInJ3v+l+xbHs4goiB6DFb2WqY9c= github.com/kubernetes-sigs/aws-iam-authenticator v0.3.0 h1:HOC7YpUao5F3RTIncfBfoh+7/ID1Jl97ALNgEmWIjxo= github.com/kubernetes-sigs/aws-iam-authenticator v0.3.0/go.mod h1:ItxiN33Ho7Di8wiC4S4XqbH1NLF0DNdDWOd/5MI9gJU= github.com/kubernetes-sigs/aws-iam-authenticator v0.3.1-0.20190111160901-390d9087a4bc h1:Ttr4Z3ZrMv4rAXn10UAqOC8ACx+F1omvcyV1a3hRArE= @@ -377,6 +382,7 @@ github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8 github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= @@ -392,9 +398,12 @@ github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= @@ -495,8 +504,8 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v2.0.0 h1:L7Oc72h7rDqGkbUorN/ncJ4N/y220/YRezHvBoKLOFA= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday v2.0.0/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v2.0.0/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -537,12 +546,16 @@ github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245/go.mod h1:O1c github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/unrolled/render v1.0.0 h1:XYtvhA3UkpB7PqkvhUFYmpKD55OudoIeygcfus4vcd4= github.com/unrolled/render v1.0.0/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg= +github.com/unrolled/render v1.0.1/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -559,6 +572,7 @@ github.com/yvasiyarov/gorelic v0.0.6 h1:qMJQYPNdtJ7UNYHjX38KXZtltKTqimMuoQjNnSVI github.com/yvasiyarov/gorelic v0.0.6/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.mongodb.org/mongo-driver v1.1.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.17.0/go.mod h1:mp1VrMQxhlqqDpKvH4UcQUa4YwlzNmymAjPrDdfxNpI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -574,12 +588,15 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRi golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 h1:ZC1Xn5A1nlpSmQCIva4bZ3ob3lmhYIefc+GU+DLg1Ow= golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191205161847-0a08dada0ff9 h1:abxekknhS/Drh3uoQDk5Hc7BgeiyI39Crb7vhf/1j5s= +golang.org/x/crypto v0.0.0-20191205161847-0a08dada0ff9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -624,6 +641,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= @@ -641,6 +660,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934 h1:u/E0NqCIWRDAo9WCFo6Ko49njPFDLSd3z+X1HgWDMpE= @@ -787,6 +807,7 @@ k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/helm v2.12.3+incompatible h1:wo1cdYjOnr5Z+LFuhtwIJaeQnec6D4gcg2H5UAKzY6w= k8s.io/helm v2.12.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= +k8s.io/helm v2.16.1+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= diff --git a/src/jetstream/plugins/kubernetes/helm/graph.go b/src/jetstream/plugins/kubernetes/helm/graph.go index cea0f9c5dd..a46bf3ac0a 100644 --- a/src/jetstream/plugins/kubernetes/helm/graph.go +++ b/src/jetstream/plugins/kubernetes/helm/graph.go @@ -27,8 +27,12 @@ type ReleaseNode struct { ID string `json:"id"` Label string `json:"label"` Data struct { - Kind string `json:"kind"` - Status NodeStatus `json:"status"` + Kind string `json:"kind"` + Status NodeStatus `json:"status"` + Metadata struct { + Name string `yaml:"name" json:"name"` + Namespace string `yaml:"namespace" json:"namespace"` + } `yaml:"metadata" json:"metadata"` } `json:"data"` } @@ -67,6 +71,8 @@ func (r *HelmReleaseGraph) ParseManifest(release *HelmRelease) { } node.Data.Kind = item.Kind + node.Data.Metadata = item.Metadata + // Note - item.Metadata.Namespace is nil node.Data.Status = "unknown" switch o := item.Resource.(type) { @@ -114,6 +120,9 @@ func (r *HelmReleaseGraph) ParseManifest(release *HelmRelease) { case *rbacv1.RoleBinding: target := getShortResourceId(item.Kind, o.Name) r.ParseRoleBinding(target, o) + case *rbacv1.ClusterRoleBinding: + target := getShortResourceId(item.Kind, o.Name) + r.ParseClusterRoleBinding(target, o) default: log.Debugf("Graph: Unknown type: %s", reflect.TypeOf(o)) } @@ -138,20 +147,15 @@ func (r *HelmReleaseGraph) generateTemporaryNode(id string) { node := ReleaseNode{ ID: id, - Label: parts[1], + Label: strings.Join(parts[1:], "-"), } node.Data.Kind = parts[0] node.Data.Status = "missing" - r.Nodes[node.ID] = node } func getShortResourceId(kind, name string) string { - // // TODO: FIX - empty kind is a pod - // if len(kind) == 0 { - // kind = "Pod" - // } return fmt.Sprintf("%s-%s", kind, name) } @@ -180,6 +184,13 @@ func (r *HelmReleaseGraph) ProcessPod(id string, res KubeResource, spec v1.PodSp } } + // Service Account + saName := spec.ServiceAccountName + if len(saName) > 0 { + ref := fmt.Sprintf("ServiceAccount-%s", saName) + r.AddLink(id, ref) + } + // Go through the pod and process each container // Add a node for each container for _, container := range spec.Containers { @@ -256,3 +267,14 @@ func (r *HelmReleaseGraph) ParseRoleBinding(id string, roleBinding *rbacv1.RoleB roleRefID := fmt.Sprintf("%s-%s", roleBinding.RoleRef.Kind, roleBinding.RoleRef.Name) r.AddLink(id, roleRefID) } + +func (r *HelmReleaseGraph) ParseClusterRoleBinding(id string, roleBinding *rbacv1.ClusterRoleBinding) { + for _, subject := range roleBinding.Subjects { + // TODO: Only match those with the same namespace ???? + subjectID := fmt.Sprintf("%s-%s", subject.Kind, subject.Name) + r.AddLink(id, subjectID) + } + + roleRefID := fmt.Sprintf("%s-%s", roleBinding.RoleRef.Kind, roleBinding.RoleRef.Name) + r.AddLink(id, roleRefID) +} diff --git a/src/jetstream/plugins/kubernetes/helm/release.go b/src/jetstream/plugins/kubernetes/helm/release.go index f8fd9c3131..868200bb89 100644 --- a/src/jetstream/plugins/kubernetes/helm/release.go +++ b/src/jetstream/plugins/kubernetes/helm/release.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" log "github.com/sirupsen/logrus" "helm.sh/helm/v3/pkg/release" appsv1 "k8s.io/api/apps/v1" @@ -18,15 +19,16 @@ import ( v1 "k8s.io/api/core/v1" extv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" - - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "sigs.k8s.io/yaml" ) var resourcesWithoutStatus = map[string]bool{ - "RoleBinding": false, - "Role": false, + "RoleBinding": false, + "Role": false, + "ClusterRole": false, + "ClusterRoleBinding": false, + "PodSecurityPolicy": false, } // HelmRelease represents a Helm Release deployed via Helm @@ -45,7 +47,8 @@ type KubeResource struct { Kind string `yaml:"kind" json:"kind"` APIVersion string `yaml:"apiVersion" json:"apiVersion"` Metadata struct { - Name string `yaml:"name" json:"name"` + Name string `yaml:"name" json:"name"` + Namespace string `yaml:"namespace" json:"namespace"` } `yaml:"metadata" json:"metadata"` Resource interface{} `yaml:"resource"` Manifest bool @@ -56,7 +59,7 @@ func (r *KubeResource) getID() string { } // NewHelmRelease represents extended info about a Helm Release -func NewHelmRelease(info *release.Release, endpoint, user string) *HelmRelease { +func NewHelmRelease(info *release.Release, endpoint, user string, jetstream interfaces.PortalProxy) *HelmRelease { r := &HelmRelease{ Release: info, Endpoint: endpoint, @@ -76,16 +79,26 @@ func (r *HelmRelease) parseManifest() { var bufr strings.Builder for { line, err := buffer.ReadString('\n') - if err != nil || (err == nil && strings.TrimSpace(line) == "---") { + if err != nil || (err == nil && strings.TrimRight(line, "\t \n") == "---") { data := []byte(bufr.String()) if len(data) > 0 { decode := scheme.Codecs.UniversalDeserializer().Decode - obj, _, err := decode([]byte(bufr.String()), nil, nil) + obj, _, err := decode(data, nil, nil) if err != nil { - log.Error(fmt.Sprintf("Helm Manifest Parser: Error while decoding YAML object. Err was: %s", err)) - r.ManifestErrors = true + // Custom Resource Definition + if strings.HasPrefix(err.Error(), "no kind") { + var t interface{} + if err := yaml.Unmarshal(data, &t); err == nil { + r.processYamlResource(t, data) + } else { + log.Errorf("Could not parse custom resource %s", err) + } + } else { + log.Error(fmt.Sprintf("Helm Manifest Parser: Error while decoding YAML object. Err was: %s", err)) + r.ManifestErrors = true + } } else { - r.processResource(obj) + r.processJsonResource(obj) } bufr.Reset() @@ -96,7 +109,11 @@ func (r *HelmRelease) parseManifest() { if err != nil { break } - bufr.WriteString(line) + + // Ignore comments + if !strings.HasPrefix(strings.TrimSpace(line), "#") && !strings.HasPrefix(strings.TrimRight(line, "\t \n"), "---") { + bufr.WriteString(line) + } } } @@ -131,32 +148,61 @@ func (r *HelmRelease) GetPods() []interface{} { return resources } -// process a yaml resource from the helm manifest -func (r *HelmRelease) processResource(obj runtime.Object) { - j, err := json.Marshal(obj) +func (r *HelmRelease) processJsonResource(obj interface{}) { + data, err := json.Marshal(obj) if err == nil { var t KubeResource - if json.Unmarshal(j, &t) == nil { - t.Resource = obj - t.Manifest = true - r.setResource(t) - log.Debugf("Got resource: %s : %s", t.Kind, t.Metadata.Name) - r.processController(t) - r.addJobForResource(t.Kind, t.APIVersion, t.Metadata.Name) + if err := json.Unmarshal(data, &t); err == nil { + // If this is a List, then unpack it + if t.APIVersion == "v1" && t.Kind == "List" { + var list v1.PodList + err := json.Unmarshal(data, &list) + if err == nil { + for _, item := range list.Items { + r.processJsonResource(item) + } + } else { + log.Error("Helm Release Manifest: Could not parse List resource") + } + } else { + r.processKubeResource(obj, t) + } } else { log.Error("Helm Release Manifest: Could not parse Kubernetes resource") } + } else { + log.Errorf("Helm Release ManifestL Could not marshal Kubernetes resource %s", err) + } +} + +func (r *HelmRelease) processYamlResource(obj interface{}, data []byte) { + var t KubeResource + if err := yaml.Unmarshal(data, &t); err == nil { + r.processKubeResource(obj, t) + } else { + log.Error("Helm Release Manifest: Could not parse Kubernetes resource") } } -func (r *HelmRelease) addJobForResource(kind, apiVersion, name string) { +// process a yaml resource from the helm manifest +//func (r *HelmRelease) processResource(obj runtime.Object) { +func (r *HelmRelease) processKubeResource(obj interface{}, t KubeResource) { + t.Resource = obj + t.Manifest = true + r.setResource(t) + log.Debugf("Got resource: %s : %s", t.Kind, t.Metadata.Name) + r.processController(t) + r.addJobForResource(t.Metadata.Namespace, t.Kind, t.APIVersion, t.Metadata.Name) +} + +func (r *HelmRelease) addJobForResource(namespace, kind, apiVersion, name string) { job := KubeResourceJob{ ID: fmt.Sprintf("%s-%s#Pods", kind, name), Endpoint: r.Endpoint, User: r.User, Namespace: r.Namespace, Name: name, - URL: getRestURL(r.Namespace, kind, apiVersion, name), + URL: getRestURL(namespace, kind, apiVersion, name), APIVersion: apiVersion, Kind: kind, } @@ -235,6 +281,8 @@ func (r *HelmRelease) UpdatePods(jetstream interfaces.PortalProxy) { podCopy := &v1.Pod{} *podCopy = pod + podCopy.Kind = "Pod" + podCopy.APIVersion = "v1" res.Resource = podCopy pods[res.getID()] = &res @@ -268,7 +316,7 @@ func (r *HelmRelease) processPodOwners(pod v1.Pod) { } resource.ObjectMeta = metav1.ObjectMeta{ Name: owner.Name, - Namespace: r.Namespace, + Namespace: pod.Namespace, } identifier := getResourceIdentifier(resource.TypeMeta, resource.ObjectMeta) if _, ok := r.Resources[identifier]; !ok { @@ -282,7 +330,7 @@ func (r *HelmRelease) processPodOwners(pod v1.Pod) { res.Resource = &resource r.setResource(res) - r.addJobForResource(owner.Kind, owner.APIVersion, owner.Name) + r.addJobForResource(pod.Namespace, owner.Kind, owner.APIVersion, owner.Name) } } else { log.Debugf("Unexpected Pod owner kind: %s", owner.Kind) @@ -290,15 +338,6 @@ func (r *HelmRelease) processPodOwners(pod v1.Pod) { } } -// func (r *HelmRelease) getKubeResource(typeMeta metav1.TypeMeta, objectMeta metav1.ObjectMeta) KubeResource { -// kres := KubeResource{ -// Kind: typeMeta.Kind, -// APIVersion: typeMeta.APIVersion, -// } -// kres.Metadata.Name = objectMeta.Name -// return kres -// } - func (r *HelmRelease) UpdateResources(jetstream interfaces.PortalProxy) { // This will be an array of resources runner := NewKubeAPIJob(jetstream, r.Jobs) @@ -312,7 +351,7 @@ func (r *HelmRelease) UpdateResources(jetstream interfaces.PortalProxy) { } res.Metadata.Name = j.Name - // TODO: If the status was 404, then we should remove the resource + // If the status was 404, then we should remove the resource if j.StatusCode == http.StatusNotFound { log.Debugf("Resource has been deleted - removing: %s -> %s", j.Kind, j.Name) r.deleteResource(res) @@ -332,7 +371,14 @@ func (r *HelmRelease) UpdateResources(jetstream interfaces.PortalProxy) { res.Resource = obj r.setResource(res) } else { - log.Error("Could not parse resource") + // Just decode from Yaml - could be a CRD + var obj interface{} + if err := yaml.Unmarshal(j.Data, &obj); err == nil { + res.Resource = obj + r.setResource(res) + } else { + log.Error("Could not parse resource") + } } // TODO: If the resource was a job, process the selector again @@ -351,6 +397,22 @@ func getRestURL(namespace, kind, apiVersion, name string) string { name += "/status" } } - restURL = fmt.Sprintf("/%s/%s/namespaces/%s/%ss/%s", base, apiVersion, namespace, strings.ToLower(kind), name) + + kindPlural := pluralize(strings.ToLower(kind)) + if len(namespace) == 0 { + // This is not a namespaced resource + restURL = fmt.Sprintf("/%s/%s/%s/%s", base, apiVersion, kindPlural, name) + } else { + restURL = fmt.Sprintf("/%s/%s/namespaces/%s/%s/%s", base, apiVersion, namespace, kindPlural, name) + } + return restURL } + +func pluralize(resource string) string { + if strings.HasSuffix(resource, "y") { + return fmt.Sprintf("%sies", resource[:len(resource)-1]) + } + + return fmt.Sprintf("%ss", resource) +}