diff --git a/.secrets.baseline b/.secrets.baseline index 3347361250..11d4996806 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,9 +1,9 @@ { "exclude": { - "files": "^.secrets.baseline$", + "files": "^.secrets.baseline$|^.secrets.baseline_temp$", "lines": null }, - "generated_at": "2019-07-02T10:37:07Z", + "generated_at": "2019-08-23T13:59:56Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -34,7 +34,7 @@ { "hashed_secret": "0b4049b5e5a601cb07835c4b4b6881a9a609387c", "is_secret": false, - "line_number": 49, + "line_number": 48, "type": "Secret Keyword" } ], @@ -60,7 +60,15 @@ { "hashed_secret": "959d73abdef41e5d0f4ec2afd98cb68d37953a2a", "is_secret": false, - "line_number": 22, + "line_number": 21, + "type": "Secret Keyword" + } + ], + "backend/lib/github/octokit.js": [ + { + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_secret": false, + "line_number": 56, "type": "Secret Keyword" } ], @@ -76,13 +84,13 @@ { "hashed_secret": "7ea6be9eecb6605329a1b1870c2fd2af9b896991", "is_secret": false, - "line_number": 42, + "line_number": 43, "type": "Secret Keyword" }, { "hashed_secret": "8b142a91cfb6e617618ad437cedf74a6745f8926", "is_secret": false, - "line_number": 79, + "line_number": 93, "type": "Secret Keyword" } ], @@ -112,7 +120,7 @@ { "hashed_secret": "30118aa1aa8a06fa5365743b3a5db69fc62b9760", "is_secret": false, - "line_number": 270, + "line_number": 263, "type": "Secret Keyword" } ], @@ -120,7 +128,7 @@ { "hashed_secret": "e9fe51f94eadabf54dbf2fbbd57188b9abee436e", "is_secret": false, - "line_number": 37, + "line_number": 35, "type": "Secret Keyword" } ], @@ -128,19 +136,19 @@ { "hashed_secret": "f7e3bc894e61a47eb1810807767610a3c88527c3", "is_secret": false, - "line_number": 118, + "line_number": 116, "type": "Secret Keyword" }, { "hashed_secret": "ccbee52710eb8a59dad640c207009ad01fc76b35", "is_secret": false, - "line_number": 120, + "line_number": 118, "type": "Secret Keyword" }, { "hashed_secret": "0ac5df4b0f96ad426c76dd945dfcef01e25c8818", "is_secret": false, - "line_number": 122, + "line_number": 120, "type": "Secret Keyword" } ], @@ -208,13 +216,13 @@ { "hashed_secret": "fb3c6e4de85bd9eae26fdc63e75f10a7f39e850e", "is_secret": false, - "line_number": 52, + "line_number": 50, "type": "Secret Keyword" }, { "hashed_secret": "8843d7f92416211de9ebb963ff4ce28125932878", "is_secret": false, - "line_number": 79, + "line_number": 77, "type": "Secret Keyword" } ], @@ -222,7 +230,7 @@ { "hashed_secret": "09343c85545555efc31e6e70b7ea7dcadf854930", "is_secret": false, - "line_number": 42, + "line_number": 43, "type": "Secret Keyword" } ], @@ -246,6 +254,52 @@ "type": "Private Key" } ], + "frontend/src/components/NewShoot/NewShootDetails.vue": [ + { + "hashed_secret": "d5d4cd07616a542891b7ec2d0257b3a24b69856e", + "is_secret": false, + "line_number": 121, + "type": "Secret Keyword" + }, + { + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_secret": false, + "line_number": 191, + "type": "Secret Keyword" + } + ], + "frontend/src/components/NewShoot/NewShootInfrastructureDetails.vue": [ + { + "hashed_secret": "2c0580ffd7d80319531cf629f5e90f747b1386f1", + "is_secret": false, + "line_number": 193, + "type": "Secret Keyword" + }, + { + "hashed_secret": "b8c6d8c49add25b398e5beeaae12689ee2bd10b6", + "is_secret": false, + "line_number": 386, + "type": "Secret Keyword" + }, + { + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_secret": false, + "line_number": 396, + "type": "Secret Keyword" + }, + { + "hashed_secret": "22f6a1953ff02167cbfecf24ea856d94bc397254", + "is_secret": false, + "line_number": 412, + "type": "Secret Keyword" + }, + { + "hashed_secret": "bd66a10f34934a079686639a5b287d8dad8d1c4c", + "is_secret": false, + "line_number": 414, + "type": "Secret Keyword" + } + ], "frontend/src/components/Secret.vue": [ { "hashed_secret": "e8cdc05b346aa0d4a91a2bf6d7c6a0941a6555a7", @@ -376,11 +430,27 @@ "type": "Secret Keyword" } ], + "frontend/src/dialogs/SecretDialogWrapper.vue": [ + { + "hashed_secret": "32f64badcf839f19bfd0b409f9c49ead291fba63", + "is_secret": false, + "line_number": 31, + "type": "Secret Keyword" + } + ], + "frontend/src/pages/NewShoot.vue": [ + { + "hashed_secret": "cb780a6106d2b3106ec569f878d3068415696acd", + "is_secret": false, + "line_number": 285, + "type": "Secret Keyword" + } + ], "frontend/src/pages/Secrets.vue": [ { "hashed_secret": "32f64badcf839f19bfd0b409f9c49ead291fba63", "is_secret": false, - "line_number": 148, + "line_number": 134, "type": "Secret Keyword" } ] diff --git a/backend/lib/middleware.js b/backend/lib/middleware.js index 623400928e..9c4652a81a 100644 --- a/backend/lib/middleware.js +++ b/backend/lib/middleware.js @@ -20,14 +20,9 @@ const _ = require('lodash') const config = require('./config') const logger = require('./logger') const { NotFound, InternalServerError } = require('./errors') -const { customAddonDefinitions } = require('./services') -async function frontendConfig (req, res, next) { - const user = req.user +function frontendConfig (req, res, next) { const frontendConfig = {} - try { - frontendConfig.customAddonDefinitions = await customAddonDefinitions.list({ user, namespace: 'garden' }) - } catch (err) { /* ignore error */ } res.json(Object.assign(frontendConfig, config.frontend)) } diff --git a/backend/lib/routes/shoots.js b/backend/lib/routes/shoots.js index 81c5f55e24..997aa80bca 100644 --- a/backend/lib/routes/shoots.js +++ b/backend/lib/routes/shoots.js @@ -129,6 +129,19 @@ router.route('/:name/spec/hibernation/schedules') } }) +router.route('/:name/spec/addons') + .put(async (req, res, next) => { + try { + const user = req.user + const namespace = req.params.namespace + const name = req.params.name + const body = req.body + res.send(await shoots.replaceAddons({ user, namespace, name, body })) + } catch (err) { + next(err) + } + }) + router.route('/:name/spec/cloud/:infrastructureKind/workers') .put(async (req, res, next) => { try { diff --git a/backend/lib/services/customAddonDefinitions.js b/backend/lib/services/customAddonDefinitions.js deleted file mode 100644 index 303f98af5c..0000000000 --- a/backend/lib/services/customAddonDefinitions.js +++ /dev/null @@ -1,41 +0,0 @@ - -// -// Copyright (c) 2019 by SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -'use strict' - -const _ = require('lodash') -const yaml = require('js-yaml') -const core = require('../kubernetes').core() - -exports.list = async function ({ user, namespace = 'garden' }) { - const { items } = await core.namespaces(namespace).configmaps.get({ - qs: { - labelSelector: 'gardenextensions.sapcloud.io/role=addonDefinitions' - } - }) - return _ - .chain(items) - .first() - .get('data') - .map((data, name) => { - try { - return _.set(yaml.safeLoad(data), 'name', name) - } catch (err) { /* ignore error */ } - }) - .compact() - .value() -} diff --git a/backend/lib/services/index.js b/backend/lib/services/index.js index 0b65004109..736d1c974b 100644 --- a/backend/lib/services/index.js +++ b/backend/lib/services/index.js @@ -23,6 +23,5 @@ module.exports = { members: require('./members'), authorization: require('./authorization'), authentication: require('./authentication'), - journals: require('./journals'), - customAddonDefinitions: require('./customAddonDefinitions') + journals: require('./journals') } diff --git a/backend/lib/services/shoots.js b/backend/lib/services/shoots.js index 37b1345050..88772ad90c 100644 --- a/backend/lib/services/shoots.js +++ b/backend/lib/services/shoots.js @@ -142,6 +142,16 @@ exports.replaceHibernationSchedules = async function ({ user, namespace, name, b return patch({ user, namespace, name, body: payload }) } +exports.replaceAddons = async function ({ user, namespace, name, body }) { + const addons = body + const payload = { + spec: { + addons + } + } + return patch({ user, namespace, name, body: payload }) +} + exports.replaceWorkers = async function ({ user, namespace, infrastructureKind, name, body }) { const workers = body const patchOperations = [ @@ -155,7 +165,7 @@ exports.replaceWorkers = async function ({ user, namespace, infrastructureKind, } exports.replaceMaintenance = async function ({ user, namespace, name, body }) { - const { timeWindowBegin, timeWindowEnd, updateKubernetesVersion } = body + const { timeWindowBegin, timeWindowEnd, updateKubernetesVersion, updateOSVersion } = body const payload = { spec: { maintenance: { @@ -164,7 +174,8 @@ exports.replaceMaintenance = async function ({ user, namespace, name, body }) { end: timeWindowEnd }, autoUpdate: { - kubernetesVersion: updateKubernetesVersion + kubernetesVersion: updateKubernetesVersion, + machineImageVersion: updateOSVersion } } } diff --git a/backend/package-lock.json b/backend/package-lock.json index 49ca763a05..f9b97f486d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -5985,7 +5985,7 @@ }, "p-is-promise": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=" }, "p-limit": { @@ -6340,7 +6340,7 @@ }, "query-string": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "resolved": "http://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "requires": { "decode-uri-component": "^0.2.0", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f8471d11bd..e07d3244ac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -189,8 +189,7 @@ "@mdi/font": { "version": "3.8.95", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-3.8.95.tgz", - "integrity": "sha512-KR6TBdVYV3p1ZOM+Ge45Kk3P8yHfSGt3YOryVlkEKdO3vMiPjRaaDcQsdogGxVl8FcXsAt/Gz8MoXObbOF1Pzg==", - "dev": true + "integrity": "sha512-KR6TBdVYV3p1ZOM+Ge45Kk3P8yHfSGt3YOryVlkEKdO3vMiPjRaaDcQsdogGxVl8FcXsAt/Gz8MoXObbOF1Pzg==" }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", @@ -941,7 +940,7 @@ }, "acorn-jsx": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", "dev": true, "optional": true, @@ -951,7 +950,7 @@ "dependencies": { "acorn": { "version": "3.3.0", - "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", "dev": true, "optional": true @@ -1112,7 +1111,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -1124,7 +1123,7 @@ }, "array-flatten": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, @@ -1214,7 +1213,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -1345,7 +1344,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -1674,7 +1673,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -1711,7 +1710,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -1756,7 +1755,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -1914,7 +1913,7 @@ }, "callsites": { "version": "0.2.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true, "optional": true @@ -2866,7 +2865,7 @@ "dependencies": { "css-color-names": { "version": "0.0.1", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.1.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.1.tgz", "integrity": "sha1-XQVI+iVkVu3kqaDCrHqxnT6xrYE=" } } @@ -3511,7 +3510,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -4205,7 +4204,7 @@ }, "doctrine": { "version": "1.5.0", - "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", "dev": true, "requires": { @@ -4372,7 +4371,7 @@ }, "espree": { "version": "3.5.4", - "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", "dev": true, "optional": true, @@ -6521,7 +6520,7 @@ }, "is-empty": { "version": "0.0.1", - "resolved": "http://registry.npmjs.org/is-empty/-/is-empty-0.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-0.0.1.tgz", "integrity": "sha1-Cf3D1kndpZaRVsCFOpt2vXgcWjM=" }, "is-extendable": { @@ -6991,7 +6990,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -7161,8 +7160,7 @@ "material-design-icons-iconfont": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-5.0.1.tgz", - "integrity": "sha512-Xg6rIdGrfySTqiTZ6d+nQbcFepS6R4uKbJP0oAqyeZXJY/bX6mZDnOmmUJusqLXfhIwirs0c++a6JpqVa8RFvA==", - "dev": true + "integrity": "sha512-Xg6rIdGrfySTqiTZ6d+nQbcFepS6R4uKbJP0oAqyeZXJY/bX6mZDnOmmUJusqLXfhIwirs0c++a6JpqVa8RFvA==" }, "md5": { "version": "2.2.1", @@ -7200,7 +7198,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -7381,7 +7379,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -7426,7 +7424,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -8096,7 +8094,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -8281,7 +8279,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -9271,7 +9269,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -9290,7 +9288,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -9379,7 +9377,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -9423,7 +9421,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -9528,7 +9526,7 @@ }, "require-uncached": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", "dev": true, "optional": true, @@ -9617,7 +9615,7 @@ }, "rgba-regex": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", "dev": true }, @@ -9692,7 +9690,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -9940,7 +9938,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -10574,7 +10572,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -10606,7 +10604,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -10676,13 +10674,13 @@ }, "sax": { "version": "0.5.8", - "resolved": "http://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", "dev": true }, "source-map": { "version": "0.1.43", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", "dev": true, "requires": { @@ -10768,7 +10766,7 @@ }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true, "optional": true @@ -10916,7 +10914,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -11153,8 +11151,7 @@ "typeface-roboto": { "version": "0.0.75", "resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-0.0.75.tgz", - "integrity": "sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==", - "dev": true + "integrity": "sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==" }, "uglify-js": { "version": "3.4.10", @@ -11488,7 +11485,7 @@ }, "vue-eslint-parser": { "version": "2.0.3", - "resolved": "http://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==", "dev": true, "optional": true, @@ -11626,7 +11623,7 @@ }, "wcag-contrast": { "version": "0.1.0", - "resolved": "http://registry.npmjs.org/wcag-contrast/-/wcag-contrast-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/wcag-contrast/-/wcag-contrast-0.1.0.tgz", "integrity": "sha1-auN5VOQElBcY7aGh2OJw2bVnGTQ=", "requires": { "hex-rgb": "~1.0.0", @@ -12058,7 +12055,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/frontend/src/assets/coreos.svg b/frontend/src/assets/coreos.svg new file mode 100644 index 0000000000..d623ade410 --- /dev/null +++ b/frontend/src/assets/coreos.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/create_cluster_background.svg b/frontend/src/assets/create_cluster_background.svg deleted file mode 100644 index c216cccfe4..0000000000 --- a/frontend/src/assets/create_cluster_background.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - create_cluster_background - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/src/assets/suse.svg b/frontend/src/assets/suse.svg new file mode 100644 index 0000000000..cc5b5611d6 --- /dev/null +++ b/frontend/src/assets/suse.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/ubuntu.svg b/frontend/src/assets/ubuntu.svg new file mode 100644 index 0000000000..6c0663127a --- /dev/null +++ b/frontend/src/assets/ubuntu.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/CloudProfile.vue b/frontend/src/components/CloudProfile.vue index 637626139a..37efa7433e 100644 --- a/frontend/src/components/CloudProfile.vue +++ b/frontend/src/components/CloudProfile.vue @@ -70,11 +70,11 @@ export default { }, data () { return { - validationErrors + validationErrors, + valid: undefined } }, validations () { - // had to move the code to a computed property so that the getValidationErrors method can access it return this.validators }, computed: { @@ -92,8 +92,15 @@ export default { }, onInput (value) { this.$v.value.$touch() - this.$emit('input', value) + this.validateInput() + }, + validateInput () { + const valid = !this.$v.$invalid + if (this.valid !== valid) { + this.valid = valid + this.$emit('valid', valid) + } } } } diff --git a/frontend/src/components/ClusterMetrics.vue b/frontend/src/components/ClusterMetrics.vue index e97436fc81..49ef483e0c 100644 --- a/frontend/src/components/ClusterMetrics.vue +++ b/frontend/src/components/ClusterMetrics.vue @@ -23,7 +23,7 @@ limitations under the License. Grafana - + {{grafanaUrlOperators}} Grafana is not running for hibernated clusters @@ -38,7 +38,7 @@ limitations under the License. Grafana - + {{grafanaUrlUsers}} Grafana is not running for hibernated clusters @@ -52,7 +52,7 @@ limitations under the License. Prometheus - + {{prometheusUrl}} Prometheus is not running for hibernated clusters @@ -66,7 +66,7 @@ limitations under the License. Alertmanager - + {{alertmanagerUrl}} Alertmanager is not running for hibernated clusters @@ -82,8 +82,8 @@ limitations under the License. diff --git a/frontend/src/components/DisabledSecret.vue b/frontend/src/components/DisabledSecret.vue index 3a4ef8aeb8..7b4c1a5972 100644 --- a/frontend/src/components/DisabledSecret.vue +++ b/frontend/src/components/DisabledSecret.vue @@ -36,7 +36,7 @@ limitations under the License. diff --git a/frontend/src/components/NewShoot/NewShootInfrastructureDetails.vue b/frontend/src/components/NewShoot/NewShootInfrastructureDetails.vue new file mode 100644 index 0000000000..2c0640b72a --- /dev/null +++ b/frontend/src/components/NewShoot/NewShootInfrastructureDetails.vue @@ -0,0 +1,431 @@ + + + + + + + diff --git a/frontend/src/components/NewShoot/NewShootSelectInfrastructure.vue b/frontend/src/components/NewShoot/NewShootSelectInfrastructure.vue new file mode 100644 index 0000000000..f06f6c03c8 --- /dev/null +++ b/frontend/src/components/NewShoot/NewShootSelectInfrastructure.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/frontend/src/components/PurposeConfiguration.vue b/frontend/src/components/PurposeConfiguration.vue new file mode 100644 index 0000000000..7b7abd3798 --- /dev/null +++ b/frontend/src/components/PurposeConfiguration.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/frontend/src/components/PurposeTag.vue b/frontend/src/components/PurposeTag.vue index 35718f390a..4fa9d87bcd 100644 --- a/frontend/src/components/PurposeTag.vue +++ b/frontend/src/components/PurposeTag.vue @@ -41,6 +41,8 @@ export default { return 'PROD' case 'infrastructure': return 'INFRA' + case 'testing': + return 'TEST' default: return toUpper(this.purpose) } diff --git a/frontend/src/components/ReconcileStart.vue b/frontend/src/components/ReconcileStart.vue index 3d7fae6ddb..6140ec369e 100644 --- a/frontend/src/components/ReconcileStart.vue +++ b/frontend/src/components/ReconcileStart.vue @@ -15,51 +15,37 @@ limitations under the License. --> diff --git a/frontend/src/components/ShootAddons/ManageAddons.vue b/frontend/src/components/ShootAddons/ManageAddons.vue new file mode 100644 index 0000000000..186f802bbf --- /dev/null +++ b/frontend/src/components/ShootAddons/ManageAddons.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/frontend/src/components/ShootAddonsCard.vue b/frontend/src/components/ShootAddonsCard.vue deleted file mode 100644 index 89b2d35893..0000000000 --- a/frontend/src/components/ShootAddonsCard.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - diff --git a/frontend/src/components/ClusterAccess.vue b/frontend/src/components/ShootDetails/ShootAccessCard.vue similarity index 85% rename from frontend/src/components/ClusterAccess.vue rename to frontend/src/components/ShootDetails/ShootAccessCard.vue index 69f8d4a3a9..96d0112072 100644 --- a/frontend/src/components/ClusterAccess.vue +++ b/frontend/src/components/ShootDetails/ShootAccessCard.vue @@ -33,7 +33,7 @@ limitations under the License. Dashboard - + {{dashboardUrlText}} Dashboard is not running for hibernated clusters @@ -75,7 +75,7 @@ limitations under the License. - + @@ -88,8 +88,8 @@ import UsernamePassword from '@/components/UsernamePasswordListTile' import CopyBtn from '@/components/CopyBtn' import CodeBlock from '@/components/CodeBlock' import get from 'lodash/get' -import { isHibernated, getProjectName } from '@/utils' import download from 'downloadjs' +import { shootItem } from '@/mixins/shootItem' export default { components: { @@ -98,7 +98,7 @@ export default { CopyBtn }, props: { - item: { + shootItem: { type: Object } }, @@ -107,39 +107,29 @@ export default { expandKubeconfigIndex: null } }, + mixins: [shootItem], computed: { dashboardUrl () { if (!this.hasDashboardEnabled) { return '' } - return this.info.dashboardUrl || '' + return this.shootInfo.dashboardUrl || '' }, dashboardUrlText () { - return this.info.dashboardUrlText || '' + return this.shootInfo.dashboardUrlText || '' }, username () { - return this.info.cluster_username || '' + return this.shootInfo.cluster_username || '' }, password () { - return this.info.cluster_password || '' - }, - name () { - return get(this.item, 'metadata.name') - }, - metadata () { - return get(this.item, 'metadata') - }, - info () { - return get(this.item, 'info', {}) + return this.shootInfo.cluster_password || '' }, hasDashboardEnabled () { - return get(this.item, 'spec.addons.kubernetes-dashboard.enabled', false) === true - }, - isHibernated () { - return isHibernated(get(this.item, 'spec')) + return get(this.shootItem, 'spec.addons.kubernetes-dashboard.enabled', false) === true }, + kubeconfig () { - return get(this, 'info.kubeconfig') + return get(this.shootInfo, 'kubeconfig') }, visibilityIconKubeconfig () { if (this.isKubeconfigVisible) { @@ -159,8 +149,7 @@ export default { return this.expandKubeconfigIndex === 0 }, getQualifiedName () { - const projectName = getProjectName(this.metadata) - return `kubeconfig--${projectName}--${this.name}.yaml` + return `kubeconfig--${this.shootProjectName}--${this.shootName}.yaml` }, hasVisibleProperties () { return !!this.dashboardUrl || (!!this.username && !!this.password) || !!this.kubeconfig diff --git a/frontend/src/components/ShootDetailsCard.vue b/frontend/src/components/ShootDetails/ShootDetailsCard.vue similarity index 67% rename from frontend/src/components/ShootDetailsCard.vue rename to frontend/src/components/ShootDetails/ShootDetailsCard.vue index e8c18fa7f2..269ec4b518 100644 --- a/frontend/src/components/ShootDetailsCard.vue +++ b/frontend/src/components/ShootDetails/ShootDetailsCard.vue @@ -24,7 +24,7 @@ limitations under the License. info_outline Name
- {{metadata.name}} + {{shootName}}
@@ -48,7 +48,7 @@ limitations under the License. mdi-cube-outline Kubernetes Version
- {{k8sVersion}} + {{shootK8sVersion}}
@@ -63,9 +63,9 @@ limitations under the License. Worker Groups
@@ -78,34 +78,54 @@ limitations under the License. - + perm_identity Created by
- + - +
- @@ -114,18 +134,20 @@ limitations under the License. import AccountAvatar from '@/components/AccountAvatar' import TimeString from '@/components/TimeString' -import WorkerGroup from '@/components/WorkerGroup' -import WorkerConfiguration from '@/components/WorkerConfiguration' -import ShootVersion from '@/components/ShootVersion' -import get from 'lodash/get' +import WorkerGroup from '@/components/ShootWorkers/WorkerGroup' +import WorkerConfiguration from '@/components/ShootWorkers/WorkerConfiguration' +import PurposeConfiguration from '@/components/PurposeConfiguration' +import ShootVersion from '@/components/ShootVersion/ShootVersion' +import AddonConfiguration from '@/components/ShootAddons/AddonConfiguration' +import filter from 'lodash/filter' +import map from 'lodash/map' import { - getDateFormatted, isSelfTerminationWarning, isValidTerminationDate, getTimeStringTo, - getCloudProviderKind, - getCreatedBy + shootAddonList } from '@/utils' +import { shootItem } from '@/mixins/shootItem' export default { components: { @@ -133,6 +155,8 @@ export default { TimeString, WorkerGroup, WorkerConfiguration, + PurposeConfiguration, + AddonConfiguration, ShootVersion }, props: { @@ -140,27 +164,10 @@ export default { type: Object } }, + mixins: [shootItem], computed: { - metadata () { - return get(this.shootItem, 'metadata', {}) - }, - annotations () { - return this.metadata.annotations || {} - }, - k8sVersion () { - return get(this.shootItem, 'spec.kubernetes.version') - }, - purpose () { - return this.annotations['garden.sapcloud.io/purpose'] - }, - createdBy () { - return getCreatedBy(this.metadata) - }, - created () { - return getDateFormatted(this.metadata.creationTimestamp) - }, expirationTimestamp () { - return this.annotations['shoot.garden.sapcloud.io/expirationTimestamp'] + return this.shootAnnotations['shoot.garden.sapcloud.io/expirationTimestamp'] }, selfTerminationMessage () { if (this.isValidTerminationDate) { @@ -175,15 +182,13 @@ export default { isValidTerminationDate () { return isValidTerminationDate(this.expirationTimestamp) }, - getCloudProviderKind () { - return getCloudProviderKind(get(this.shootItem, 'spec.cloud')) - }, - workerGroups () { - const kind = this.getCloudProviderKind - return get(this.shootItem, `spec.cloud.${kind}.workers`, []) + addon () { + return (name) => { + return this.shootAddons[name] || {} + } }, - cloudProfileName () { - return get(this.shootItem, 'spec.cloud.profile') + shootAddonNames () { + return map(filter(shootAddonList, item => this.addon(item.name).enabled), 'title') } } } diff --git a/frontend/src/components/ShootInfrastructureCard.vue b/frontend/src/components/ShootDetails/ShootInfrastructureCard.vue similarity index 60% rename from frontend/src/components/ShootInfrastructureCard.vue rename to frontend/src/components/ShootDetails/ShootInfrastructureCard.vue index a6b20f839f..ce75552af0 100644 --- a/frontend/src/components/ShootInfrastructureCard.vue +++ b/frontend/src/components/ShootDetails/ShootInfrastructureCard.vue @@ -20,52 +20,58 @@ limitations under the License. Infrastructure
+ - cloud_queue - - Provider
- - {{getCloudProviderKind}} - Provider - - / - - {{region}} - Region - - -
+ + + cloud_queue + + + + + Provider
+ {{shootCloudProviderKind}} +
+
+ + + Credential
+ + {{shootSecretName}} + +
+
+ + + {{regionZoneTitle}}
+ {{regionZoneText}} +
+
+
+
diff --git a/frontend/src/components/MachineType.vue b/frontend/src/components/ShootWorkers/MachineType.vue similarity index 86% rename from frontend/src/components/MachineType.vue rename to frontend/src/components/ShootWorkers/MachineType.vue index 561d495a97..e04f61ea0e 100644 --- a/frontend/src/components/MachineType.vue +++ b/frontend/src/components/ShootWorkers/MachineType.vue @@ -25,6 +25,8 @@ diff --git a/frontend/src/components/ManageWorkers.vue b/frontend/src/components/ShootWorkers/ManageWorkers.vue similarity index 57% rename from frontend/src/components/ManageWorkers.vue rename to frontend/src/components/ShootWorkers/ManageWorkers.vue index 2d74868a93..c522c6f864 100644 --- a/frontend/src/components/ManageWorkers.vue +++ b/frontend/src/components/ShootWorkers/ManageWorkers.vue @@ -16,18 +16,13 @@ limitations under the License. - - diff --git a/frontend/src/components/VolumeSizeInput.vue b/frontend/src/components/ShootWorkers/VolumeSizeInput.vue similarity index 100% rename from frontend/src/components/VolumeSizeInput.vue rename to frontend/src/components/ShootWorkers/VolumeSizeInput.vue diff --git a/frontend/src/components/VolumeType.vue b/frontend/src/components/ShootWorkers/VolumeType.vue similarity index 85% rename from frontend/src/components/VolumeType.vue rename to frontend/src/components/ShootWorkers/VolumeType.vue index 92734dbb7e..2147950978 100644 --- a/frontend/src/components/VolumeType.vue +++ b/frontend/src/components/ShootWorkers/VolumeType.vue @@ -21,6 +21,9 @@ diff --git a/frontend/src/components/ShootWorkers/WorkerConfiguration.vue b/frontend/src/components/ShootWorkers/WorkerConfiguration.vue new file mode 100644 index 0000000000..170b3c9aa7 --- /dev/null +++ b/frontend/src/components/ShootWorkers/WorkerConfiguration.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/components/WorkerGroup.vue b/frontend/src/components/ShootWorkers/WorkerGroup.vue similarity index 69% rename from frontend/src/components/WorkerGroup.vue rename to frontend/src/components/ShootWorkers/WorkerGroup.vue index b517832646..f225550c7d 100644 --- a/frontend/src/components/WorkerGroup.vue +++ b/frontend/src/components/ShootWorkers/WorkerGroup.vue @@ -27,10 +27,9 @@ limitations under the License. :key="index" fill-height align-center> - {{line.icon}} {{line.title}}: {{line.value}} {{line.description}} - {{workerGroup.name}} + {{workerGroup.name}} @@ -39,6 +38,7 @@ limitations under the License. import GPopper from '@/components/GPopper' import find from 'lodash/find' import { mapGetters } from 'vuex' +import { getTimestampFormatted } from '@/utils' export default { name: 'worker-group', @@ -55,28 +55,30 @@ export default { }, computed: { ...mapGetters([ - 'machineTypesByCloudProfileName', - 'volumeTypesByCloudProfileName' + 'machineTypesByCloudProfileNameAndZones', + 'volumeTypesByCloudProfileNameAndZones', + 'machineImagesByCloudProfileName' ]), machineTypes () { - return this.machineTypesByCloudProfileName(this.cloudProfileName) + return this.machineTypesByCloudProfileNameAndZones({ cloudProfileName: this.cloudProfileName }) }, volumeTypes () { - return this.volumeTypesByCloudProfileName(this.cloudProfileName) + return this.volumeTypesByCloudProfileNameAndZones({ cloudProfileName: this.cloudProfileName }) + }, + machineImages () { + return this.machineImagesByCloudProfileName(this.cloudProfileName) }, description () { const description = [] if (this.workerGroup.machineType) { const machineType = find(this.machineTypes, { name: this.workerGroup.machineType }) description.push({ - icon: 'mdi-speedometer', title: 'Machine Type', value: machineType.name, description: `(CPU: ${machineType.cpu} | GPU: ${machineType.gpu} | Memory: ${machineType.memory})` }) if (machineType.volumeType && machineType.volumeSize) { description.push({ - icon: 'mdi-harddisk', title: 'Volume Type', value: `${machineType.volumeType} / ${machineType.volumeSize}` }) @@ -85,19 +87,34 @@ export default { if (this.workerGroup.volumeType && this.workerGroup.volumeSize) { const volumeType = find(this.volumeTypes, { name: this.workerGroup.volumeType }) description.push({ - icon: 'mdi-harddisk', title: 'Volume Type', value: `${volumeType.name} / ${this.workerGroup.volumeSize}`, description: `(Class: ${volumeType.class})` }) } + if (this.workerGroup.machineImage) { + const machineImage = find(this.machineImages, { name: this.workerGroup.machineImage.name }) + const machineImageDescription = { + title: 'Machine Image', + value: `${machineImage.name} | Version: ${machineImage.version}` + } + if (machineImage.expirationDate) { + machineImageDescription.description = `(Expiration Date: ${getTimestampFormatted(machineImage.expirationDate)})` + } + description.push(machineImageDescription) + } if (this.workerGroup.autoScalerMin && this.workerGroup.autoScalerMax) { description.push({ - icon: 'mdi-arrow-expand-all', title: 'Autoscaler', value: `Min. ${this.workerGroup.autoScalerMin} / Max. ${this.workerGroup.autoScalerMax}` }) } + if (this.workerGroup.maxSurge) { + description.push({ + title: 'Max. Surge', + value: `${this.workerGroup.maxSurge}` + }) + } return description } } diff --git a/frontend/src/components/ShootWorkers/WorkerInputGeneric.vue b/frontend/src/components/ShootWorkers/WorkerInputGeneric.vue new file mode 100644 index 0000000000..35f098ac76 --- /dev/null +++ b/frontend/src/components/ShootWorkers/WorkerInputGeneric.vue @@ -0,0 +1,326 @@ + + + + + + + diff --git a/frontend/src/components/InfrastructureIcon.vue b/frontend/src/components/VendorIcon.vue similarity index 79% rename from frontend/src/components/InfrastructureIcon.vue rename to frontend/src/components/VendorIcon.vue index 655856f3af..1bb17a3208 100644 --- a/frontend/src/components/InfrastructureIcon.vue +++ b/frontend/src/components/VendorIcon.vue @@ -16,7 +16,7 @@ limitations under the License. @@ -30,8 +30,10 @@ export default { required: true }, width: { - type: Number, - default: 20 + type: Number + }, + height: { + type: Number }, contentClass: { type: String, @@ -67,8 +69,23 @@ export default { return require('@/assets/vmware.svg') case 'china-telecom': return require('@/assets/china-telecom.svg') + case 'coreos': + return require('@/assets/coreos.svg') + case 'suse': + return require('@/assets/suse.svg') + case 'ubuntu': + return require('@/assets/ubuntu.svg') } return undefined + }, + getHeight () { + return this.height + }, + getWidth () { + if (!this.width && !this.height) { + return 20 + } + return this.width } } } diff --git a/frontend/src/components/WorkerConfiguration.vue b/frontend/src/components/WorkerConfiguration.vue deleted file mode 100644 index e1fcb92c7f..0000000000 --- a/frontend/src/components/WorkerConfiguration.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - diff --git a/frontend/src/components/WorkerInputGeneric.vue b/frontend/src/components/WorkerInputGeneric.vue deleted file mode 100644 index 58a98c19ac..0000000000 --- a/frontend/src/components/WorkerInputGeneric.vue +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - - diff --git a/frontend/src/dialogs/ActionIconDialog.vue b/frontend/src/dialogs/ActionIconDialog.vue new file mode 100644 index 0000000000..b08fabe184 --- /dev/null +++ b/frontend/src/dialogs/ActionIconDialog.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/frontend/src/dialogs/ConfirmDialog.vue b/frontend/src/dialogs/ConfirmDialog.vue index 0487e70371..72c56959f8 100644 --- a/frontend/src/dialogs/ConfirmDialog.vue +++ b/frontend/src/dialogs/ConfirmDialog.vue @@ -15,181 +15,48 @@ limitations under the License. --> - - diff --git a/frontend/src/dialogs/GDialog.vue b/frontend/src/dialogs/GDialog.vue new file mode 100644 index 0000000000..e38c3c29cf --- /dev/null +++ b/frontend/src/dialogs/GDialog.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/frontend/src/dialogs/MemberAddDialog.vue b/frontend/src/dialogs/MemberAddDialog.vue index 7eb5b1356e..80a68b2f84 100644 --- a/frontend/src/dialogs/MemberAddDialog.vue +++ b/frontend/src/dialogs/MemberAddDialog.vue @@ -50,7 +50,7 @@ limitations under the License. persistent-hint tabindex="1" > - + @@ -66,7 +66,7 @@ import toLower from 'lodash/toLower' import { mapActions, mapState, mapGetters } from 'vuex' import { required } from 'vuelidate/lib/validators' import { resourceName, unique } from '@/utils/validators' -import Alert from '@/components/Alert' +import GAlert from '@/components/GAlert' import { errorDetailsFromError, isConflict } from '@/utils/error' import { serviceAccountToDisplayName, isServiceAccount } from '@/utils' import filter from 'lodash/filter' @@ -79,7 +79,7 @@ const defaultServiceName = 'robot' export default { name: 'add-member-dialog', components: { - Alert + GAlert }, props: { value: { diff --git a/frontend/src/dialogs/ProjectDialog.vue b/frontend/src/dialogs/ProjectDialog.vue index 20271842ab..586c776fcd 100644 --- a/frontend/src/dialogs/ProjectDialog.vue +++ b/frontend/src/dialogs/ProjectDialog.vue @@ -86,7 +86,7 @@ limitations under the License. > - + @@ -129,7 +129,7 @@ import get from 'lodash/get' import includes from 'lodash/includes' import concat from 'lodash/concat' import filter from 'lodash/filter' -import Alert from '@/components/Alert' +import GAlert from '@/components/GAlert' import { errorDetailsFromError, isConflict, isGatewayTimeout } from '@/utils/error' const defaultProjectName = '' @@ -151,7 +151,7 @@ const validationErrors = { export default { name: 'project-dialog', components: { - Alert + GAlert }, props: { value: { diff --git a/frontend/src/dialogs/SecretDialog.vue b/frontend/src/dialogs/SecretDialog.vue index 8597028728..18dc9bac3d 100644 --- a/frontend/src/dialogs/SecretDialog.vue +++ b/frontend/src/dialogs/SecretDialog.vue @@ -68,7 +68,7 @@ limitations under the License. - + @@ -94,8 +94,8 @@ import get from 'lodash/get' import head from 'lodash/head' import sortBy from 'lodash/sortBy' import filter from 'lodash/filter' -import Alert from '@/components/Alert' -import InfraIcon from '@/components/InfrastructureIcon' +import GAlert from '@/components/GAlert' +import InfraIcon from '@/components/VendorIcon' import { errorDetailsFromError, isConflict } from '@/utils/error' const validationErrors = { @@ -111,7 +111,7 @@ export default { name: 'secret-dialog', components: { CloudProfile, - Alert, + GAlert, InfraIcon }, props: { diff --git a/frontend/src/dialogs/SecretDialogDelete.vue b/frontend/src/dialogs/SecretDialogDelete.vue index eb9fdcd2ef..6f9f546a13 100644 --- a/frontend/src/dialogs/SecretDialogDelete.vue +++ b/frontend/src/dialogs/SecretDialogDelete.vue @@ -41,7 +41,7 @@ limitations under the License. can not be undone.
- + @@ -55,13 +55,13 @@ limitations under the License. diff --git a/frontend/src/layouts/Login.vue b/frontend/src/layouts/Login.vue index d393a08ee6..d5c16d10b3 100644 --- a/frontend/src/layouts/Login.vue +++ b/frontend/src/layouts/Login.vue @@ -319,14 +319,15 @@ export default { } } - @import "~vue-snotify/styles/material.css" + @import "~vue-snotify/styles/material.css"; + @import '~vuetify/src/stylus/settings/_colors.styl'; .snotify-rightTop { top: 75px; } .snotify-info { - background-color: #0097A7; // cyan darken-2 + background-color: $cyan.darken-2 } .snotify { diff --git a/frontend/src/mixins/shootItem.js b/frontend/src/mixins/shootItem.js new file mode 100644 index 0000000000..348d70665a --- /dev/null +++ b/frontend/src/mixins/shootItem.js @@ -0,0 +1,145 @@ +import get from 'lodash/get' + +import { + getDateFormatted, + getCreatedBy, + isHibernated, + isReconciliationDeactivated, + getCloudProviderKind, + getProjectName +} from '@/utils' + +export const shootItem = { + computed: { + shootMetadata () { + return get(this.shootItem, 'metadata', {}) + }, + + shootName () { + return this.shootMetadata.name + }, + shootNamespace () { + return this.shootMetadata.namespace + }, + isShootMarkedForDeletion () { + const confirmation = get(this.shootAnnotations['confirmation.garden.sapcloud.io/deletion'], false) + const deletionTimestamp = this.shootDeletionTimestamp + + return !!deletionTimestamp && !!confirmation + }, + shootCreatedBy () { + return getCreatedBy(this.shootMetadata) + }, + shootCreatedAt () { + return getDateFormatted(this.shootMetadata.creationTimestamp) + }, + shootCreationTimestamp () { + return this.shootMetadata.creationTimestamp + }, + isShootReconciliationDeactivated () { + return isReconciliationDeactivated(this.shootMetadata) + }, + shootGenerationValue () { + return this.shootMetadata.generation + }, + shootDeletionTimestamp () { + return this.shootMetadata.deletionTimestamp + }, + shootProjectName () { + return getProjectName(this.shootMetadata) + }, + + shootAnnotations () { + return get(this.shootMetadata, 'annotations', {}) + }, + shootGardenOperation () { + return this.shootAnnotations['shoot.garden.sapcloud.io/operation'] + }, + shootPurpose () { + return this.shootAnnotations['garden.sapcloud.io/purpose'] + }, + shootExpirationTimestamp () { + return this.shootAnnotations['shoot.garden.sapcloud.io/expirationTimestamp'] + }, + isShootActionsDisabledForPurpose () { + return this.shootPurpose === 'infrastructure' + }, + + shootSpec () { + return get(this.shootItem, 'spec', {}) + }, + isShootHibernated () { + return isHibernated(this.shootSpec) + }, + shootSecret () { + return get(this.shootSpec, 'cloud.secretBindingRef.name') + }, + shootK8sVersion () { + return get(this.shootSpec, 'kubernetes.version') + }, + shootCloudProfileName () { + return get(this.shootSpec, 'cloud.profile') + }, + shootCloudProviderKind () { + return getCloudProviderKind(this.shootSpec.cloud) + }, + shootWorkerGroups () { + return get(this.shootSpec, `cloud.${this.shootCloudProviderKind}.workers`, []) + }, + shootAddons () { + return get(this.shootSpec, 'addons', {}) + }, + shootRegion () { + return get(this.shootSpec, 'cloud.region') + }, + shootZones () { + return get(this.shootSpec, ['cloud', this.shootCloudProviderKind, 'zones'], []) + }, + shootCidr () { + return get(this.shootSpec, ['cloud', this.shootCloudProviderKind, 'networks', 'nodes']) + }, + shootSeed () { + return get(this.shootSpec, 'cloud.seed') + }, + shootDomain () { + return get(this.shootSpec, 'dns.domain') + }, + shootHibernationSchedules () { + return get(this.shootSpec, 'hibernation.schedules', []) + }, + shootMaintenance () { + return get(this.shootSpec, 'maintenance', []) + }, + + shootInfo () { + return get(this.shootItem, 'info', {}) + }, + seedShootIngressDomain () { + return this.shootInfo.seedShootIngressDomain || '' + }, + + shootLastOperation () { + return get(this.shootItem, 'status.lastOperation', {}) + }, + shootLastError () { + return get(this.shootItem, 'status.lastError', {}) + }, + shootConditions () { + return get(this.shootItem, 'status.conditions', []) + }, + shootObservedGeneration () { + return get(this.shootItem, 'status.observedGeneration', []) + }, + shootTechnicalId () { + return get(this.shootItem, `status.technicalID`) + } + }, + methods: { + shootActionToolTip (tooltip) { + if (!this.isShootActionsDisabledForPurpose) { + return tooltip + } + return 'Actions disabled for cluster purpose infrastructure' + } + } +} diff --git a/frontend/src/pages/Administration.vue b/frontend/src/pages/Administration.vue index 3a7665d0d8..0d9f858faf 100644 --- a/frontend/src/pages/Administration.vue +++ b/frontend/src/pages/Administration.vue @@ -23,7 +23,7 @@ limitations under the License. Project Details - + delete You can only delete projects that do not contain clusters @@ -74,22 +74,20 @@ limitations under the License. - -
+ :detailedErrorMessage.sync="detailedErrorMessage" + ref="gDialog"> + + + @@ -98,7 +96,7 @@ import { mapState, mapGetters, mapActions } from 'vuex' import find from 'lodash/find' import AccountAvatar from '@/components/AccountAvatar' import UpdateDialog from '@/dialogs/ProjectDialog' -import ConfirmDialog from '@/dialogs/ConfirmDialog' +import GDialog from '@/dialogs/GDialog' import TimeString from '@/components/TimeString' import { getDateFormatted } from '@/utils' import { errorDetailsFromError } from '@/utils/error' @@ -108,13 +106,12 @@ export default { components: { AccountAvatar, UpdateDialog, - ConfirmDialog, + GDialog, TimeString }, data () { return { edit: false, - deleteConfirm: false, floatingButton: false, errorMessage: undefined, detailedErrorMessage: undefined @@ -161,28 +158,32 @@ export default { ...mapActions([ 'deleteProject' ]), - hide () { + async showDialog () { + this.$refs.gDialog.showDialog() + + const confirmed = await this.$refs.gDialog.confirmWithDialog() + if (confirmed) { + try { + await this.deleteProject(this.project) + if (this.projectList.length > 0) { + const p1 = this.projectList[0] + this.$router.push({ name: 'ShootList', params: { namespace: p1.metadata.namespace } }) + } else { + this.$router.push({ name: 'Home', params: { } }) + } + } catch (err) { + const errorDetails = errorDetailsFromError(err) + this.errorMessage = 'Failed to delete project' + this.detailedErrorMessage = errorDetails.detailedMessage + console.error(this.errorMessage, errorDetails.errorCode, errorDetails.detailedMessage, err) + this.showDialog() + } + } + }, + reset () { this.errorMessage = undefined this.detailedMessage = undefined - this.deleteConfirm = false this.edit = false - }, - async onDeleteProject () { - try { - await this.deleteProject(this.project) - this.hide() - if (this.projectList.length > 0) { - const p1 = this.projectList[0] - this.$router.push({ name: 'ShootList', params: { namespace: p1.metadata.namespace } }) - } else { - this.$router.push({ name: 'Home', params: { } }) - } - } catch (err) { - const errorDetails = errorDetailsFromError(err) - this.errorMessage = 'Failed to delete project.' - this.detailedErrorMessage = errorDetails.detailedMessage - console.error(this.errorMessage, errorDetails.errorCode, errorDetails.detailedMessage, err) - } } }, mounted () { diff --git a/frontend/src/pages/NewShoot.vue b/frontend/src/pages/NewShoot.vue new file mode 100644 index 0000000000..03d0bbab78 --- /dev/null +++ b/frontend/src/pages/NewShoot.vue @@ -0,0 +1,392 @@ + + + + + + + diff --git a/frontend/src/pages/NewShootEditor.vue b/frontend/src/pages/NewShootEditor.vue new file mode 100644 index 0000000000..83b02c71de --- /dev/null +++ b/frontend/src/pages/NewShootEditor.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/frontend/src/pages/Secrets.vue b/frontend/src/pages/Secrets.vue index 187ad7bf66..782c049573 100644 --- a/frontend/src/pages/Secrets.vue +++ b/frontend/src/pages/Secrets.vue @@ -130,21 +130,7 @@ limitations under the License. - - - - - - - - - - - - - - - + @@ -177,40 +163,22 @@ limitations under the License. import { mapGetters } from 'vuex' import get from 'lodash/get' import { isOwnSecretBinding, infrastructureColor } from '@/utils' -import GcpDialog from '@/dialogs/SecretDialogGcp' -import GcpHelpDialog from '@/dialogs/SecretDialogGcpHelp' -import AwsHelpDialog from '@/dialogs/SecretDialogAwsHelp' -import AwsDialog from '@/dialogs/SecretDialogAws' -import AzureDialog from '@/dialogs/SecretDialogAzure' -import AzureHelpDialog from '@/dialogs/SecretDialogAzureHelp' -import OpenstackDialog from '@/dialogs/SecretDialogOpenstack' -import OpenstackHelpDialog from '@/dialogs/SecretDialogOpenstackHelp' -import AlicloudDialog from '@/dialogs/SecretDialogAlicloud' -import AlicloudHelpDialog from '@/dialogs/SecretDialogAlicloudHelp' import DeleteDialog from '@/dialogs/SecretDialogDelete' +import SecretDialogWrapper from '@/dialogs/SecretDialogWrapper' import Secret from '@/components/Secret' import DisabledSecret from '@/components/DisabledSecret' -import InfraIcon from '@/components/InfrastructureIcon' +import InfraIcon from '@/components/VendorIcon' import isEmpty from 'lodash/isEmpty' import merge from 'lodash/merge' export default { name: 'secrets', components: { - GcpDialog, - GcpHelpDialog, - AzureHelpDialog, - AzureDialog, - AwsDialog, - AwsHelpDialog, - OpenstackDialog, - OpenstackHelpDialog, - AlicloudDialog, - AlicloudHelpDialog, DeleteDialog, Secret, DisabledSecret, - InfraIcon + InfraIcon, + SecretDialogWrapper }, data () { return { diff --git a/frontend/src/pages/ShootDetailsEditor.vue b/frontend/src/pages/ShootDetailsEditor.vue new file mode 100644 index 0000000000..57c96d3921 --- /dev/null +++ b/frontend/src/pages/ShootDetailsEditor.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/frontend/src/pages/ShootItemCards.vue b/frontend/src/pages/ShootItemCards.vue index aa41dd2716..38e44dd447 100644 --- a/frontend/src/pages/ShootItemCards.vue +++ b/frontend/src/pages/ShootItemCards.vue @@ -22,7 +22,7 @@ limitations under the License. - + @@ -30,21 +30,19 @@ limitations under the License. Access - + - + Logging - + - - - + @@ -56,14 +54,13 @@ limitations under the License. diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index e0ec535795..a2caf0cef3 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -21,7 +21,6 @@ import moment from 'moment-timezone' import includes from 'lodash/includes' import head from 'lodash/head' import get from 'lodash/get' -import concat from 'lodash/concat' import { getPrivileges } from '@/utils/api' /* Layouts */ @@ -30,10 +29,11 @@ const Default = () => import('@/layouts/Default') /* Pages */ const Home = () => import('@/pages/Home') +const NewShoot = () => import('@/pages/NewShoot') const ShootList = () => import('@/pages/ShootList') - const ShootItemCards = () => import('@/pages/ShootItemCards') -const ShootItemEditor = () => import('@/pages/ShootItemEditor') +const ShootDetailsEditor = () => import('@/pages/ShootDetailsEditor') +const NewShootEditor = () => import('@/pages/NewShootEditor') const Secrets = () => import('@/pages/Secrets') const Members = () => import('@/pages/Members') const Account = () => import('@/pages/Account') @@ -73,7 +73,27 @@ export default function createRouter ({ store, userManager }) { title: 'YAML', to: ({ params }) => { return { - name: 'ShootItemEditor', + name: 'ShootDetailsEditor', + params + } + } + } + ] + const newShootTabs = [ + { + title: 'Overview', + to: ({ params }) => { + return { + name: 'NewShoot', + params + } + } + }, + { + title: 'YAML', + to: ({ params }) => { + return { + name: 'NewShootEditor', params } } @@ -178,6 +198,31 @@ export default function createRouter ({ store, userManager }) { title: 'Project Clusters' } }, + { + path: '+', + name: 'NewShoot', + component: NewShoot, + meta: { + namespaced: true, + projectScope: false, + title: 'Create Cluster', + toRouteName: 'NewShoot', + breadcrumbTextFn: routeTitle, + tabs: newShootTabs + } + }, + { + path: '+/yaml', + name: 'NewShootEditor', + component: NewShootEditor, + meta: { + namespaced: true, + projectScope: false, + title: 'Create Cluster Editor', + breadcrumbTextFn: routeTitle, + tabs: newShootTabs + } + }, { path: ':name', name: 'ShootItem', @@ -193,8 +238,8 @@ export default function createRouter ({ store, userManager }) { }, { path: ':name/yaml', - name: 'ShootItemEditor', - component: ShootItemEditor, + name: 'ShootDetailsEditor', + component: ShootDetailsEditor, meta: { namespaced: true, projectScope: true, @@ -417,20 +462,27 @@ export default function createRouter ({ store, userManager }) { return undefined case 'Secrets': case 'Secret': + case 'NewShoot': return Promise .all([ store.dispatch('fetchInfrastructureSecrets'), store.dispatch('subscribeShoots') ]) - .then(() => undefined) - case 'ShootList': - const promises = [] - concat(promises, store.dispatch('subscribeShoots')) - if (namespace !== '_all') { - concat(promises, store.dispatch('fetchInfrastructureSecrets')) + .then(() => { + if (from.name !== 'NewShoot' && from.name !== 'NewShootEditor') { + return store.dispatch('resetNewShootResource', { name: params.name, namespace }) + .then(() => undefined) + } + return undefined + }) + case 'NewShootEditor': + if (from.name !== 'NewShoot' && from.name !== 'NewShootEditor') { + return store.dispatch('resetNewShootResource', { name: params.name, namespace }) + .then(() => undefined) } - return Promise - .all(promises) + return undefined + case 'ShootList': + return store.dispatch('subscribeShoots', { name: params.name, namespace }) .then(() => undefined) case 'ShootItem': case 'ShootItemHibernationSettings': @@ -440,7 +492,7 @@ export default function createRouter ({ store, userManager }) { store.dispatch('subscribeComments', { name: params.name, namespace }) ]) .then(() => undefined) - case 'ShootItemEditor': + case 'ShootDetailsEditor': return store.dispatch('subscribeShoot', { name: params.name, namespace }) .then(() => undefined) case 'Members': diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index a4d66903c6..8f7176dccd 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -30,6 +30,13 @@ import some from 'lodash/some' import concat from 'lodash/concat' import merge from 'lodash/merge' import difference from 'lodash/difference' +import forEach from 'lodash/forEach' +import isEmpty from 'lodash/isEmpty' +import intersection from 'lodash/intersection' +import find from 'lodash/find' +import head from 'lodash/head' +import lowerCase from 'lodash/lowerCase' +import cloneDeep from 'lodash/cloneDeep' import moment from 'moment-timezone' import shoots from './modules/shoots' @@ -39,6 +46,7 @@ import projects from './modules/projects' import members from './modules/members' import infrastructureSecrets from './modules/infrastructureSecrets' import journals from './modules/journals' +const semSort = require('semver-sort') Vue.use(Vuex) @@ -71,11 +79,19 @@ const getFilterValue = (state) => { return state.namespace === '_all' && state.onlyShootsWithIssues ? 'issues' : null } +const machineAndVolumeTypePredicate = (item, zones) => { + if (item.usable === false) { + return false + } + const itemZones = item.zones + if (isEmpty(itemZones) || isEmpty(zones)) { + return true + } + return difference(zones, itemZones).length === 0 +} + // getters const getters = { - customAddonDefinitionList (state) { - return get(state, 'cfg.customAddonDefinitions', []) - }, apiServerUrl (state) { return get(state.cfg, 'apiServerUrl', window.location.origin) }, @@ -96,18 +112,55 @@ const getters = { return filter(state.cloudProfiles.all, predicate) } }, - machineTypesByCloudProfileName (state, getters) { - return (cloudProfileName) => { + machineTypesByCloudProfileNameAndZones (state, getters) { + return ({ cloudProfileName, zones }) => { const cloudProfile = getters.cloudProfileByName(cloudProfileName) const machineTypes = get(cloudProfile, 'data.machineTypes') - return filter(machineTypes, machineType => get(machineType, 'usable', true) === true) + return filter(machineTypes, machineType => machineAndVolumeTypePredicate(machineType, zones)) } }, - volumeTypesByCloudProfileName (state, getters) { - return (cloudProfileName) => { + volumeTypesByCloudProfileNameAndZones (state, getters) { + return ({ cloudProfileName, zones }) => { const cloudProfile = getters.cloudProfileByName(cloudProfileName) const volumeTypes = get(cloudProfile, 'data.volumeTypes') - return filter(volumeTypes, volumeType => get(volumeType, 'usable', true) === true) + return filter(volumeTypes, volumeType => machineAndVolumeTypePredicate(volumeType, zones)) + } + }, + machineImagesByCloudProfileName (state, getters) { + return (cloudProfileName) => { + const cloudProfile = getters.cloudProfileByName(cloudProfileName) + const machineImages = get(cloudProfile, 'data.machineImages') + const allMachineImages = [] + forEach(machineImages, machineImage => { + forEach(machineImage.versions, version => { + if (!version.expirationDate || moment().isBefore(version.expirationDate)) { + allMachineImages.push({ + name: machineImage.name, + version: version.version, + expirationDate: version.expirationDate + }) + } + }) + }) + + return allMachineImages + } + }, + zonesByCloudProfileNameAndRegion (state, getters) { + return ({ cloudProfileName, region }) => { + const cloudProfile = getters.cloudProfileByName(cloudProfileName) + const predicate = item => item.region === region + return get(find(get(cloudProfile, 'data.zones'), predicate), 'names') + } + }, + defaultMachineImageForCloudProfileName (state, getters) { + return (cloudProfileName) => { + const machineImages = getters.machineImagesByCloudProfileName(cloudProfileName) + let defaultMachineImage = find(machineImages, machineImage => lowerCase(machineImage.name).includes('coreos') === true) + if (!defaultMachineImage) { + defaultMachineImage = head(machineImages) + } + return defaultMachineImage } }, shootList (state, getters) { @@ -136,6 +189,9 @@ const getters = { cloudProviderKindList (state) { return uniq(map(state.cloudProfiles.all, 'metadata.cloudProviderKind')) }, + sortedCloudProviderKindList (state, getters) { + return intersection(['aws', 'azure', 'gcp', 'openstack', 'alicloud'], getters.cloudProviderKindList) + }, regionsWithSeedByCloudProfileName (state, getters) { return (cloudProfileName) => { const cloudProfile = getters.cloudProfileByName(cloudProfileName) @@ -204,6 +260,11 @@ const getters = { return get(cloudProfile, 'data.kubernetes.versions', []) } }, + sortedKubernetesVersions (state, getters) { + return (cloudProfileName) => { + return semSort.desc(cloneDeep(getters.kubernetesVersions(cloudProfileName))) + } + }, isAdmin (state) { return get(state.user, 'isAdmin', false) }, @@ -264,6 +325,9 @@ const getters = { }, getShootListFilters (state, getters) { return getters['shoots/getShootListFilters'] + }, + newShootResource (state, getters) { + return getters['shoots/newShootResource'] } } @@ -383,6 +447,12 @@ const actions = { dispatch('setError', err) }) }, + setNewShootResource ({ dispatch }, data) { + return dispatch('shoots/setNewShootResource', data) + }, + resetNewShootResource ({ dispatch }) { + return dispatch('shoots/resetNewShootResource') + }, createProject ({ dispatch, commit }, data) { return dispatch('projects/create', data) .then(res => { diff --git a/frontend/src/store/modules/shoots.js b/frontend/src/store/modules/shoots.js index d07125501e..ea0277aa29 100644 --- a/frontend/src/store/modules/shoots.js +++ b/frontend/src/store/modules/shoots.js @@ -31,10 +31,14 @@ import filter from 'lodash/filter' import includes from 'lodash/includes' import split from 'lodash/split' import join from 'lodash/join' +import set from 'lodash/set' import head from 'lodash/head' +import sample from 'lodash/sample' +import isEmpty from 'lodash/isEmpty' import semver from 'semver' import store from '../' import { getShoot, getShootInfo, createShoot, deleteShoot } from '@/utils/api' +import { getCloudProviderTemplate } from '@/utils/createShoot' import { isNotFound } from '@/utils/error' import { isHibernated, getCloudProviderKind, @@ -43,7 +47,12 @@ import { isHibernated, isStatusProgressing, getCreatedBy, getProjectName, - shootHasIssue } from '@/utils' + shootHasIssue, + purposesForSecret, + shortRandomString, + shootAddonList, + utcMaintenanceWindowFromLocalBegin, + randomLocalMaintenanceBegin } from '@/utils' const uriPattern = /^([^:/?#]+:)?(\/\/[^/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/ @@ -63,7 +72,8 @@ const state = { sortParams: undefined, searchValue: undefined, selection: undefined, - shootListFilters: undefined + shootListFilters: undefined, + newShootResource: undefined } // getters @@ -83,6 +93,9 @@ const getters = { }, getShootListFilters () { return state.shootListFilters + }, + newShootResource () { + return state.newShootResource } } @@ -200,6 +213,16 @@ const actions = { commit('SET_SHOOT_LIST_FILTER', { rootState, filterValue }) return state.shootListFilters } + }, + setNewShootResource ({ commit }, data) { + commit('SET_NEW_SHOOT_RESOURCE', { data }) + + return state.newShootResource + }, + resetNewShootResource ({ commit, rootState }) { + commit('RESET_NEW_SHOOT_RESOURCE') + + return state.newShootResource } } @@ -532,6 +555,100 @@ const mutations = { const { filter, value } = filterValue state.shootListFilters[filter] = value setFilteredAndSortedItems(state, rootState) + }, + SET_NEW_SHOOT_RESOURCE (state, { data }) { + state.newShootResource = data + }, + RESET_NEW_SHOOT_RESOURCE (state) { + const shootResource = { + apiVersion: 'garden.sapcloud.io/v1beta1', + kind: 'Shoot' + } + + const infrastructureKind = head(store.getters.sortedCloudProviderKindList) + set(shootResource, ['spec', 'cloud', infrastructureKind], getCloudProviderTemplate(infrastructureKind)) + + const cloudProfileName = get(head(store.getters.cloudProfilesByCloudProviderKind(infrastructureKind)), 'metadata.name') + set(shootResource, 'spec.cloud.profile', cloudProfileName) + + const secret = head(store.getters.infrastructureSecretsByCloudProfileName(cloudProfileName)) + const secretBindingRef = { + name: get(secret, 'metadata.bindingName') + } + set(shootResource, 'spec.cloud.secretBindingRef', secretBindingRef) + + const region = head(store.getters.regionsWithSeedByCloudProfileName(cloudProfileName)) + set(shootResource, 'spec.cloud.region', region) + + const zones = [sample(store.getters.zonesByCloudProfileNameAndRegion({ cloudProfileName, region }))] + if (!isEmpty(zones)) { + set(shootResource, ['spec', 'cloud', infrastructureKind, 'zones'], zones) + } + + const loadBalancerProviderName = head(store.getters.loadBalancerProviderNamesByCloudProfileName(cloudProfileName)) + if (!isEmpty(loadBalancerProviderName)) { + set(shootResource, ['spec', 'cloud', infrastructureKind, 'loadBalancerProvider'], loadBalancerProviderName) + } + const floatingPoolName = head(store.getters.floatingPoolNamesByCloudProfileName(cloudProfileName)) + if (!isEmpty(floatingPoolName)) { + set(shootResource, ['spec', 'cloud', infrastructureKind, 'floatingPoolName'], floatingPoolName) + } + + const name = shortRandomString(10) + set(shootResource, 'metadata.name', name) + + const purpose = head(purposesForSecret(secret)) + set(shootResource, 'metadata.annotations["garden.sapcloud.io/purpose"]', purpose) + + const kubernetesVersion = head(store.getters.sortedKubernetesVersions(cloudProfileName)) + set(shootResource, 'spec.kubernetes.version', kubernetesVersion) + + const workerName = `worker-${shortRandomString(5)}` + const volumeType = get(head(store.getters.volumeTypesByCloudProfileNameAndZones({ cloudProfileName, zones })), 'name') + const volumeSize = volumeType ? '50Gi' : undefined + const machineType = get(head(store.getters.machineTypesByCloudProfileNameAndZones({ cloudProfileName, zones })), 'name') + const machineImage = store.getters.defaultMachineImageForCloudProfileName(cloudProfileName) + const workers = [ + { + name: workerName, + machineType, + volumeType, + volumeSize, + autoScalerMin: 1, + autoScalerMax: 2, + maxSurge: 1, + machineImage + } + ] + set(shootResource, ['spec', 'cloud', infrastructureKind, 'workers'], workers) + + const addons = {} + forEach(filter(shootAddonList, addon => addon.visible), addon => { + set(addons, [addon.name, 'enabled'], addon.enabled) + }) + set(shootResource, 'spec.addons', addons) + + const { utcBegin, utcEnd } = utcMaintenanceWindowFromLocalBegin({ localBegin: randomLocalMaintenanceBegin(), timezone: store.state.localTimezone }) + const maintenance = { + timeWindow: { + begin: utcBegin, + end: utcEnd + }, + autoUpdate: { + kubernetesVersion: true, + machineImageVersion: true + } + } + set(shootResource, 'spec.maintenance', maintenance) + + let hibernationSchedule = get(store.state.cfg.defaultHibernationSchedule, purpose) + hibernationSchedule = map(hibernationSchedule, schedule => { + schedule.location = store.state.localTimezone + return schedule + }) + set(shootResource, 'spec.hibernation.schedule', hibernationSchedule) + + state.newShootResource = shootResource } } diff --git a/frontend/src/stylus/main.styl b/frontend/src/stylus/main.styl index 11e5554a82..fd2b74ff88 100644 --- a/frontend/src/stylus/main.styl +++ b/frontend/src/stylus/main.styl @@ -15,7 +15,7 @@ */ /** Stylus Styles */ -@import '../../node_modules/vuetify/src/stylus/settings/_colors.styl' +@import '~vuetify/src/stylus/settings/_colors.styl' $theme = { primary: $green.darken-2 accent: $green.accent-2 @@ -25,7 +25,7 @@ $theme = { error: $red.base success: $green.accent-4 } -@import '../../node_modules/vuetify/src/stylus/main' +@import '~vuetify/src/stylus/main' /** Fonts */ $font-roboto = Roboto, 'Noto Sans', Noto, sans-serif diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 2df479afe9..eb11e7a612 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -107,13 +107,14 @@ export function updateShootVersion ({ namespace, name, data }) { return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/kubernetes/version`, data) } -export function updateMaintenance ({ namespace, name, data }) { +export function updateShootMaintenance ({ namespace, name, data }) { + console.log(namespace, name, data) namespace = encodeURIComponent(namespace) name = encodeURIComponent(name) return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/maintenance`, data) } -export function updateHibernationSchedules ({ namespace, name, data }) { +export function updateShootHibernationSchedules ({ namespace, name, data }) { namespace = encodeURIComponent(namespace) name = encodeURIComponent(name) return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/hibernation/schedules`, data) @@ -125,13 +126,19 @@ export function updateShootHibernation ({ namespace, name, data }) { return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/hibernation/enabled`, data) } -export function updateWorkers ({ namespace, name, infrastructureKind, data }) { +export function updateShootWorkers ({ namespace, name, infrastructureKind, data }) { namespace = encodeURIComponent(namespace) name = encodeURIComponent(name) infrastructureKind = encodeURIComponent(infrastructureKind) return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/cloud/${infrastructureKind}/workers`, data) } +export function updateShootAddons ({ namespace, name, data }) { + namespace = encodeURIComponent(namespace) + name = encodeURIComponent(name) + return updateResource(`/api/namespaces/${namespace}/shoots/${name}/spec/addons`, data) +} + /* Cloud Profiles */ export function getCloudprofiles () { diff --git a/frontend/src/utils/createShoot.js b/frontend/src/utils/createShoot.js new file mode 100644 index 0000000000..519ad5815b --- /dev/null +++ b/frontend/src/utils/createShoot.js @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019 by SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export function getCloudProviderTemplate (infrastructureKind) { + switch (infrastructureKind) { + case 'aws': + return { + networks: { + vpc: { + cidr: '10.250.0.0/16' + }, + internal: [ + '10.250.112.0/22' + ], + nodes: '10.250.0.0/16', + public: [ + '10.250.96.0/22' + ], + workers: [ + '10.250.0.0/19' + ] + } + } + case 'azure': + return { + networks: { + vnet: { + cidr: '10.250.0.0/16' + }, + nodes: '10.250.0.0/19', + public: '10.250.96.0/22', + workers: '10.250.0.0/19' + } + } + case 'gcp': + return { + networks: { + nodes: '10.250.0.0/19', + workers: [ + '10.250.0.0/19' + ] + } + } + case 'openstack': + return { + networks: { + nodes: '10.250.0.0/19', + workers: [ + '10.250.0.0/19' + ] + } + } + case 'alicloud': + return { + networks: { + vpc: { + cidr: '10.250.0.0/16' + }, + nodes: '10.250.0.0/16', + workers: [ + '10.250.0.0/19' + ] + } + } + } +} diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index e49e9782a3..ee87a0694c 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -39,6 +39,7 @@ import startsWith from 'lodash/startsWith' import split from 'lodash/split' import join from 'lodash/join' import last from 'lodash/last' +import sample from 'lodash/sample' import compact from 'lodash/compact' import store from '../store' @@ -384,13 +385,6 @@ export function isValidTerminationDate (expirationTimestamp) { return expirationTimestamp && new Date(expirationTimestamp) > new Date() } -export function isShootMarkedForDeletion (metadata) { - const confirmation = get(metadata, ['annotations', 'confirmation.garden.sapcloud.io/deletion'], false) - const deletionTimestamp = get(metadata, 'deletionTimestamp') - - return !!deletionTimestamp && !!confirmation -} - export function isTypeDelete (lastOperation) { return get(lastOperation, 'type') === 'Delete' } @@ -458,3 +452,71 @@ export function shortRandomString (length) { } return text } + +export function selfTerminationDaysForSecret (secret) { + const clusterLifetimeDays = function (quotas, scope) { + const predicate = item => get(item, 'spec.scope') === scope + return get(find(quotas, predicate), 'spec.clusterLifetimeDays') + } + + const quotas = get(secret, 'quotas') + let terminationDays = clusterLifetimeDays(quotas, 'project') + if (!terminationDays) { + terminationDays = clusterLifetimeDays(quotas, 'secret') + } + + return terminationDays +} + +export function purposesForSecret (secret) { + return selfTerminationDaysForSecret(secret) ? ['evaluation'] : ['evaluation', 'development', 'production'] +} + +export const shootAddonList = [ + { + name: 'kubernetes-dashboard', + title: 'Dashboard', + description: 'General-purpose web UI for Kubernetes clusters. Several high-profile attacks have shown weaknesses, so installation is not recommend, especially not for production clusters.', + visible: true, + enabled: false + }, + { + name: 'monocular', + title: 'Monocular', + description: 'Monocular is a web-based UI for managing Kubernetes applications and services packaged as Helm Charts. It allows you to search and discover available charts from multiple repositories, and install them in your cluster with one click.' + }, + { + name: 'nginx-ingress', + title: 'Nginx Ingress', + description: 'Default ingress-controller. Alternatively you may install any other ingress-controller of your liking. If you select this option, please note that Gardener will include it in its reconciliation and you can’t override it’s configuration.', + visible: true, + enabled: true + } +] + +export function randomLocalMaintenanceBegin () { + // randomize maintenance time window + const hours = ['22', '23', '00', '01', '02', '03', '04', '05'] + const randomHour = sample(hours) + // use local timezone offset + const localBegin = `${randomHour}:00` + + return localBegin +} + +export function utcMaintenanceWindowFromLocalBegin ({ localBegin, timezone }) { + timezone = timezone || store.state.localTimezone + if (localBegin) { + const utcMoment = moment.tz(localBegin, 'HH:mm', timezone).utc() + + let utcBegin + let utcEnd + if (utcMoment && utcMoment.isValid()) { + utcBegin = utcMoment.format('HHmm00+0000') + utcMoment.add(1, 'h') + utcEnd = utcMoment.format('HHmm00+0000') + } + return { utcBegin, utcEnd } + } + return undefined +} diff --git a/frontend/src/utils/validators.js b/frontend/src/utils/validators.js index 52e0e822e7..71ce5938bb 100644 --- a/frontend/src/utils/validators.js +++ b/frontend/src/utils/validators.js @@ -26,6 +26,7 @@ const alphaNumUnderscoreHyphenPattern = /^[a-zA-Z0-9-_]+$/ const resourceNamePattern = /^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/ const consecutiveHyphenPattern = /.?-{2,}.?/ const startEndHyphenPattern = /^-.*.|.*-$/ +const numberOrPercentagePattern = /^[\d]+[%]?$/ const base64 = regex('base64', base64Pattern) const uppercaseAlphaNum = regex('uppercaseAlphaNum', uppercaseAlphaNumPattern) @@ -38,6 +39,9 @@ const noConsecutiveHyphen = (value) => { const noStartEndHyphen = (value) => { return !startEndHyphenPattern.test(value) } +const numberOrPercentage = (value) => { + return numberOrPercentagePattern.test(value) +} const unique = key => withParams({ type: 'unique', key }, function (value, parentVm) { @@ -86,5 +90,6 @@ export { noStartEndHyphen, serviceAccountKey, minVolumeSize, - uniqueWorkerName + uniqueWorkerName, + numberOrPercentage }