ui/lib/global-admin/addon/components/new-multi-cluster-app/component.js

762 lines
21 KiB
JavaScript

import Errors from 'ui/utils/errors';
import {
get, set, computed, setProperties, observer
} from '@ember/object';
import { scheduleOnce } from '@ember/runloop';
import { alias, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import NewOrEdit from 'shared/mixins/new-or-edit';
import C from 'ui/utils/constants';
import Util from 'ui/utils/util';
import { task } from 'ember-concurrency';
import YAML from 'yamljs';
import layout from './template';
import { stringifyAnswer } from 'shared/utils/evaluate';
import { isEmpty } from '@ember/utils';
import CatalogApp from 'shared/mixins/catalog-app';
import { all } from 'rsvp';
import { evaluate } from 'shared/utils/evaluate';
const OVERRIDE_HEADERS = [
{
translationKey: 'newMultiClusterApp.overrides.table.scope',
name: 'scope',
sort: ['scope'],
},
{
translationKey: 'newMultiClusterApp.overrides.table.question',
name: 'question',
sort: ['question'],
},
{
translationKey: 'newMultiClusterApp.overrides.table.answer',
name: 'answer',
sort: ['answer'],
},
];
export default Component.extend(NewOrEdit, CatalogApp, {
catalog: service(),
intl: service(),
scope: service(),
router: service(),
settings: service(),
globalStore: service(),
layout,
allTemplates: null,
templateResource: null,
versionsArray: null,
versionsLinks: null,
actuallySave: true,
showHeader: true,
showPreview: true,
decoding: false,
upgradeStrategy: false,
titleAdd: 'newCatalog.titleAdd',
titleUpgrade: 'newCatalog.titleUpgrade',
selectVersionAdd: 'newCatalog.selectVersionAdd',
selectVersionUpgrade: 'newCatalog.selectVersionUpgrade',
saveUpgrade: 'newCatalog.saveUpgrade',
saveNew: 'newCatalog.saveNew',
sectionClass: 'box mb-20',
showDefaultVersionOption: false,
classNames: ['launch-catalog', 'launch-multicluster-app'],
multiClusterApp: null,
srcSet: false,
detailExpanded: false,
previewOpen: false,
previewTab: null,
questionsArray: null,
selectedTemplateUrl: null,
selectedTemplateModel: null,
readmeContent: null,
appReadmeContent: null,
pastedAnswers: null,
noAppReadme: null,
selectedFileContetnt: null,
answerOverrides: null,
projects: null,
clusters: null,
isClone: false,
projectsToAddOnUpgrade: null,
projectsToRemoveOnUpgrade: null,
overridesHeaders: OVERRIDE_HEADERS,
isGKE: alias('scope.currentCluster.isGKE'),
primaryResource: alias('multiClusterApp'),
editing: notEmpty('primaryResource.id'),
init() {
this._super(...arguments);
this.initAttrs();
this.initUpgradeStrategy();
scheduleOnce('afterRender', () => {
if ( get(this, 'selectedTemplateUrl') ) {
this.templateChanged();
} else {
this.initSelectedTemplateModel();
}
});
},
didRender() {
this.initCatalogIcon();
},
actions: {
addTarget(targetIn) {
if (targetIn && !get(targetIn, 'type')) {
const {
multiClusterApp, editing, projectsToAddOnUpgrade, projectsToRemoveOnUpgrade
} = this;
let target = null;
let toRemoveMatch = (projectsToRemoveOnUpgrade || []).findBy('projectId', get(targetIn, 'value'));
if (toRemoveMatch) {
// a project was remove then re-added
this.projectsToRemoveOnUpgrade.removeObject(targetIn);
target = toRemoveMatch;
} else {
target = this.globalStore.createRecord({
type: 'target',
projectId: get(targetIn, 'value'),
});
}
if (editing) {
projectsToAddOnUpgrade.pushObject(target);
}
if (multiClusterApp.targets) {
multiClusterApp.targets.pushObject(target);
} else {
set(multiClusterApp, 'targets', [target]);
}
}
},
removeTarget(target) {
const {
editing, projectsToRemoveOnUpgrade, projectsToAddOnUpgrade
} = this;
let targetToAddMatch = (projectsToAddOnUpgrade || []).findBy('projectId', get(target, 'projectId'));
if (targetToAddMatch) {
// a project was added then removed
this.projectsToAddOnUpgrade.removeObject(targetToAddMatch);
} else {
if (editing) {
projectsToRemoveOnUpgrade.pushObject(target);
}
}
get(this, 'multiClusterApp.targets').removeObject(target);
},
addRole(roleId, roleToRemove) {
let { roles } = this.multiClusterApp;
if (!roles) {
roles = [];
}
if (roles.indexOf(roleToRemove) > -1) {
roles.removeObject(roleToRemove);
}
if (roleId !== '') {
roles.pushObject(roleId);
set(this, 'multiClusterApp.roles', roles);
} else {
// its possible that the user set extra roles via the api, we shouldn't clobber those roles as well.
if (roles.length > 1) {
set(this, 'multiClusterApp.roles', roles);
} else {
set(this, 'multiClusterApp.roles', null);
}
}
},
addAuthorizedPrincipal(principal) {
if (principal) {
let { members = [] } = this.multiClusterApp;
if (!members) {
members = [];
}
members.pushObject(principal);
set(this, 'multiClusterApp.members', members);
}
},
removeMember(member) {
let { members } = this.multiClusterApp;
members.removeObject(member);
},
gotError(err) {
set(this, 'errors', [Errors.stringify(err)]);
},
addAnswerOverride() {
let { answerOverrides } = this;
let nueOverride = {
scope: null,
question: null,
answer: null,
isSubQuestion: false,
}
if (answerOverrides) {
answerOverrides.pushObject(nueOverride);
} else {
answerOverrides = [nueOverride];
}
set(this, 'answerOverrides', answerOverrides);
},
removeAnswerOverride(answer) {
this.answerOverrides.removeObject(answer);
},
addDependentSubQuestions(answers) {
let { answerOverrides } = this;
answerOverrides.pushObjects(answers);
},
removeDependentSubQuestions(answers) {
let { answerOverrides } = this;
answerOverrides.removeObjects(answers);
},
toogleDetailedDescriptions() {
set(this, 'detailExpanded', true);
},
cancel() {
if (this.cancel) {
this.cancel();
}
},
togglePreview() {
this.toggleProperty('previewOpen');
},
selectPreviewTab(tab) {
set(this, 'previewTab', tab);
},
},
updateAnswerOverrides: observer('selectedTemplateModel', 'multiClusterApp.answers.@each.{values}', function() {
let { selectedTemplateModel = {} } = this;
const questions = get(selectedTemplateModel, 'questions')
const customAnswers = questions ? get(selectedTemplateModel, 'customAnswers') : get(this, 'multiClusterApp.answers');
const answerOverrides = [];
Object.keys(customAnswers).forEach( (customAnswerKey) => {
let answer = get(customAnswers, customAnswerKey);
if (isGlobalAnswersCollection(answer)) {
if (this.isClone || this.editing) {
Object.keys(answer.values).forEach((valueKey) => {
( questions || []).forEach((q) => {
if (get(q, 'variable') === valueKey) {
customAnswers[customAnswerKey];
let allAnswersFromInput = get(customAnswers, `${ customAnswerKey }.values`);
let answerFromInput = allAnswersFromInput ? allAnswersFromInput[valueKey] : null;
try {
answerFromInput = JSON.parse(answerFromInput)
} catch (e) {
}
set(q, 'answer', answerFromInput);
} else if (get(q, 'subquestions') && get(q, 'subquestions').findBy('variable', valueKey)) {
let sqMatch = get(q, 'subquestions').findBy('variable', valueKey);
let allAnswersFromInput = get(customAnswers, `${ customAnswerKey }.values`);
let answerFromInput = allAnswersFromInput ? allAnswersFromInput[valueKey] : null;
set(sqMatch, 'answer', answerFromInput);
}
});
});
}
} else {
Object.keys(answer.values).forEach((valueKey) => {
let isSubQuestion = false;
(questions || []).forEach((q) => {
if (get(q, 'subquestions') && get(q, 'subquestions').findBy('variable', valueKey)) {
isSubQuestion = true;
}
});
let nueOverride = {
scope: answer.clusterId || answer.projectId,
answer: get(answer.values, valueKey),
question: valueKey,
isSubQuestion,
}
answerOverrides.pushObject(nueOverride);
});
}
});
set(this, 'answerOverrides', answerOverrides);
function isGlobalAnswersCollection(answer) {
if (isEmpty(answer.clusterId) && isEmpty(answer.projectId)) {
return true;
} else {
return false;
}
}
}),
upgradeStrategyChanged: observer('upgradeStrategy', function() {
const {
upgradeStrategy, multiClusterApp, globalStore
} = this;
if (upgradeStrategy) {
set(multiClusterApp, 'upgradeStrategy', globalStore.createRecord({
type: 'upgradeStrategy',
rollingUpdate: globalStore.createRecord({
type: 'rollingUpdate',
batchSize: 1,
interval: 1,
})
}));
} else {
set(multiClusterApp, 'upgradeStrategy', null);
}
}),
templateOrHelmChartQuestions: computed('selectedTemplateModel', function() {
let { selectedTemplateModel, multiClusterApp } = this;
let nueQuestions = [];
if (get(selectedTemplateModel, 'questions')) {
return get(selectedTemplateModel, 'questions');
} else {
if (get(multiClusterApp, 'answers.firstObject.values')) {
let helmQuestions = get(multiClusterApp, 'answers.firstObject.values');
nueQuestions = Object.keys(helmQuestions).map((qk) => {
return {
variable: qk,
answer: get(helmQuestions, qk),
};
});
}
return nueQuestions;
}
}),
answers: computed('templateOrHelmChartQuestions.@each.{variable,answer}', function() {
const out = {};
const allQuestions = [];
(get(this, 'templateOrHelmChartQuestions') || []).forEach((item) => {
allQuestions.push(item);
(get(item, 'subquestions') || []).forEach((sub) => {
allQuestions.push(sub);
});
});
const filteredQuestions = allQuestions.filter((q) => evaluate(q, allQuestions));
filteredQuestions.forEach((item) => {
out[item.variable] = stringifyAnswer(item.answer);
});
return out;
}),
answersArray: computed('selectedTemplateModel.questions', 'selectedTemplateModel.customAnswers', 'primaryResource.answers', function() {
const model = get(this, 'selectedTemplateModel');
if (get(model, 'questions')) {
const questions = [];
(get(this, 'selectedTemplateModel.questions') || []).forEach((q) => {
questions.push(q);
const subquestions = get(q, 'subquestions');
if ( subquestions ) {
questions.pushObjects(subquestions);
}
});
const customAnswers = get(this, 'selectedTemplateModel.customAnswers') || {};
Object.keys(customAnswers).forEach((key) => {
questions.push({
variable: key,
answer: customAnswers[key],
});
});
return questions;
} else {
return get(this, 'primaryResource.answers.firstObject.values');
}
}),
answersString: computed('answersArray.@each.{variable,answer}', function() {
const model = get(this, 'selectedTemplateModel');
if (get(model, 'questions')) {
let neu = {};
(get(this, 'answersArray') || []).filter((a) => typeof a.answer !== 'object' ).forEach((a) => {
neu[a.variable] = isEmpty(a.answer) ? a.default : a.answer;
});
const customAnswers = get(model, 'customAnswers') || {};
Object.keys(customAnswers).forEach((key) => {
if ( typeof customAnswers[key] !== 'object' ) {
neu[key] = customAnswers[key];
}
});
return YAML.stringify(neu);
} else {
return JSON.stringify(get(this, 'answersArray'));
}
}),
allProjectsAndClustersUngrouped: computed('projects.[]', 'primaryResource.targets.@each.projectId', function() {
let out = [];
get(this, 'clusters').forEach( (c) => {
out.pushObject({
name: get(c, 'name'),
value: get(c, 'id'),
isCluster: true,
});
c.get('projects').forEach( (p) => {
out.pushObject({
name: get(p, 'name'),
value: get(p, 'id'),
isProject: true,
});
});
});
return out;
}),
getTemplate: task(function * () {
let url = get(this, 'selectedTemplateUrl');
if ( url === 'default' ) {
let defaultUrl = get(this, 'defaultUrl');
if ( defaultUrl ) {
url = defaultUrl;
} else {
url = null;
}
}
if (url) {
let version = get(this, 'settings.rancherVersion');
if ( version ) {
url = Util.addQueryParam(url, 'rancherVersion', version);
}
let current = get(this, 'primaryResource.answers');
if ( !current ) {
current = {};
set(this, 'primaryResource.answers', current);
}
var selectedTemplateModel = yield get(this, 'catalog').fetchByUrl(url)
.then((response) => {
if (response.questions) {
const questions = [];
const customAnswers = {};
response.questions.forEach((q) => {
questions.push(q);
const subquestions = get(q, 'subquestions');
if ( subquestions ) {
questions.pushObjects(subquestions);
}
});
questions.forEach((item) => {
// This will be the component that is rendered to edit this answer
item.inputComponent = `schema/input-${ item.type }`;
// Only types marked supported will show the component, Ember will explode if the component doesn't exist
item.supported = C.SUPPORTED_SCHEMA_INPUTS.indexOf(item.type) >= 0;
if (typeof current[item.variable] !== 'undefined') {
// If there's an existing value, use it (for upgrade)
item.answer = current[item.variable];
} else if (item.type === 'service' || item.type === 'certificate') {
// Loaded async and then the component picks the default
} else if ( item.type === 'boolean' ) {
// Coerce booleans
item.answer = (item.default === 'true' || item.default === true);
} else {
// Everything else
item.answer = item.default || null;
}
});
Object.keys(current).forEach((key) => {
const q = questions.findBy('variable', key);
if ( !q ) {
customAnswers[key] = current[key];
}
});
response.customAnswers = customAnswers;
}
return response;
});
setProperties(this, {
selectedTemplateModel,
'primaryResource.templateVersionId': selectedTemplateModel.id,
});
const files = Object.keys(selectedTemplateModel.get('files')) || [];
if ( files.length > 0 ) {
const valuesYaml = files.find((file) => file.endsWith('/values.yaml'));
set(this, 'previewTab', valuesYaml ? valuesYaml : files[0]);
}
} else {
setProperties(this, {
selectedTemplateModel: null,
readmeContent: null,
appReadmeContent: null,
noAppReadme: false,
})
}
this.updateReadme();
}),
validate() {
this._super(...arguments);
const errors = get(this, 'errors') || [];
errors.pushObjects(get(this, 'selectedTemplateModel').validationErrors() || []);
errors.pushObjects(this.validateTargetsProjectIds());
set(this, 'errors', errors.uniq());
return errors.length === 0;
},
validateTargetsProjectIds() {
let errors = [];
let targets = get(this, 'multiClusterApp.targets');
if (targets && targets.length >= 1) {
targets.forEach((target) => {
if (!get(target, 'projectId')) {
errors.push(this.intl.t('validation.targets.missingProjectId'));
}
});
}
return errors;
},
willSave() {
set(this, 'errors', null);
const { primaryResource } = this;
set(primaryResource, 'answers', this.buildAnswerMap())
const ok = this.validate();
if (!ok) {
// Validation failed
return false;
}
if ( get(this, 'actuallySave') ) {
if (this.editing) {
return this.doProjectActions();
} else {
return true;
}
} else {
return false;
}
},
doProjectActions() {
const { primaryResource } = this;
const { projectsToAddOnUpgrade, projectsToRemoveOnUpgrade } = this;
const promises = [];
const toAdd = (projectsToAddOnUpgrade || []).map((p) => get(p, 'projectId')).uniq();
const toRemove = (projectsToRemoveOnUpgrade || []).map((p) => get(p, 'projectId')).uniq();
const filteredToAdd = toAdd.filter((id) => !toRemove.includes(id));
const filteredToRemove = toRemove.filter((id) => !toAdd.includes(id));
if ( filteredToAdd.length > 0 ) {
promises.push(primaryResource.doAction('addProjects', { projects: filteredToAdd }));
}
if ( filteredToRemove.length > 0 ) {
promises.push(primaryResource.doAction('removeProjects', { projects: filteredToRemove }));
}
if (promises.length > 0) {
return all(promises)
.then(() => {
return true;
})
.catch((/* handled by growl error */) => {
return false;
});
} else {
return true;
}
},
doneSaving() {
return this.router.transitionTo('global-admin.multi-cluster-apps');
},
buildAnswerMap() {
const {
globalStore, answers, answerOverrides
} = this;
let answer = {
type: 'answer',
clusterId: null,
projectId: null,
values: null
};
let out = [];
let globalAnswers = answer;
set(globalAnswers, 'values', answers);
out.pushObject(globalStore.createRecord(globalAnswers));
if (answerOverrides && answerOverrides.length > 0) {
answerOverrides.forEach( (override) => {
let outMatch = out.findBy('clusterId', override.scope) || out.findBy('projectId', override.scope);
let questionVariable = get(override, 'question.variable') ? override.question.variable : override.question;
if (outMatch) {
outMatch.values[questionVariable] = stringifyAnswer(override.answer);
} else {
let newOverrideAnswer = {
type: 'answer',
clusterId: null,
projectId: null,
values: {}
};
let overrideScope = get(this, 'allProjectsAndClustersUngrouped').findBy('value', override.scope);
if (get(overrideScope, 'isProject')) {
set(newOverrideAnswer, 'projectId', override.scope);
}
if (get(overrideScope, 'isCluster')) {
set(newOverrideAnswer, 'clusterId', override.scope);
}
newOverrideAnswer.values[questionVariable] = stringifyAnswer(override.answer);
out.pushObject(globalStore.createRecord(newOverrideAnswer));
}
});
}
return out;
},
initAttrs() {
setProperties(this, {
selectedTemplateModel: null,
projectsToAddOnUpgrade: [],
projectsToRemoveOnUpgrade: [],
});
},
initCatalogIcon() {
if (!this.get('srcSet')) {
set(this, 'srcSet', true);
const $icon = this.$('img');
$icon.attr('src', $icon.data('src'));
this.$('img').on('error', () => {
$icon.attr('src', `${ this.get('app.baseAssets') }assets/images/generic-catalog.svg`);
});
}
},
initSelectedTemplateModel() {
let def = get(this, 'templateResource.defaultVersion');
const latest = get(this, 'templateResource.latestVersion');
const links = get(this, 'versionLinks');
const app = get(this, 'primaryResource');
if (get(app, 'id') && !get(this, 'upgrade')) {
def = get(app, 'externalIdInfo.version');
}
set(this, 'selectedTemplateUrl', links[def] || links[latest] || null)
return;
},
initUpgradeStrategy() {
const { multiClusterApp } = this;
if (get(multiClusterApp, 'upgradeStrategy.rollingUpdate')) {
set(this, 'upgradeStrategy', true);
}
},
});