Skip to content

Commit

Permalink
Integrate Kyma into Dashboard (#537)
Browse files Browse the repository at this point in the history
* allow kyma config in helm chart

* kyma integration frontend changes

* read config for kyma title
allow to copy email and username

* fix chart

* explicit enable frontend features

* PR review feedback I

* update kyma addon description

* add try catch

* fixed merge errors

* fixed errors

* fixed manage addon layout

* PR feedback I

* PR feedback II

Co-authored-by: Peter Sutter <peter.sutter@sap.com>
  • Loading branch information
holgerkoser and petersutter committed Jan 14, 2020
1 parent d258f90 commit 4965d61
Show file tree
Hide file tree
Showing 22 changed files with 441 additions and 58 deletions.
4 changes: 2 additions & 2 deletions backend/.nycrc
@@ -1,8 +1,8 @@
{
"all": true,
"check-coverage": true,
"lines": 65,
"statements": 65,
"lines": 64,
"statements": 64,
"functions": 66,
"branches": 40,
"include": [
Expand Down
19 changes: 9 additions & 10 deletions backend/lib/kubernetes-client/Client.js
Expand Up @@ -17,7 +17,6 @@
'use strict'

const _ = require('lodash')
const assert = require('assert').strict
const { isIP } = require('net')
const { HTTPError } = require('got')
const { isHttpError, setAuthorization } = require('./util')
Expand Down Expand Up @@ -79,28 +78,24 @@ class Client {
}

async getKubeconfig ({ name, namespace }) {
const secret = await this.getSecret({ name, namespace, throwNotFound: false })
const secret = await this.getSecret({ name, namespace })
const kubeconfigBase64 = _.get(secret, 'data.kubeconfig')
if (kubeconfigBase64) {
return decodeBase64(kubeconfigBase64)
if (!kubeconfigBase64) {
throw createHttpError(404, 'No kubeconfig found in secret')
}
return decodeBase64(kubeconfigBase64)
}

async createKubeconfigClient (secretRef) {
const kubeconfig = await this.getKubeconfig(secretRef)
assert.ok(kubeconfig, 'kubeconfig does not exist (yet)')
return new this.constructor(fromKubeconfig(kubeconfig))
}

async getProjectByNamespace (namespace) {
const ns = await this.core.namespaces.get(namespace)
const name = _.get(ns, ['metadata', 'labels', 'project.garden.sapcloud.io/name'])
if (!name) {
const response = {
statusCode: 404,
statusMessage: `Namespace '${namespace}' is not related to a gardener project`
}
throw new HTTPError(response)
throw createHttpError(404, `Namespace '${namespace}' is not related to a gardener project`)
}
return this['core.gardener.cloud'].projects.get(name)
}
Expand All @@ -114,6 +109,10 @@ class Client {
}
}

function createHttpError (statusCode, statusMessage) {
return new HTTPError({ statusCode, statusMessage })
}

async function getResource (resource, { namespace, name, throwNotFound = true }) {
try {
return await resource.get(namespace, name)
Expand Down
11 changes: 11 additions & 0 deletions backend/lib/kubernetes-client/resources/Core.js
Expand Up @@ -61,6 +61,16 @@ class Pod extends mix(Core).with(NamespaceScoped, Readable, Observable, Writable
}
}

class ConfigMap extends mix(Core).with(NamespaceScoped, Readable) {
static get names () {
return {
plural: 'configmaps',
singular: 'configmap',
kind: 'ConfigMap'
}
}
}

class Secret extends mix(Core).with(NamespaceScoped, Readable, Observable, Writable) {
static get names () {
return {
Expand Down Expand Up @@ -96,6 +106,7 @@ module.exports = {
Namespace,
Node,
Pod,
ConfigMap,
Secret,
Service,
ServiceAccount
Expand Down
16 changes: 16 additions & 0 deletions backend/lib/routes/shoots.js
Expand Up @@ -17,6 +17,8 @@
'use strict'

const express = require('express')
const _ = require('lodash')
const config = require('../config')
const { shoots } = require('../services')

const router = module.exports = express.Router({
Expand Down Expand Up @@ -179,3 +181,17 @@ router.route('/:name/info')
next(err)
}
})

if (_.get(config, 'frontend.features.kymaEnabled', false)) {
router.route('/:name/kyma')
.get(async (req, res, next) => {
try {
const user = req.user
const namespace = req.params.namespace
const name = req.params.name
res.send(await shoots.kyma({ user, namespace, name }))
} catch (err) {
next(err)
}
})
}
57 changes: 53 additions & 4 deletions backend/lib/services/shoots.js
Expand Up @@ -16,6 +16,7 @@

'use strict'

const { HTTPError } = require('got')
const { isHttpError } = require('../kubernetes-client')
const kubeconfig = require('../kubernetes-config')
const utils = require('../utils')
Expand All @@ -41,13 +42,16 @@ exports.list = async function ({ user, namespace, shootsWithIssuesOnly = false }

exports.create = async function ({ user, namespace, body }) {
const client = user.client

const username = user.id
const finalizers = ['gardener']

const annotations = {
'garden.sapcloud.io/createdBy': username
}
body = _.merge({}, body, { metadata: { namespace, finalizers, annotations } })
if (_.get(body, 'spec.addons.kyma.enabled', false)) {
annotations['experimental.addons.shoot.gardener.cloud/kyma'] = 'enabled'
}
body = _.merge({}, body, { metadata: { namespace, annotations } })
_.unset(body, 'spec.addons.kyma')
return client['core.gardener.cloud'].shoots.create(namespace, body)
}

Expand Down Expand Up @@ -119,15 +123,60 @@ exports.replaceHibernationSchedules = async function ({ user, namespace, name, b

exports.replaceAddons = async function ({ user, namespace, name, body }) {
const client = user.client
const addons = body
const { kyma = {}, ...addons } = body
const payload = {
spec: {
addons
}
}
if (kyma.enabled) {
payload.metadata = {
annotations: {
'experimental.addons.shoot.gardener.cloud/kyma': 'enabled'
}
}
}
return client['core.gardener.cloud'].shoots.mergePatch(namespace, name, payload)
}

exports.kyma = async function ({ user, namespace, name }) {
const client = user.client
try {
const shootClient = await client.createKubeconfigClient({ namespace, name: `${name}.kubeconfig` })
const [{
data: {
'global.ingress.domainName': domain
}
}, {
data: {
email, password, username
}
}] = await Promise.all([
shootClient.core.configmaps.get('kyma-installer', 'net-global-overrides'),
shootClient.core.secrets.get('kyma-system', 'admin-user')
])
return {
url: `https://console.${domain}`,
email: decodeBase64(email),
username: decodeBase64(username),
password: decodeBase64(password)
}
} catch (err) {
logger.error('Failed to fetch kyma addon info', err)
const statusCode = 404
let statusMessage
if (isHttpError(err, 404)) {
statusMessage = 'Kubeconfig for cluster does not exist'
} else if (/^ECONNRE/.test(err.code)) {
statusMessage = 'Connection to cluster could not be estalished'
} else {
statusMessage = 'Kyma not correctly installed in cluster'
}
const response = { statusCode, statusMessage }
throw new HTTPError(response)
}
}

exports.replaceWorkers = async function ({ user, namespace, name, body }) {
const client = user.client
const workers = body
Expand Down
10 changes: 6 additions & 4 deletions backend/lib/services/terminals/index.js
Expand Up @@ -21,7 +21,6 @@ const assert = require('assert').strict
const hash = require('object-hash')

const { isHttpError } = require('../../kubernetes-client')
const { AssertionError } = assert

const {
decodeBase64,
Expand Down Expand Up @@ -397,8 +396,9 @@ async function getOrCreateTerminalSession ({ user, namespace, name, target, body
throw new Error('Hosting cluster or target cluster is hibernated')
}

const hostKubeconfig = await client.getKubeconfig(hostCluster.secretRef)
if (!hostKubeconfig) {
try {
await client.getKubeconfig(hostCluster.secretRef)
} catch (err) {
throw new Error('Host kubeconfig does not exist (yet)')
}

Expand Down Expand Up @@ -426,9 +426,11 @@ async function createHostClient (client, secretRef) {
try {
return await client.createKubeconfigClient(secretRef)
} catch (err) {
if (err instanceof AssertionError) {
if (isHttpError(err, 404)) {
throw new Error('Host kubeconfig does not exist (yet)')
}
const { namespace, name } = secretRef
logger.error(`Failed to create client from kubeconfig secret ${namespace}/${name}`, err)
throw err
}
}
Expand Down
3 changes: 1 addition & 2 deletions backend/test/acceptance/api.shoots.spec.js
Expand Up @@ -62,7 +62,6 @@ module.exports = function ({ agent, sandbox, k8s, auth }) {

it('should create a shoot', async function () {
const bearer = await user.bearer
const finalizers = ['gardener']
k8s.stub.createShoot({ bearer, namespace, name, spec, resourceVersion })
const res = await agent
.post(`/api/namespaces/${namespace}/shoots`)
Expand All @@ -77,7 +76,7 @@ module.exports = function ({ agent, sandbox, k8s, auth }) {

expect(res).to.have.status(200)
expect(res).to.be.json
expect(res.body.metadata).to.eql({ name, namespace, resourceVersion, annotations, finalizers })
expect(res.body.metadata).to.eql({ name, namespace, resourceVersion, annotations })
expect(res.body.spec).to.eql(spec)
})

Expand Down
9 changes: 6 additions & 3 deletions charts/gardener-dashboard/templates/configmap.yaml
Expand Up @@ -132,16 +132,19 @@ data:
{{- if .Values.frontendConfig.gitHubRepoUrl }}
gitHubRepoUrl: {{ .Values.frontendConfig.gitHubRepoUrl }}
{{- end }}
{{- if .Values.terminal }}
features:
terminalEnabled: true
{{- end }}
terminalEnabled: {{ .Values.frontendConfig.features.terminalEnabled | default false }}
kymaEnabled: {{ .Values.frontendConfig.features.kymaEnabled | default false }}
{{- if .Values.frontendConfig.terminal }}
{{- if .Values.frontendConfig.terminal.heartbeatIntervalSeconds }}
terminal:
heartbeatIntervalSeconds: {{ .Values.frontendConfig.terminal.heartbeatIntervalSeconds }}
{{- end }}
{{- end }}
{{- if .Values.frontendConfig.kyma }}
kyma:
{{ toYaml .Values.frontendConfig.kyma | trim | indent 8 }}
{{- end }}
{{- if .Values.frontendConfig.defaultHibernationSchedule }}
defaultHibernationSchedule:
{{ toYaml .Values.frontendConfig.defaultHibernationSchedule | trim | indent 8 }}
Expand Down
13 changes: 11 additions & 2 deletions charts/gardener-dashboard/values.yaml
Expand Up @@ -69,11 +69,20 @@ frontendConfig:
end: 00 08 * * 1,2,3,4,5
production: ~
seedCandidateDeterminationStrategy: SameRegion
# features:
# terminalEnabled: true
features:
terminalEnabled: false
kymaEnabled: false

# terminal:
# heartbeatIntervalSeconds: 60

# kyma:
# title: Kyma
# description: |
# Kyma is a platform for extending applications with serverless functions and microservices.
# It provides a selection of cloud-native projects glued together to simplify the creation and management of extensions.
# enabled: false

gitHub:
apiUrl: https://api.foo-github.com
org: dummyorg
Expand Down
19 changes: 17 additions & 2 deletions frontend/src/components/ShootAddons/AddonConfiguration.vue
Expand Up @@ -20,22 +20,27 @@ limitations under the License.
@dialogOpened="onConfigurationDialogOpened"
ref="actionDialog"
caption="Configure Add-ons"
maxWidth="900">
maxWidth="900"
max-height="60vh"
>
<template slot="actionComponent">
<manage-shoot-addons
ref="addons"
:isCreateMode="false"
></manage-shoot-addons>
</template>
</action-icon-dialog>
</template>

<script>
import { mapGetters } from 'vuex'
import ActionIconDialog from '@/dialogs/ActionIconDialog'
import ManageShootAddons from '@/components/ShootAddons/ManageAddons'
import { updateShootAddons } from '@/utils/api'
import { errorDetailsFromError } from '@/utils/error'
import { shootItem } from '@/mixins/shootItem'
import get from 'lodash/get'
import cloneDeep from 'lodash/cloneDeep'
export default {
name: 'addon-configuration',
Expand All @@ -49,6 +54,11 @@ export default {
}
},
mixins: [shootItem],
computed: {
...mapGetters([
'isKymaFeatureEnabled'
])
},
methods: {
async onConfigurationDialogOpened () {
this.reset()
Expand All @@ -70,7 +80,12 @@ export default {
}
},
reset () {
this.$refs.addons.updateAddons(get(this.shootItem, 'spec.addons', {}))
const addons = cloneDeep(get(this.shootItem, 'spec.addons', {}))
if (this.isKymaFeatureEnabled) {
const kymaEnabled = !!get(this.shootItem, 'metadata.annotations["experimental.addons.shoot.gardener.cloud/kyma"]')
addons['kyma'] = { enabled: kymaEnabled }
}
this.$refs.addons.updateAddons(addons)
}
}
}
Expand Down

0 comments on commit 4965d61

Please sign in to comment.