Skip to content

Commit

Permalink
Support gardenlogin kubeconfig (gardener#1377)
Browse files Browse the repository at this point in the history
* Support gardenlogin kubeconfig

* Add install hint for gardenctl-v2

* Tests: Add advertised addresses in shoot status

* logger.info mock should be reset after each test

* add tests for gardenlogin kubeconfig

* Improve texts and layout

* Increase max-width

* fix router link to Account page

* fix copyright year

* add clipboard property

* tolower

* add install hint

* PR feedback I

* move defaulting to function body

* PR feedback I

* PR feedback II
  • Loading branch information
petersutter authored Jan 24, 2023
1 parent 090d2c9 commit b508b9d
Show file tree
Hide file tree
Showing 16 changed files with 957 additions and 110 deletions.
109 changes: 109 additions & 0 deletions backend/__fixtures__/configmaps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

'use strict'

const { cloneDeep, find, filter, split, isEmpty } = require('lodash')
const createError = require('http-errors')
const pathToRegexp = require('path-to-regexp')
const { createUrl } = require('./helper')

function getConfigMap ({ namespace, name, labels, creationTimestamp, data = {} }) {
const metadata = {
namespace,
name
}
if (!isEmpty(labels)) {
metadata.labels = labels
}

if (creationTimestamp) {
metadata.creationTimestamp = creationTimestamp
}

return { metadata, data }
}

function getClusterIdentitConfigMap ({ identity = 'landscape-test' } = {}) {
return getConfigMap({
namespace: 'kube-system',
name: 'cluster-identity',
data: {
'cluster-identity': identity
}
})
}

const configMapsList = [
getClusterIdentitConfigMap()
]

const configMaps = {
get (namespace, name) {
const items = configMaps.list(namespace)
return find(items, ['metadata.name', name])
},
list (namespace) {
const items = cloneDeep(configMapsList)
return namespace
? filter(items, ['metadata.namespace', namespace])
: items
},
createClusterIdentityConfigMap (identity) {
return getClusterIdentitConfigMap({ identity })
}
}

const matchOptions = { decode: decodeURIComponent }
const matchList = pathToRegexp.match('/api/v1/namespaces/:namespace/configmaps', matchOptions)
const matchItem = pathToRegexp.match('/api/v1/namespaces/:namespace/configmaps/:name', matchOptions)

const mocks = {
list ({ forceEmpty = false } = {}) {
return headers => {
if (forceEmpty) {
return Promise.resolve({ items: [] })
}

const url = createUrl(headers)
const matchResult = matchList(url.pathname)
if (matchResult) {
const { params: { namespace } = {} } = matchResult
const items = configMaps.list(namespace)
return Promise.resolve({ items })
}

return Promise.reject(createError(503))
}
},
get (options) {
return headers => {
const matchResult = matchItem(headers[':path'])
if (matchResult === false) {
return Promise.reject(createError(503))
}
const { params: { namespace, name } = {} } = matchResult

if (namespace === 'kube-system' && name === 'cluster-identity') {
const [hostname] = split(headers[':authority'], ':')
const item = configMaps.createClusterIdentityConfigMap(hostname)
return Promise.resolve(item)
}

const item = configMaps.get(namespace, name)
if (item) {
return Promise.resolve(item)
}

return Promise.reject(createError(404))
}
}
}

module.exports = {
...configMaps,
mocks
}
2 changes: 2 additions & 0 deletions backend/__fixtures__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const kube = require('./kube')
const shoots = require('./shoots')
const seeds = require('./seeds')
const managedseeds = require('./managedseeds')
const configmaps = require('./configmaps')
const secrets = require('./secrets')
const secretbindings = require('./secretbindings')
const quotas = require('./quotas')
Expand Down Expand Up @@ -44,6 +45,7 @@ const fixtures = {
shoots,
seeds,
managedseeds,
configmaps,
secrets,
secretbindings,
projects,
Expand Down
12 changes: 10 additions & 2 deletions backend/__fixtures__/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ const shootList = [
createdBy: '[email protected]',
purpose: 'fooPurpose',
secretBindingName: 'barSecretName',
seed: 'infra4-seed-without-secretRef'
seed: 'infra4-seed-without-secretRef',
advertisedAddresses: null
}),
getShoot({
uid: 4,
Expand All @@ -68,7 +69,8 @@ function getShoot ({
secretBindingName = 'foo-secret',
seed = 'infra1-seed',
hibernation = { enabled: false },
kubernetesVersion = '1.16.0'
kubernetesVersion = '1.16.0',
advertisedAddresses
}) {
uid = uid || `${namespace}--${name}`
const shoot = {
Expand Down Expand Up @@ -100,6 +102,12 @@ function getShoot ({
if (project) {
shoot.status.technicalID = `shoot--${project}--${name}`
}
if (advertisedAddresses !== null) {
shoot.status.advertisedAddresses = advertisedAddresses ?? [{
name: 'external',
url: `https://api.${name}.${project}.shoot.test`
}]
}
return shoot
}

Expand Down
2 changes: 2 additions & 0 deletions backend/__mocks__/@gardener-dashboard/kube-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const mockLoadResult = new ClientConfig(Config.build({

module.exports = {
...originalKubeconfig,
Config,
ClientConfig,
cleanKubeconfig: jest.fn().mockImplementation(cleanKubeconfig),
load: jest.fn().mockReturnValue(mockLoadResult),
mockLoadResult
Expand Down
103 changes: 102 additions & 1 deletion backend/lib/services/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'use strict'

const { isHttpError } = require('@gardener-dashboard/request')
const { cleanKubeconfig } = require('@gardener-dashboard/kube-config')
const { cleanKubeconfig, Config } = require('@gardener-dashboard/kube-config')
const { dashboardClient } = require('@gardener-dashboard/kube-client')
const utils = require('../utils')
const cache = require('../cache')
Expand Down Expand Up @@ -284,6 +284,13 @@ exports.info = async function ({ user, namespace, name }) {
const data = {
canLinkToSeed: false
}

try {
data.kubeconfigGardenlogin = await getKubeconfigGardenlogin(client, shoot)
} catch (err) {
logger.info('failed to get gardenlogin kubeconfig', err.message)
}

if (shoot.spec.seedName) {
const seed = getSeed(getSeedNameFromShoot(shoot))
const prefix = _.replace(shoot.status.technicalID, /^shoot--/, '')
Expand Down Expand Up @@ -372,6 +379,100 @@ exports.seedInfo = async function ({ user, namespace, name }) {
return data
}

async function getKubeconfigGardenlogin (client, shoot) {
if (!shoot.status?.advertisedAddresses?.length) {
throw new Error('Shoot has no advertised addresses')
}

const { namespace, name } = shoot.metadata

const [
ca,
clusterIdentity
] = await Promise.all([
client.core.secrets.get(namespace, `${name}.ca-cluster`),
dashboardClient.core.configmaps.get('kube-system', 'cluster-identity')
])

const gardenClusterIdentity = clusterIdentity.data['cluster-identity']

const caData = ca.data?.['ca.crt']

const extensions = [{
name: 'client.authentication.k8s.io/exec',
extension: {
shootRef: { namespace, name },
gardenClusterIdentity
}
}]
const userName = `${namespace}--${name}`

const installHint = `Follow the instructions on
- https://github.com/gardener/gardenlogin#installation to install and
- https://github.com/gardener/gardenlogin#configure-gardenlogin to configure the gardenlogin credential plugin.
The following is a sample configuration for gardenlogin as well as gardenctl. Place the file under ~/.garden/gardenctl-v2.yaml.
---
gardens:
- identity: ${gardenClusterIdentity}
kubeconfig: "<path-to-garden-cluster-kubeconfig>"
...
Alternatively, you can run the following gardenctl command:
$ gardenctl config set-garden ${gardenClusterIdentity} --kubeconfig "<path-to-garden-cluster-kubeconfig>"
Note that the kubeconfig refers to the path of the garden cluster kubeconfig which you can download from the Account page.`

const cfg = {
clusters: [],
contexts: [],
users: [{
name: userName,
user: {
exec: {
apiVersion: 'client.authentication.k8s.io/v1beta1',
command: 'kubectl-gardenlogin',
args: [
'get-client-certificate'
],
provideClusterInfo: true,
interactiveMode: 'IfAvailable',
installHint
}
}
}]
}

for (const [i, address] of shoot.status.advertisedAddresses.entries()) {
const name = `${userName}-${address.name}`
if (i === 0) {
cfg['current-context'] = name
}

cfg.clusters.push({
name,
cluster: {
server: address.url,
'certificate-authority-data': caData,
extensions
}
})

cfg.contexts.push({
name,
context: {
cluster: name,
user: userName,
namespace: 'default'
}
})
}

return new Config(cfg).toYAML()
}

async function getSecret (client, { namespace, name }) {
try {
return await client.core.secrets.get(namespace, name)
Expand Down
Loading

0 comments on commit b508b9d

Please sign in to comment.