Skip to content

Commit

Permalink
Merge branch 'master' of git://github.com/gardener/dashboard
Browse files Browse the repository at this point in the history
* 'master' of git://github.com/gardener/dashboard:
  #92 fix transparent toolbar in ShootEditor
  frontend: updated marked to 0.4.0 and oidc-client to 1.5.1
  Now using SelfSubjectAccessReview for isAdmin check. Instead of reading the cluster role binding garden-administrators and checking if the user is included in the subjects, we now make use of the SelfSubjectAccessReview. -> If someone is allowed to delete shoots in all namespaces he is considered to be an administrator
  Preparation for #42: Include quota information when fetching infrastructure secrets
  Prepare next dev cycle 1.15.0-dev
  Release 1.14.0
  Update frontend & backend to version 1.14.0 [skip ci]
  Popover bottom placement On the shoot details page the Popover for the Status and Readiness chips were behind the tabs bar. Had to place the popover at the bottom of the chips (poor man's solution).
  Shoot Details page / yaml not refreshing #106 - Refactored Emitter.js - Now we are subscribing a single shoot when accessing the shoot details page - fixed also a bug with journals: The comments would not be shown when the websocket (browser) has no connection to the dashboard backend and meanwhile an already closed journal is reopened and afterwards the websocket connection is established again. It would only show the issue description.
  [fix] Shoot details not cleared when shoot is deleted
  Show Shoot status and readiness on detail page #90 switched Monitoring card with Kube-Cluster Access card
  now using `confirmation.garden.sapcloud.io/deletion=true` annotation when deleting shoots - fixed unit test
  Restart dex pod when configmap changes
  now using `confirmation.garden.sapcloud.io/deletion=true` annotation when deleting shoots, but still having the old behavior for backwards compatibility see also gardener/gardener@cce0385#diff-63756e0de99297a92eabe6585558346b
  fixed/improved backend error handling in middleware
  Prometheus metrics
  Show Shoot status and readiness on detail page #90
  fixed #104 Shoots list and Journal comments not refreshing after recovering from lost websocket connection

Resolved Conflicts:
*	backend/test/support/nocks/k8s.js
*	frontend/package-lock.json
  • Loading branch information
holgerkoser committed Jun 26, 2018
2 parents 70fd05c + 6e66fea commit 21bc9df
Show file tree
Hide file tree
Showing 45 changed files with 1,113 additions and 426 deletions.
4 changes: 4 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ Copyright (c) JS Foundation and other contributors
[oidc-client](https://github.com/IdentityModel/oidc-client-js)
[Apache License 2.0](https://github.com/IdentityModel/oidc-client-js/blob/dev/LICENSE)

[prom-client](https://github.com/siimon/prom-client)
Copyright (c) 2015 Simon Nyberg
[Apache License 2.0](https://github.com/siimon/prom-client/blob/master/LICENSE)

[semver-sort](https://github.com/ragingwind/semver-sort)
Copyright (c) ragingwind <ragingwind@gmail.com> (ragingwind.me)
[MIT License](https://github.com/ragingwind/semver-sort/blob/master/license)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.14.0-dev
1.15.0-dev
12 changes: 10 additions & 2 deletions backend/lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ const config = require('./config')
const { parse: parseUrl } = require('url')
const { resolve, join } = require('path')
const logger = require('./logger')
const { notFound, renderError, historyFallback } = require('./middleware')
const { notFound, renderError, historyFallback, prometheusMetrics } = require('./middleware')
const helmet = require('helmet')
const api = require('./api')
const githubWebhook = require('./github/webhook')
const port = config.port
const jwt = require('express-jwt')

// resolve pathnames
const PUBLIC_DIRNAME = resolve(join(__dirname, '..', 'public'))
Expand Down Expand Up @@ -56,7 +57,14 @@ app.use(helmet.hsts())

app.use('/api', api.router)
app.use('/webhook', githubWebhook.router)
app.use('/config.json', api.frontendConfig)
app.get('/config.json', api.frontendConfig)

if (_.has(config, 'prometheus.secret')) {
app.get('/metrics',
jwt({ secret: config.prometheus.secret }),
prometheusMetrics()
)
}

app.use(helmet.xssFilter())
app.use(helmet.contentSecurityPolicy({
Expand Down
2 changes: 1 addition & 1 deletion backend/lib/github/webhook/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ router.route('/')

switch (event) {
case 'issues':
webhookService.processIssue({action, issue})
await webhookService.processIssue({action, issue})
break
case 'issue_comment':
const comment = _.get(body, 'comment')
Expand Down
24 changes: 22 additions & 2 deletions backend/lib/github/webhook/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,38 @@
const _ = require('lodash')
const { journals } = require('../../services')
const { getJournalCache } = require('../../cache')
const logger = require('../../logger')

exports.processIssue = function ({action, issue}) {
exports.processIssue = async function ({action, issue}) {
issue = journals.fromIssue(issue)

if (action === 'closed') {
getJournalCache().removeIssue({issue})
} else {
getJournalCache().addOrUpdateIssue({issue})

const hasComments = _.get(issue, 'data.comments', 0) > 0
if (action === 'reopened' && hasComments) {
const issueNumber = _.get(issue, 'metadata.number')
const batchFn = comments => {
logger.debug('fetched %s comments (batch) for issue %s', comments.length, issueNumber)
_.forEach(comments, comment => getJournalCache().addOrUpdateComment({issueNumber, comment}))
}
try {
await journals.commentsForIssueNumber({
issueNumber,
name: _.get(issue, 'metadata.name'),
namespace: _.get(issue, 'metadata.namespace'),
batchFn
})
} catch (error) {
logger.error('failed to fetch comments for reopened issue %s: %s', issueNumber, error)
}
}
}
}

const processComment = function ({action, issue, comment}) {
const processComment = ({action, issue, comment}) => {
issue = journals.fromIssue(issue)
const issueNumber = _.get(issue, 'metadata.number')
const name = _.get(issue, 'metadata.name')
Expand Down
46 changes: 39 additions & 7 deletions backend/lib/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,15 @@ module.exports = () => {
const shootsNsp = io.of('/shoots')
shootsNsp.on('connection', socket => {
logger.debug('Socket %s connected', socket.id)
socket.on('disconnect', onDisconnect)
socket.on('subscribe', async ({namespaces}) => {

const leaveShootsAndShootRoom = (socket) => {
const filterFn = key => key !== socket.id
leavePreviousRooms(socket, filterFn)
}

socket.on('disconnect', onDisconnect)
socket.on('subscribeShoots', async ({namespaces}) => {
leaveShootsAndShootRoom(socket)

/* join current rooms */
if (_.isArray(namespaces)) {
Expand All @@ -126,7 +131,7 @@ module.exports = () => {
const predicate = item => item.metadata.namespace === namespace
const project = _.find(projectList, predicate)
if (project) {
const room = filter ? `${namespace}_${filter}` : namespace
const room = filter ? `shoots_${namespace}_${filter}` : `shoots_${namespace}`
joinRoom(socket, room)

shootsPromises.push(new Promise(async (resolve, reject) => {
Expand All @@ -153,14 +158,41 @@ module.exports = () => {
socket.emit('batchNamespacedEventsDone', {kind, namespaces: _.map(namespaces, nsObj => nsObj.namespace)})
}
})
socket.on('subscribeShoot', async ({name, namespace}) => {
leaveShootsAndShootRoom(socket)

const kind = 'shoot'
const user = getUserFromSocket(socket)
const batchEmitter = new NamespacedBatchEmitter({kind, socket, objectKeyPath: 'metadata.uid'})
try {
const projectList = await projects.list({user})

const predicate = item => item.metadata.namespace === namespace
const project = _.find(projectList, predicate)
if (project) {
const room = `shoot_${namespace}_${name}`
joinRoom(socket, room)

const shoot = await shoots.read({user, name, namespace})
batchEmitter.batchEmitObjects([shoot], namespace)
}
} catch (error) {
logger.error('Socket %s: failed to subscribe to shoot: (%s) %s', _.get(socket, 'id'), error.code, error)
socket.emit('subscription_error', {kind, code: error.code, message: 'Failed to fetch shoot'})
}
batchEmitter.flush()

socket.emit('shootSubscriptionDone', {kind, target: {name, namespace}})
})
})
const journalsNsp = io.of('/journals')
const leaveCommentRooms = (socket) => {
const filterFn = key => key !== socket.id && key !== 'issues'
leavePreviousRooms(socket, filterFn)
}
journalsNsp.on('connection', socket => {
logger.debug('Socket %s connected', socket.id)

const leaveCommentRooms = (socket) => {
const filterFn = key => key !== socket.id && key !== 'issues'
leavePreviousRooms(socket, filterFn)
}
socket.on('disconnect', onDisconnect)
socket.on('subscribeIssues', async () => {
const filterFn = key => key !== socket.id && !_.startsWith(key, 'comments_')
Expand Down
5 changes: 5 additions & 0 deletions backend/lib/kubernetes/Resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,10 @@ module.exports = {
name: 'secretbindings',
kind: 'SecretBinding',
apiVersion: 'garden.sapcloud.io/v1beta1'
},
Quota: {
name: 'quotas',
kind: 'Quotas',
apiVersion: 'garden.sapcloud.io/v1beta1'
}
}
19 changes: 18 additions & 1 deletion backend/lib/kubernetes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const BaseObject = require('kubernetes-client/lib/base')
BaseObject.prototype.watch = require('./watch')
BaseObject.prototype.mergePatch = mergePatch
BaseObject.prototype.jsonPatch = jsonPatch
const ApiGroup = require('kubernetes-client/lib/api-group')
const kubernetesClient = require('kubernetes-client')
const yaml = require('js-yaml')
const Resources = require('./Resources')
Expand Down Expand Up @@ -128,9 +129,25 @@ module.exports = {
Resources.Shoot.name,
Resources.Seed.name,
Resources.CloudProfile.name,
Resources.SecretBinding.name
Resources.SecretBinding.name,
Resources.Quota.name
]
})
return new CustomResourceDefinitions(credentials(options))
},
authorization (options) {
options = assign(options, {
path: 'apis/authorization.k8s.io',
version: 'v1',
namespaceResources: [
'localsubjectaccessreviews'
],
groupResources: [
'selfsubjectaccessreviews',
'selfsubjectrulesreviews',
'subjectaccessreviews'
]
})
return new ApiGroup(credentials(options))
}
}
26 changes: 20 additions & 6 deletions backend/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@ const jwks = require('jwks-rsa')
const { JwksError } = jwks
const config = require('./config')
const logger = require('./logger')
const { NotFound, Unauthorized } = require('./errors')

const { NotFound, Unauthorized, InternalServerError } = require('./errors')
const client = require('prom-client')
const secretProvider = jwtSecret(config.jwks)

function prometheusMetrics ({timeout = 30000} = {}) {
client.collectDefaultMetrics({ timeout })

return (req, res, next) => {
res.set('Content-Type', client.register.contentType)
res.end(client.register.metrics())
}
}

function frontendConfig (req, res, next) {
res.json(config.frontend)
}
Expand Down Expand Up @@ -93,11 +102,11 @@ function jwtSecret (options) {
function historyFallback (filename) {
return (req, res, next) => {
if (!_.includes(['GET', 'HEAD'], req.method) || !req.accepts('html')) {
next()
return next()
}
res.sendFile(filename, err => {
if (err) {
next(err)
next(new InternalServerError(err.message))
}
})
}
Expand Down Expand Up @@ -130,7 +139,11 @@ function sendError (err, req, res, next) {

function renderError (err, req, res, next) {
const locals = errorToLocals(err, req)
res.status(locals.status).send(ErrorTemplate(locals))

res.format({
json: () => res.status(locals.status).send(locals),
default: () => res.status(locals.status).send(ErrorTemplate(locals))
})
}

const ErrorTemplate = _.template(`<!doctype html>
Expand Down Expand Up @@ -168,5 +181,6 @@ module.exports = {
notFound,
sendError,
renderError,
ErrorTemplate
ErrorTemplate,
prometheusMetrics
}
2 changes: 1 addition & 1 deletion backend/lib/routes/shoots.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ router.route('/:name/metadata/annotations')
const namespace = req.params.namespace
const name = req.params.name
const annotations = req.body
res.send(await shoots.patchAnnotation({user, namespace, name, annotations}))
res.send(await shoots.patchAnnotations({user, namespace, name, annotations}))
} catch (err) {
next(err)
}
Expand Down
56 changes: 16 additions & 40 deletions backend/lib/services/administrators.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,28 @@

const _ = require('lodash')
const kubernetes = require('../kubernetes')
const Resources = kubernetes.Resources
const rbac = kubernetes.rbac()

const ClusterRoleBindingName = 'garden-administrators'
const ClusterRoleResource = Resources.ClusterRole
const EmptyClusterRoleBinding = {
metadata: {
name: ClusterRoleBindingName
},
roleRef: {
apiGroup: ClusterRoleResource.apiGroup,
kind: ClusterRoleResource.kind,
name: 'garden.sapcloud.io:system:project-member'
},
subjects: []
function Authorization ({auth}) {
return kubernetes.authorization({auth})
}

function readClusterRoleBinding () {
return rbac.clusterrolebindings(ClusterRoleBindingName).get()
.catch(err => {
if (err.code === 404) {
return EmptyClusterRoleBinding
}
throw err
})
}

function fromResource ({subjects} = {}) {
return _
.chain(subjects)
.filter(['kind', 'User'])
.map('name')
.value()
}

const list = async function () {
const clusterRoleBinding = await readClusterRoleBinding()
return fromResource(clusterRoleBinding)
}
exports.list = list

exports.isAdmin = async function (user) {
if (!user) {
return false
}
const admins = await list()
const isAdmin = _.includes(admins, user.id)
// if someone is allowed to delete shoots in all namespaces he is considered to be an administrator
const body = {
kind: 'SelfSubjectAccessReview',
apiVersion: 'authorization.k8s.io/v1',
spec: {
resourceAttributes: {
verb: 'delete',
group: 'garden.sapcloud.io',
resource: 'shoots'
}
}
}
const response = await Authorization(user).selfsubjectaccessreviews.post({ body })
const isAdmin = _.get(response, 'status.allowed', false)
return isAdmin
}

0 comments on commit 21bc9df

Please sign in to comment.