-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[kibana] create a secret with es token #1720
Changes from 5 commits
bacbce1
2b7524b
827ddb3
753facd
547a349
81ce165
ff4a1c6
43c08a1
e3ef0c1
5b812e5
86386f0
cb0d6bb
c08720a
47ebb41
84a955d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,18 +4,161 @@ kind: ConfigMap | |||||
metadata: | ||||||
name: {{ template "kibana.fullname" . }}-helm-scripts | ||||||
labels: {{ include "kibana.labels" . | nindent 4 }} | ||||||
annotations: | ||||||
"helm.sh/hook": pre-install,pre-upgrade,post-delete | ||||||
"helm.sh/hook-delete-policy": hook-succeeded | ||||||
{{- if .Values.annotations }} | ||||||
{{- range $key, $value := .Values.annotations }} | ||||||
{{ $key }}: {{ $value | quote }} | ||||||
{{- end }} | ||||||
{{- end }} | ||||||
data: | ||||||
kibana-entrypoint.sh: | | ||||||
#!/bin/bash | ||||||
set -euo pipefail | ||||||
manage-es-token.js: | | ||||||
const https = require('https'); | ||||||
const fs = require('fs'); | ||||||
|
||||||
echo "export ELASTICSEARCH_SERVICEACCOUNTTOKEN=$({{ template "kibana.home_dir" . }}/node/bin/node {{ template "kibana.home_dir" . }}/helm-scripts/parse-token.js {{ template "kibana.home_dir" . }}/config/tokens/{{ template "kibana.fullname" . }}.json)" > $HOME/.elasticsearch-serviceaccounttoken | ||||||
source $HOME/.elasticsearch-serviceaccounttoken | ||||||
// Elasticsearch API | ||||||
const esPath = '_security/service/elastic/kibana/credential/token/{{ template "kibana.fullname" . }}'; | ||||||
const esUrl = '{{ .Values.elasticsearchHosts }}' + '/' + esPath | ||||||
const esUsername = process.env.ELASTICSEARCH_USERNAME; | ||||||
const esPassword = process.env.ELASTICSEARCH_PASSWORD; | ||||||
const esAuth = esUsername + ':' + esPassword; | ||||||
pugnascotia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const esCaFile = process.env.ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES; | ||||||
const esCa = fs.readFileSync(esCaFile); | ||||||
|
||||||
# https://github.com/elastic/dockerfiles/blob/a405a4d692031b72cefcb8523bd464aa3221ec47/kibana/Dockerfile#L131 | ||||||
exec /bin/tini -- /usr/local/bin/kibana-docker "$@" | ||||||
// Kubernetes API | ||||||
const k8sHostname = process.env.KUBERNETES_SERVICE_HOST; | ||||||
const k8sPort = process.env.KUBERNETES_SERVICE_PORT_HTTPS; | ||||||
const k8sPostSecretPath = 'api/v1/namespaces/{{ .Release.Namespace }}/secrets'; | ||||||
const k8sDeleteSecretPath = 'api/v1/namespaces/{{ .Release.Namespace }}/secrets/{{ template "kibana.fullname" . }}-es-token'; | ||||||
const k8sPostSecretUrl = 'https://' + k8sHostname + ':' + k8sPort + '/' + k8sPostSecretPath; | ||||||
const k8sDeleteSecretUrl = 'https://' + k8sHostname + ':' + k8sPort + '/' + k8sDeleteSecretPath; | ||||||
jmlrt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const k8sBearer = fs.readFileSync('/run/secrets/kubernetes.io/serviceaccount/token'); | ||||||
const k8sCa = fs.readFileSync('/run/secrets/kubernetes.io/serviceaccount/ca.crt'); | ||||||
|
||||||
parse-token.js: | | ||||||
let dataFile = process.argv[2]; | ||||||
let dataContent = require(dataFile.toString()); | ||||||
console.log(dataContent.token.value); | ||||||
// Post Data | ||||||
const esTokenDeleteOptions = { | ||||||
method: 'DELETE', | ||||||
auth: esAuth, | ||||||
ca: esCa, | ||||||
}; | ||||||
const esTokenCreateOptions = { | ||||||
method: 'POST', | ||||||
auth: esAuth, | ||||||
ca: esCa, | ||||||
}; | ||||||
const secretCreateOptions = { | ||||||
method: 'POST', | ||||||
ca: k8sCa, | ||||||
headers: { | ||||||
'Authorization': 'Bearer ' + k8sBearer, | ||||||
'Accept': 'application/json', | ||||||
'Content-Type': 'application/json', | ||||||
} | ||||||
}; | ||||||
const secretDeleteOptions = { | ||||||
method: 'DELETE', | ||||||
ca: k8sCa, | ||||||
headers: { | ||||||
'Authorization': 'Bearer ' + k8sBearer, | ||||||
'Accept': 'application/json', | ||||||
'Content-Type': 'application/json', | ||||||
} | ||||||
}; | ||||||
|
||||||
// With thanks to https://stackoverflow.com/questions/57332374/how-to-chain-http-request | ||||||
function requestPromise(url, options, payload) { | ||||||
return new Promise((resolve, reject) => { | ||||||
const request = https.request(url, options, response => { | ||||||
|
||||||
console.log('statusCode:', response.statusCode); | ||||||
pugnascotia marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
// TODO: remove 404 and handle it during esToken deletion | ||||||
const isSuccess = response.statusCode >= 200 && response.statusCode < 300 || response.statusCode == 404; | ||||||
|
||||||
let data = ''; | ||||||
response.on('data', chunk => data += chunk); // accumulate data | ||||||
response.once('end', () => isSuccess ? resolve(data) : reject(data)); // resolve promise here | ||||||
}); | ||||||
|
||||||
request.once('error', err => { | ||||||
// This won't log anything for e.g. an HTTP 404 or 500 response, | ||||||
// since from HTTP's point-of-view we successfully received a | ||||||
// response. | ||||||
console.log(`${options.method} ${options.path} failed: `, err.message || err); | ||||||
reject(err); // if promise is not already resolved, then we can reject it here | ||||||
}); | ||||||
|
||||||
if (payload) { | ||||||
request.write(payload); | ||||||
} | ||||||
request.end(); | ||||||
}); | ||||||
} | ||||||
|
||||||
function createEsToken() { | ||||||
// Chaining requests | ||||||
console.log('Cleaning previous token'); | ||||||
requestPromise(esUrl, esTokenDeleteOptions).then(() => { | ||||||
console.log('Creating new token'); | ||||||
requestPromise(esUrl, esTokenCreateOptions).then(response => { | ||||||
const body = JSON.parse(response); | ||||||
const token = body.token.value | ||||||
|
||||||
// Encode the token in base64 | ||||||
const base64Token = Buffer.from(token, 'utf8').toString('base64'); | ||||||
|
||||||
// Prepare the k8s secret | ||||||
secretData = JSON.stringify({ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops I missed the merge, didn't refresh in time. Sorry about the confusion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I'll follow up with a PR) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
"apiVersion": "v1", | ||||||
"kind": "Secret", | ||||||
"metadata": { | ||||||
"namespace": "{{ .Release.Namespace }}", | ||||||
"name": "{{ template "kibana.fullname" . }}-es-token", | ||||||
}, | ||||||
"type": "Opaque", | ||||||
"data": { | ||||||
"token": base64Token, | ||||||
} | ||||||
}) | ||||||
|
||||||
// Create the k8s secret | ||||||
console.log('Creating K8S secret'); | ||||||
requestPromise(k8sPostSecretUrl, secretCreateOptions, secretData).then().catch(err => { | ||||||
console.error(err) | ||||||
}); | ||||||
return; | ||||||
}) | ||||||
}).catch(err => { | ||||||
console.error(err); | ||||||
}); | ||||||
} | ||||||
|
||||||
function cleanEsToken() { | ||||||
// Chaining requests | ||||||
console.log('Cleaning token'); | ||||||
requestPromise(esUrl, esTokenDeleteOptions).then(() => { | ||||||
// Create the k8s secret | ||||||
console.log('Delete K8S secret'); | ||||||
requestPromise(k8sDeleteSecretUrl, secretDeleteOptions).then().catch(err => { | ||||||
console.error(err) | ||||||
}); | ||||||
return; | ||||||
}).catch(err => { | ||||||
console.error(err); | ||||||
}); | ||||||
} | ||||||
|
||||||
const command = process.argv[2]; | ||||||
switch (command) { | ||||||
case 'create': | ||||||
console.log('Creating a new Elasticsearch token for Kibana') | ||||||
createEsToken(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be cleaner to always return the promises created by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean something like that? function createEsToken() {
// Chaining requests
console.log('Cleaning previous token');
return requestPromise(esUrl, esTokenDeleteOptions).then(() => {
console.log('Creating new token');
requestPromise(esUrl, esTokenCreateOptions).then(response => {
// ...
requestPromise(k8sPostSecretUrl, secretCreateOptions, secretData)
});
});
}
function cleanEsToken() {
// Chaining requests
console.log('Cleaning token');
return requestPromise(esUrl, esTokenDeleteOptions).then(() => {
// Create the k8s secret
console.log('Delete K8S secret');
requestPromise(k8sDeleteSecretUrl, secretDeleteOptions)
});
}
const command = process.argv[2];
switch (command) {
case 'create':
console.log('Creating a new Elasticsearch token for Kibana')
createEsToken().catch(err => {
console.error(err);
});
break;
case 'clean':
console.log('Cleaning the Kibana Elasticsearch token')
cleanEsToken().catch(err => {
console.error(err);
});
break;
default:
console.log('Unknown command');
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, just make sure you're always returning the value of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in e3ef0c1 |
||||||
break; | ||||||
case 'clean': | ||||||
console.log('Cleaning the Kibana Elasticsearch token') | ||||||
cleanEsToken(); | ||||||
break; | ||||||
default: | ||||||
console.log('Unknown command'); | ||||||
jmlrt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
kind: Role | ||
metadata: | ||
name: post-delete-{{ template "kibana.fullname" . }} | ||
labels: {{ include "kibana.labels" . | nindent 4 }} | ||
annotations: | ||
"helm.sh/hook": post-delete | ||
"helm.sh/hook-delete-policy": hook-succeeded | ||
{{- if .Values.annotations }} | ||
{{- range $key, $value := .Values.annotations }} | ||
{{ $key }}: {{ $value | quote }} | ||
{{- end }} | ||
{{- end }} | ||
rules: | ||
- apiGroups: | ||
- "" | ||
resources: | ||
- secrets | ||
verbs: | ||
- delete |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
kind: RoleBinding | ||
metadata: | ||
name: post-delete-{{ template "kibana.fullname" . }} | ||
labels: {{ include "kibana.labels" . | nindent 4 }} | ||
annotations: | ||
"helm.sh/hook": post-delete | ||
"helm.sh/hook-delete-policy": hook-succeeded | ||
{{- if .Values.annotations }} | ||
{{- range $key, $value := .Values.annotations }} | ||
{{ $key }}: {{ $value | quote }} | ||
{{- end }} | ||
{{- end }} | ||
subjects: | ||
- kind: ServiceAccount | ||
name: post-delete-{{ template "kibana.fullname" . }} | ||
namespace: {{ .Release.Namespace | quote }} | ||
roleRef: | ||
kind: Role | ||
name: post-delete-{{ template "kibana.fullname" . }} | ||
apiGroup: rbac.authorization.k8s.io |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
apiVersion: v1 | ||
kind: ServiceAccount | ||
metadata: | ||
name: post-delete-{{ template "kibana.fullname" . }} | ||
labels: {{ include "kibana.labels" . | nindent 4 }} | ||
annotations: | ||
"helm.sh/hook": post-delete | ||
"helm.sh/hook-delete-policy": hook-succeeded | ||
{{- if .Values.annotations }} | ||
{{- range $key, $value := .Values.annotations }} | ||
{{ $key }}: {{ $value | quote }} | ||
{{- end }} | ||
{{- end }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will
.Values.elasticsearchHosts
always only have a single value?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, it seems it was supposed to support multiple values but a bug that was never fixed prevented it (#603).
As this Kibana chart is supposed to connect to an Elasticsearch cluster deployed via the Elastic Helm charts, all Elasticsearch pods are supposed to be available via a single K8S service that should be used in
.Values.elasticsearchHosts
.IMHO, It's not worth fixing #603 and making the script compatible with multiple URLs. I'll update the doc to mention that a single URL (Elasticsearch service URL) is expected for this value, to make it clear.