Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger control plane migration #1262

Merged
merged 11 commits into from
Aug 15, 2022
19 changes: 19 additions & 0 deletions backend/__fixtures__/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const matchOptions = { decode: decodeURIComponent }
const matchList = pathToRegexp.match('/apis/core.gardener.cloud/v1beta1/namespaces/:namespace/shoots', matchOptions)
const matchItem = pathToRegexp.match('/apis/core.gardener.cloud/v1beta1/namespaces/:namespace/shoots/:name', matchOptions)
const matchAdminKubeconfig = pathToRegexp.match('/apis/core.gardener.cloud/v1beta1/namespaces/:namespace/shoots/:name/adminkubeconfig', matchOptions)
const matchBinding = pathToRegexp.match('/apis/core.gardener.cloud/v1beta1/namespaces/:namespace/shoots/:name/binding', matchOptions)

const mocks = {
list () {
Expand Down Expand Up @@ -255,6 +256,24 @@ const mocks = {
const { params: { namespace, name } = {} } = matchResult
const item = shoots.get(namespace, name)
set(item, 'metadata.annotations["confirmation.gardener.cloud/deletion"]', 'true')
return Promise.resolve(item)
}
},
patchBinding () {
return (headers, json) => {
const matchResult = matchBinding(headers[':path'])
if (matchResult === false) {
return Promise.reject(createError(503))
}
const { params: { namespace, name } = {} } = matchResult
const item = shoots.get(namespace, name)
if (!item) {
return Promise.reject(createError(404))
}

const { seedName } = json.spec
item.spec.seedName = seedName

return Promise.resolve(item)
}
}
Expand Down
13 changes: 13 additions & 0 deletions backend/lib/routes/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,16 @@ router.route('/:name/spec/purpose')
next(err)
}
})

router.route('/:name/spec/seedName')
.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.replaceSeedName({ user, namespace, name, body }))
} catch (err) {
next(err)
}
})
11 changes: 11 additions & 0 deletions backend/lib/services/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ exports.replacePurpose = async function ({ user, namespace, name, body }) {
return client['core.gardener.cloud'].shoots.mergePatch(namespace, name, payload)
}

exports.replaceSeedName = async function ({ user, namespace, name, body }) {
const client = user.client
const seedName = body.seedName
const payload = {
spec: {
seedName
}
}
return client['core.gardener.cloud'].shoots.mergePatch(namespace, [name, 'binding'], payload)
}

exports.replaceAddons = async function ({ user, namespace, name, body }) {
const client = user.client
const addons = body
Expand Down
84 changes: 68 additions & 16 deletions backend/test/acceptance/__snapshots__/api.shoots.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,58 @@ Object {
}
`;

exports[`api shoots should replace shoot seedname 1`] = `
Array [
Array [
Object {
":authority": "kubernetes:6443",
":method": "patch",
":path": "/apis/core.gardener.cloud/v1beta1/namespaces/garden-foo/shoots/barShoot/binding",
":scheme": "https",
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImZvb0BleGFtcGxlLm9yZyIsImlhdCI6MTU3NzgzNjgwMCwiYXVkIjpbImdhcmRlbmVyIl0sImV4cCI6MzE1NTcxNjgwMCwianRpIjoianRpIn0.k3kGjF6AgugJLdwERXEWZPaibFAPFPOnmpT3YM9H0xU",
"content-type": "application/merge-patch+json",
},
Object {
"spec": Object {
"seedName": "foo-seed",
},
},
],
]
`;

exports[`api shoots should replace shoot seedname 2`] = `
Object {
"metadata": Object {
"annotations": Object {
"gardener.cloud/created-by": "[email protected]",
},
"name": "barShoot",
"namespace": "garden-foo",
"uid": 2,
},
"spec": Object {
"cloudProfileName": "infra1-profileName",
"hibernation": Object {
"enabled": false,
},
"kubernetes": Object {
"version": "1.16.0",
},
"provider": Object {
"type": "fooInfra",
},
"purpose": "barPurpose",
"region": "foo-west",
"secretBindingName": "foo-infra1",
"seedName": "foo-seed",
},
"status": Object {
"technicalID": "shoot--foo--barShoot",
},
}
`;

exports[`api shoots should replace shoot workers 1`] = `
Array [
Array [
Expand Down Expand Up @@ -981,7 +1033,7 @@ users:
}
`;

exports[`api shoots should return shoot seed info when no fallback is needed 1`] = `
exports[`api shoots should return shoot seed info when need to fallback to old monitoring secret 1`] = `
Array [
Array [
Object {
Expand Down Expand Up @@ -1030,17 +1082,25 @@ Array [
":scheme": "https",
},
],
Array [
Object {
":authority": "api.foo-east.infra1.seed.cluster",
":method": "get",
":path": "/api/v1/namespaces/shoot--foo--barShoot/secrets/monitoring-ingress-credentials",
":scheme": "https",
},
],
]
`;

exports[`api shoots should return shoot seed info when no fallback is needed 2`] = `
exports[`api shoots should return shoot seed info when need to fallback to old monitoring secret 2`] = `
Object {
"monitoringPassword": "pass-shoot--foo--barShoot-bar.monitoring",
"monitoringUsername": "user-shoot--foo--barShoot-bar.monitoring",
"monitoringPassword": "pass-shoot--foo--barShoot-monitoring-ingress-credentials",
"monitoringUsername": "user-shoot--foo--barShoot-monitoring-ingress-credentials",
}
`;

exports[`api shoots should return shoot seed info when need to fallback to old monitoring secret 1`] = `
exports[`api shoots should return shoot seed info when no fallback is needed 1`] = `
Array [
Array [
Object {
Expand Down Expand Up @@ -1089,21 +1149,13 @@ Array [
":scheme": "https",
},
],
Array [
Object {
":authority": "api.foo-east.infra1.seed.cluster",
":method": "get",
":path": "/api/v1/namespaces/shoot--foo--barShoot/secrets/monitoring-ingress-credentials",
":scheme": "https",
},
],
]
`;

exports[`api shoots should return shoot seed info when need to fallback to old monitoring secret 2`] = `
exports[`api shoots should return shoot seed info when no fallback is needed 2`] = `
Object {
"monitoringPassword": "pass-shoot--foo--barShoot-monitoring-ingress-credentials",
"monitoringUsername": "user-shoot--foo--barShoot-monitoring-ingress-credentials",
"monitoringPassword": "pass-shoot--foo--barShoot-bar.monitoring",
"monitoringUsername": "user-shoot--foo--barShoot-bar.monitoring",
}
`;

Expand Down
18 changes: 18 additions & 0 deletions backend/test/acceptance/api.shoots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,24 @@ describe('api', function () {
expect(res.body).toMatchSnapshot()
})

it('should replace shoot seedname', async function () {
mockRequest.mockImplementationOnce(fixtures.shoots.mocks.patchBinding())

const res = await agent
.put(`/api/namespaces/${namespace}/shoots/${name}/spec/seedname`)
.set('cookie', await user.cookie)
.send({
seedName: 'foo-seed'
})
.expect('content-type', /json/)
.expect(200)

expect(mockRequest).toBeCalledTimes(1)
expect(mockRequest.mock.calls).toMatchSnapshot()

expect(res.body).toMatchSnapshot()
})

it('should replace addons', async function () {
mockRequest.mockImplementationOnce(fixtures.shoots.mocks.patch())

Expand Down
96 changes: 96 additions & 0 deletions frontend/src/components/SeedConfiguration.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<!--
SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors

SPDX-License-Identifier: Apache-2.0
-->

<template>
<action-button-dialog
:shoot-item="shootItem"
:valid="seedClusterValid"
@dialog-opened="onConfigurationDialogOpened"
ref="actionDialog"
width="450"
caption="Configure Seed"
confirm-required>
<template v-slot:actionComponent>
<seed-select
v-model="seedName"
:seedClusters="seedClusters"
:valid.sync="seedClusterValid"
></seed-select>
</template>
</action-button-dialog>
</template>

<script>
import map from 'lodash/map'
import filter from 'lodash/filter'

import { mapGetters } from 'vuex'

import ActionButtonDialog from '@/components/dialogs/ActionButtonDialog'
import SeedSelect from '@/components/SeedSelect'

import { updateShootSeedName } from '@/utils/api'
import { errorDetailsFromError } from '@/utils/error'

import shootItem from '@/mixins/shootItem'

export default {
name: 'seed-configuration',
components: {
ActionButtonDialog,
SeedSelect
},
mixins: [
shootItem
],
data () {
return {
seedName: undefined,
seedClusterValid: false
}
},
computed: {
...mapGetters([
'seedList'
]),
seedClusters () {
const filteredSeeds = filter(this.seedList, { data: { type: this.shootCloudProviderKind } })
return map(filteredSeeds, seed => {
return seed.metadata.name
})
}
},
methods: {
async onConfigurationDialogOpened () {
await this.reset()
const confirmed = await this.$refs.actionDialog.waitForDialogClosed()
if (confirmed) {
await this.updateConfiguration()
}
},
async updateConfiguration () {
try {
await updateShootSeedName({
namespace: this.shootNamespace,
name: this.shootName,
data: {
seedName: this.seedName
}
})
} catch (err) {
const errorMessage = 'Could not update seedname'
const errorDetails = errorDetailsFromError(err)
const detailedErrorMessage = errorDetails.detailedMessage
this.$refs.actionDialog.setError({ errorMessage, detailedErrorMessage })
console.error(this.errorMessage, errorDetails.errorCode, errorDetails.detailedMessage, err)
}
},
async reset () {
this.seedName = this.shootSeedName
}
}
}
</script>
78 changes: 78 additions & 0 deletions frontend/src/components/SeedSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!--
SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors

SPDX-License-Identifier: Apache-2.0
-->

<template>
<v-select
hint="Change seed cluster for this shoot's control plane"
color="primary"
item-color="primary"
label="Seed Cluster"
:items="seedClusters"
v-model="seedCluster"
persistent-hint
:error-messages="getErrorMessages('seedCluster')"
@blur="$v.seedCluster.$touch()">
</v-select>
</template>

<script>
import { required } from 'vuelidate/lib/validators'
import { getValidationErrors } from '@/utils'

const validationErrors = {
seedCluster: {
required: 'Seed is required.'
}
}

export default {
name: 'seed-select',
props: {
seedClusters: {
type: Array
},
value: {
type: String
},
valid: {
type: Boolean
}
},
data () {
return {
validationErrors
}
},
validations: {
seedCluster: {
required
}
},
computed: {
seedCluster: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
},
methods: {
getErrorMessages (field) {
return getValidationErrors(this, field)
}
},
watch: {
'$v.seedCluster.$invalid' (value) {
this.$emit('update:valid', !value)
}
},
mounted () {
this.$v.seedCluster.$touch()
}
}
</script>
Loading