Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
},
"devDependencies": {
"env-cmd": "^10.1.0",
"eslint": "^9.11.0",
"eslint": "^9.11.1",
"jest": "^29.7.0",
"jest-sonar-reporter": "^2.0.0",
"node-notifier": "^10.0.1",
Expand Down
61 changes: 61 additions & 0 deletions src/middleware/gitops.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { responseException } from '../exceptions';
import { checkGitopsIntegration, SwitcherKeys } from '../external/switcher-api-facade';

const PATH_CONSTRAINTS_NEW = {
GROUP: 0,
CONFIG: 1,
STRATEGY: 2,
COMPONENT: 2,
STRATEGY_VALUE: 3
};

export async function featureFlag(req, res, next) {
try {
await checkGitopsIntegration(req.domain);
next();
} catch (e) {
responseException(res, e, 400, SwitcherKeys.GITOPS_INTEGRATION);
}
};

export function validateChanges(req, res, next) {
try {
req.body = req.body || {};
const changes = req.body.changes;

validatePathForElement(changes);
validateChangesContent(changes);
next();
} catch (e) {
res.status(422).send({
errors: [{
msg: e.message,
location: 'body'
}]
});
}
}

function validatePathForElement(changes) {
for (const change of changes) {
if (change.action === 'NEW') {
const path = change.path;
const diff = change.diff;
if (path.length !== PATH_CONSTRAINTS_NEW[diff]) {
throw new Error('Request has invalid path settings for new element');
}
}
}
}

function validateChangesContent(changes) {
for (const change of changes) {
if (['COMPONENT', 'STRATEGY_VALUE'].includes(change.diff)) {
if (!Array.isArray(change.content)) {
throw new Error('Request has invalid content type [object]');
}
} else if (Array.isArray(change.content)) {
throw new Error('Request has invalid content type [array]');
}
}
}
34 changes: 14 additions & 20 deletions src/routers/gitops.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import express from 'express';
import { check } from 'express-validator';
import { body } from 'express-validator';
import { responseException } from '../exceptions/index.js';
import { gitopsAuth } from '../middleware/auth.js';
import { validate } from '../middleware/validators.js';
import { checkGitopsIntegration, SwitcherKeys } from '../external/switcher-api-facade.js';
import * as Service from '../services/gitops.js';
import { featureFlag, validateChanges } from '../middleware/gitops.js';
import * as Service from '../services/gitops/index.js';

const router = new express.Router();

const featureFlagMiddleware = async (req, res, next) => {
try {
await checkGitopsIntegration(req.domain);
next();
} catch (e) {
responseException(res, e, 400, SwitcherKeys.GITOPS_INTEGRATION);
}
};

router.post('/gitops/v1/push', featureFlagMiddleware, gitopsAuth, [
check('changes').exists(),
], validate, async (req, res) => {
router.post('/gitops/v1/push', featureFlag, gitopsAuth, [
body('environment').isString(),
body('changes').isArray(),
body('changes.*.path').isArray({ min: 0, max: 3 }),
body('changes.*.action')
.custom(value => ['NEW', 'CHANGED', 'DELETED'].includes(value))
.withMessage('Request has invalid type of action'),
body('changes.*.diff')
.custom(value => ['GROUP', 'CONFIG', 'STRATEGY', 'STRATEGY_VALUE', 'COMPONENT'].includes(value))
.withMessage('Request has invalid type of diff'),
], validate, validateChanges, async (req, res) => {
try {
const result = await Service.pushChanges(req.domain, req.body.environment, req.body.changes);

if (!result.valid) {
return res.status(400).send(result);
}

res.status(200).send(result);
} catch (e) {
responseException(res, e, 500);
Expand Down
23 changes: 23 additions & 0 deletions src/services/gitops/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getDomainById, updateDomainVersion } from '../domain.js';
import { processNew } from './push-new.js';

export const ADMIN_EMAIL = 'gitops@admin.noreply.switcherapi.com';

export async function pushChanges(domainId, environment, changes) {
let domain = await getDomainById(domainId);
for (const change of changes) {
if (change.action === 'NEW') {
await processNew(domain, change, environment);
}
};

domain = await updateDomainVersion(domainId);
return successResponse('Changes applied successfully', domain.lastUpdate);
}

function successResponse(message, version) {
return {
message,
version
};
}
108 changes: 23 additions & 85 deletions src/services/gitops.js → src/services/gitops/push-new.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,10 @@
import { getComponents } from './component.js';
import { createStrategy, getStrategies, updateStrategy } from './config-strategy.js';
import { createConfig, getConfig } from './config.js';
import { getDomainById, updateDomainVersion } from './domain.js';
import { createGroup, getGroupConfig } from './group-config.js';
import { getComponents } from '../component.js';
import { createStrategy, getStrategies, updateStrategy } from '../config-strategy.js';
import { addComponent, createConfig, getConfig } from '../config.js';
import { createGroup, getGroupConfig } from '../group-config.js';
import { ADMIN_EMAIL } from './index.js';

const PATH_CONSTRAINTS_NEW = {
GROUP: 0,
CONFIG: 1,
STRATEGY: 2,
STRATEGY_VALUE: 3
};

export async function pushChanges(domainId, environment, changes) {
const validations = validateChanges(changes);
if (validations) {
return errorResponse(validations);
}

let domain = await getDomainById(domainId);
for (const change of changes) {
if (change.action === 'NEW') {
await processNew(domain, change, environment);
}
};

domain = await updateDomainVersion(domainId);
return successResponse('Changes applied successfully', domain.lastUpdate);
}

function validateChanges(changes) {
try {
validateActions(changes);
validateDiff(changes);
validatePathForElement(changes);
} catch (e) {
return e.message;
}

return undefined;
}

function validateActions(changes) {
const validActions = ['NEW', 'CHANGED', 'DELETED'];
const hasInvalidAction = changes.some(change => !validActions.includes(change.action));

if (hasInvalidAction) {
throw new Error('Request has invalid type of change');
}
}

function validateDiff(changes) {
const validDiff = ['GROUP', 'CONFIG', 'STRATEGY', 'STRATEGY_VALUE'];
const hasInvalidDiff = changes.some(change => !validDiff.includes(change.diff));

if (hasInvalidDiff) {
throw new Error('Request has invalid type of diff');
}
}

function validatePathForElement(changes) {
for (const change of changes) {
if (change.action === 'NEW') {
const path = change.path;
const diff = change.diff;
if (path.length !== PATH_CONSTRAINTS_NEW[diff]) {
throw new Error('Request has invalid path settings for new element');
}
}
}
}

async function processNew(domain, change, environment) {
export async function processNew(domain, change, environment) {
switch (change.diff) {
case 'GROUP':
await processNewGroup(domain, change, environment);
Expand All @@ -84,6 +18,9 @@ async function processNew(domain, change, environment) {
case 'STRATEGY_VALUE':
await processNewStrategyValue(domain, change);
break;
case 'COMPONENT':
await processNewComponent(domain, change);
break;
}
}

Expand Down Expand Up @@ -160,7 +97,7 @@ async function processNewStrategy(domain, change, environment) {
async function processNewStrategyValue(domain, change) {
const path = change.path;
const content = change.content;
const admin = { _id: domain.owner, email: 'gitops@admin.noreply.switcherapi.com' };
const admin = { _id: domain.owner, email: ADMIN_EMAIL };
const config = await getConfig({ domain: domain._id, key: path[1] });

const strategies = await getStrategies({ config: config._id });
Expand All @@ -171,17 +108,18 @@ async function processNewStrategyValue(domain, change) {
}, admin);
}

function successResponse(message, version) {
return {
valid: true,
message,
version
};
}
async function processNewComponent(domain, change) {
const path = change.path;
const content = change.content;
const admin = { _id: domain.owner, email: ADMIN_EMAIL };
const config = await getConfig({ domain: domain._id, key: path[1] });

function errorResponse(message) {
return {
valid: false,
message
};
const components = await getComponents({ domain: domain._id, name: { $in: content } });
const componentIds = components.map(component => component._id);

for (const id of componentIds) {
if (!config.components.includes(id)) {
await addComponent(config._id, { component: id }, admin);
}
}
}
Loading