mirror of https://github.com/rancher/dashboard.git
452 lines
11 KiB
Vue
452 lines
11 KiB
Vue
<script>
|
|
import CodeMirror from './CodeMirror';
|
|
import AsyncButton from '@/components/AsyncButton';
|
|
import Footer from '@/components/form/Footer';
|
|
import InfoBox from '@/components/InfoBox';
|
|
import { NAMESPACE } from '@/config/types';
|
|
import { _VIEW, _EDIT } from '@/config/query-params';
|
|
import { findBy } from '@/utils/array';
|
|
|
|
export default {
|
|
name: 'GatekeeperConfig',
|
|
|
|
components: {
|
|
AsyncButton,
|
|
CodeMirror,
|
|
InfoBox,
|
|
Footer,
|
|
},
|
|
|
|
props: {
|
|
/**
|
|
* The gatekeeper config if enabled.
|
|
* @model
|
|
*/
|
|
config: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
|
|
/**
|
|
* Page mode
|
|
* @values view, create
|
|
*/
|
|
mode: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
|
|
/**
|
|
* All namespaces
|
|
*/
|
|
namespaces: {
|
|
type: Array,
|
|
default: null,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
let gatekeeperEnabled = false;
|
|
let showYamlEditor = false;
|
|
|
|
if (this.config && this.config.id) {
|
|
gatekeeperEnabled = true;
|
|
}
|
|
|
|
if (this.mode === _EDIT) {
|
|
showYamlEditor = true;
|
|
}
|
|
|
|
return {
|
|
gatekeeperEnabled,
|
|
showYamlEditor,
|
|
errors: [],
|
|
saving: false,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
appVersion() {
|
|
const externalId = this.config?.spec?.externalId;
|
|
|
|
if (!externalId) {
|
|
return null;
|
|
}
|
|
|
|
const version = externalId.split('&').find(e => e.includes('version')).split('=').pop() || null;
|
|
|
|
return version;
|
|
},
|
|
|
|
gatekeeperSystemNamespace() {
|
|
const { namespaces } = this;
|
|
|
|
return namespaces.find(ns => ns.metadata.name === 'gatekeeper-system');
|
|
},
|
|
|
|
cmOptions() {
|
|
const readOnly = this.mode === _VIEW;
|
|
const gutters = ['CodeMirror-lint-markers'];
|
|
|
|
if ( !readOnly ) {
|
|
gutters.push('CodeMirror-foldgutter');
|
|
}
|
|
|
|
return {
|
|
readOnly,
|
|
gutters,
|
|
mode: 'yaml',
|
|
lint: true,
|
|
lineNumbers: !readOnly,
|
|
extraKeys: { 'Ctrl-Space': 'autocomplete' },
|
|
cursorBlinkRate: ( readOnly ? -1 : 530)
|
|
};
|
|
},
|
|
|
|
parsedValuesYaml() {
|
|
const yamlValues = this.config?.spec?.valuesYaml;
|
|
let safeValues = null;
|
|
|
|
try {
|
|
safeValues = window.jsyaml.safeLoad(yamlValues);
|
|
|
|
return safeValues;
|
|
} catch (e) {
|
|
console.error('Unable to parse Yaml Values', e);
|
|
|
|
return {};
|
|
}
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
mode() {
|
|
if (this.mode === _EDIT) {
|
|
this.showYamlEditor = true;
|
|
} else {
|
|
this.showYamlEditor = false;
|
|
}
|
|
},
|
|
|
|
config: {
|
|
deep: true,
|
|
handler() {
|
|
const gatekeeper = this.config || {};
|
|
const meta = gatekeeper?.metadata;
|
|
const gatekeeperStatus = (gatekeeper.status?.conditions || []).slice();
|
|
|
|
// this doesn't seeem right but the only way I can see to check that it was removed before the object goes away
|
|
if (Object.prototype.hasOwnProperty.call(meta, 'deletionTimestamp')) {
|
|
this.gatekeeperEnabled = false;
|
|
this.$emit('gatekeeperEnabled', this.gatekeeperEnabled);
|
|
|
|
return;
|
|
}
|
|
|
|
if (gatekeeperStatus.some(app => app.type === 'Deployed')) {
|
|
this.gatekeeperEnabled = true;
|
|
this.$emit('gatekeeperEnabled', this.gatekeeperEnabled);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
async ensureNamespace() {
|
|
if ( findBy(this.namespaces, 'metadata.name', 'gatekeeper-system') ) {
|
|
return;
|
|
}
|
|
|
|
const newSystemNs = await this.$store.dispatch('cluster/create', {
|
|
type: NAMESPACE,
|
|
metadata: {
|
|
name: 'gatekeeper-system',
|
|
annotations: { 'field.cattle.io/projectId': this.config.spec.projectName },
|
|
labels: { 'field.cattle.io/projectId': this.config.metadata.namespace },
|
|
},
|
|
});
|
|
|
|
await newSystemNs.save();
|
|
await newSystemNs.waitForState('active');
|
|
},
|
|
|
|
showActions() {
|
|
this.$store.commit('action-menu/show', {
|
|
resources: this.config,
|
|
elem: this.$refs.actions,
|
|
});
|
|
},
|
|
/**
|
|
* Gets called when the user clicks on the button
|
|
* Checks for the system namespace and creates that first if it does not exist
|
|
* Creates gatekeeper app deployment
|
|
*
|
|
* @param {buttonCb} Callback to be called on success or fail
|
|
*/
|
|
async enable(buttonCb) {
|
|
try {
|
|
this.saving = true;
|
|
await this.ensureNamespace();
|
|
await this.config.save();
|
|
await this.config.waitForState('active', 60000);
|
|
this.gatekeeperEnabled = true;
|
|
this.showYamlEditor = false;
|
|
this.saving = false;
|
|
buttonCb(true);
|
|
} catch (err) {
|
|
this.gatekeeperEnabled = false;
|
|
this.saving = false;
|
|
if (err?.message) {
|
|
this.errors = [err.message];
|
|
} else {
|
|
this.errors = [err];
|
|
}
|
|
buttonCb(false);
|
|
}
|
|
|
|
this.saving = false;
|
|
},
|
|
|
|
/**
|
|
* Gets called when the user selects advanced configuartion
|
|
*
|
|
*/
|
|
openYamlEditor() {
|
|
if (this.showYamlEditor) {
|
|
if (this.mode === _EDIT) {
|
|
this.$router.push({ name: this.$route.name });
|
|
} else {
|
|
this.showYamlEditor = false;
|
|
}
|
|
} else {
|
|
this.showYamlEditor = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets called when the user clicks on the disable button
|
|
*
|
|
* @param {buttonCb} Callback to be called on success or fail
|
|
*/
|
|
async disable(buttonCb) {
|
|
try {
|
|
await this.config.remove();
|
|
|
|
this.gatekeeperEnabled = false;
|
|
|
|
buttonCb(true);
|
|
} catch (err) {
|
|
this.errors = [err];
|
|
|
|
buttonCb(false);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* An event handler that will be called whenever keydown fires in the CodeMirror input.
|
|
*
|
|
* @param {value} Yaml string
|
|
*/
|
|
onInput(value) {
|
|
this.config.spec.valuesYaml = value;
|
|
},
|
|
|
|
/**
|
|
* Gets called when CodeMirror is ready and folds
|
|
*
|
|
*/
|
|
onReady(cm) {
|
|
cm.getMode().fold = 'yaml';
|
|
|
|
cm.execCommand('foldAll');
|
|
},
|
|
|
|
/**
|
|
* An event handler that will be called whenever CodeMirror onChange event fires.
|
|
*
|
|
* @param {cm} CodeMirror instance
|
|
* @param {changes} Changes from CodeMirror
|
|
*/
|
|
onChanges(cm, changes) {
|
|
if ( changes.length !== 1 ) {
|
|
return;
|
|
}
|
|
|
|
const change = changes[0];
|
|
|
|
if ( change.from.line !== change.to.line ) {
|
|
return;
|
|
}
|
|
|
|
let line = change.from.line;
|
|
let str = cm.getLine(line);
|
|
let maxIndent = indentChars(str);
|
|
|
|
if ( maxIndent === null ) {
|
|
return;
|
|
}
|
|
|
|
cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
|
|
|
|
while ( line > 0 ) {
|
|
line--;
|
|
str = cm.getLine(line);
|
|
const indent = indentChars(str);
|
|
|
|
if ( indent === null ) {
|
|
break;
|
|
}
|
|
|
|
if ( indent < maxIndent ) {
|
|
cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
|
|
|
|
if ( indent === 0 ) {
|
|
break;
|
|
}
|
|
|
|
maxIndent = indent;
|
|
}
|
|
}
|
|
|
|
function indentChars(str) {
|
|
const match = str.match(/^#(\s+)/);
|
|
|
|
if ( match ) {
|
|
return match[1].length;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<header>
|
|
<h1>
|
|
OPA Gatekeeper
|
|
</h1>
|
|
<div v-if="gatekeeperEnabled" class="actions">
|
|
<button ref="actions" type="button" class="btn btn-sm role-multi-action actions" @click="showActions">
|
|
<i class="icon icon-actions" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
<div v-if="gatekeeperEnabled" class="mt-20">
|
|
<InfoBox
|
|
v-if="!showYamlEditor"
|
|
class="row"
|
|
>
|
|
<div class="col span-6 info-column">
|
|
<div class="info-row">
|
|
<label>Audit From Cache: </label>
|
|
{{ parsedValuesYaml.auditFromCache }}
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Audit Interval: </label>
|
|
{{ parsedValuesYaml.auditInterval }}s
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Constraint Violation Limit: </label>
|
|
{{ parsedValuesYaml.constraintViolationsLimit }}
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Replicas: </label>
|
|
{{ parsedValuesYaml.replicas }}
|
|
</div>
|
|
</div>
|
|
<div class="col span-6 info-column">
|
|
<div class="info-row">
|
|
<label>Image: </label>
|
|
{{ parsedValuesYaml.image.repository }}
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Version: </label>
|
|
{{ parsedValuesYaml.image.release }}
|
|
</div>
|
|
</div>
|
|
</InfoBox>
|
|
</div>
|
|
<div v-else class="mt-20 mb-20">
|
|
<article class="col span-12 info">
|
|
<p>
|
|
OPA Gatekeeper provides first-class integration between OPA (Open Policy Agent) and Kubernetes. For more information, visit the <a href="https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/" target="blank">OPA documentation.</a>
|
|
</p>
|
|
</article>
|
|
<div class="row action-group">
|
|
<div class="col">
|
|
<h4 class="mb-20">
|
|
Enable Gatekeeper <span v-if="appVersion">({{ appVersion }}) </span>with defaults.
|
|
</h4>
|
|
<AsyncButton
|
|
:mode="mode"
|
|
action-label="Enable"
|
|
waiting-label="Enabling"
|
|
success-label="Enabled"
|
|
error-label="Error enabling"
|
|
:disabled="showYamlEditor"
|
|
v-bind="$attrs"
|
|
@click="enable"
|
|
/>
|
|
</div>
|
|
<div class="col">
|
|
<h4 class="mb-20">
|
|
Customize Gatekeeper yaml configuartion.
|
|
</h4>
|
|
<button
|
|
type="button"
|
|
class="btn bg-primary"
|
|
:class="{ disabled: saving }"
|
|
:disable="saving"
|
|
@click="openYamlEditor"
|
|
>
|
|
Customize Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section v-if="showYamlEditor">
|
|
<CodeMirror
|
|
:value="config.spec.valuesYaml"
|
|
:options="cmOptions"
|
|
:footer-space="71"
|
|
@onInput="onInput"
|
|
@onReady="onReady"
|
|
@onChanges="onChanges"
|
|
/>
|
|
<Footer
|
|
:mode="mode"
|
|
@errors="errors"
|
|
@save="enable"
|
|
@done="openYamlEditor"
|
|
/>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
article {
|
|
font-size: .8em;
|
|
&.info {
|
|
padding: 10px 0;
|
|
|
|
p {
|
|
padding-bottom: 16px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.action-group {
|
|
padding-top: 20px;
|
|
.col {
|
|
text-align: center;
|
|
flex: 1 1;
|
|
}
|
|
.col:first-of-type {
|
|
border-right: 1px solid;
|
|
}
|
|
}
|
|
</style>
|