From a61d414a77ac0e893c7e52e565dcf90a422d1022 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 22 Sep 2024 14:12:15 -0700 Subject: [PATCH] GitOps - added sync push for Groups and Strategies --- src/services/gitops.js | 118 ++++++++++++++---- tests/gitops.test.js | 271 +++++++++++++++++++++++++++++++++-------- 2 files changed, 314 insertions(+), 75 deletions(-) diff --git a/src/services/gitops.js b/src/services/gitops.js index cee9b06..dcdd5a1 100644 --- a/src/services/gitops.js +++ b/src/services/gitops.js @@ -1,17 +1,19 @@ import { getComponents } from './component.js'; import { createStrategy } from './config-strategy.js'; -import { createConfig } from './config.js'; +import { createConfig, getConfig } from './config.js'; import { getDomainById, updateDomainVersion } from './domain.js'; -import { getGroupConfig } from './group-config.js'; +import { createGroup, getGroupConfig } from './group-config.js'; export async function pushChanges(domainId, environment, changes) { - let domain = await getDomainById(domainId); + const validations = validateChanges(changes); + if (validations) { + return errorResponse(validations); + } - for (const change of changes) { + let domain = await getDomainById(domainId); + for (const change of changes) { if (change.action === 'NEW') { await processNew(domain, change, environment); - } else { - return errorResponse('Request has invalid actions', domain.lastUpdate); } }; @@ -19,9 +21,66 @@ export async function pushChanges(domainId, environment, changes) { return successResponse('Changes applied successfully', domain.lastUpdate); } +function validateChanges(changes) { + try { + validateActions(changes); + validateDiff(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']; + const hasInvalidDiff = changes.some(change => !validDiff.includes(change.diff)); + + if (hasInvalidDiff) { + throw new Error('Request has invalid type of diff'); + } +} + async function processNew(domain, change, environment) { - if (change.diff === 'CONFIG') { - await processNewConfig(domain, change, environment); + switch (change.diff) { + case 'GROUP': + await processNewGroup(domain, change, environment); + break; + case 'CONFIG': + await processNewConfig(domain, change, environment); + break; + case 'STRATEGY': + await processNewStrategy(domain, change, environment); + break; + } +} + +async function processNewGroup(domain, change, environment) { + const admin = { _id: domain.owner }; + const group = await createGroup({ + domain: domain._id, + name: change.content.name, + description: change.content.description, + activated: new Map().set(environment, change.content.activated), + owner: domain.owner + }, admin); + + if (change.content.configs?.length) { + for (const config of change.content.configs) { + await processNewConfig(domain, { + path: [group.name], + content: config + }, environment); + } } } @@ -49,26 +108,30 @@ async function processNewConfig(domain, change, environment) { if (change.content.strategies?.length) { for (const strategy of change.content.strategies) { - await createStrategy({ - env: environment, - description: strategy.description, - strategy: strategy.strategy, - values: strategy.values, - operation: strategy.operation, - config: config._id, - domain: domain._id, - owner: domain.owner - }, admin); + await processNewStrategy(domain, { + path: [group.name, config.key], + content: strategy + }, environment); } } } -function errorResponse(message, version) { - return { - valid: false, - message, - version - }; +async function processNewStrategy(domain, change, environment) { + const path = change.path; + const content = change.content; + const admin = { _id: domain.owner }; + const config = await getConfig({ domain: domain._id, key: path[1] }); + + await createStrategy({ + env: environment, + description: content.description, + strategy: content.strategy, + values: content.values, + operation: content.operation, + config: config._id, + domain: domain._id, + owner: domain.owner + }, admin); } function successResponse(message, version) { @@ -77,4 +140,11 @@ function successResponse(message, version) { message, version }; +} + +function errorResponse(message) { + return { + valid: false, + message + }; } \ No newline at end of file diff --git a/tests/gitops.test.js b/tests/gitops.test.js index ede0ef8..b8366fa 100644 --- a/tests/gitops.test.js +++ b/tests/gitops.test.js @@ -4,13 +4,15 @@ import jwt from 'jsonwebtoken'; import app from '../src/app'; import { Client } from 'switcher-client'; import { Config } from '../src/models/config'; -import { ConfigStrategy } from '../src/models/config-strategy'; +import { ConfigStrategy, OperationsType, StrategiesType } from '../src/models/config-strategy'; import { EnvType } from '../src/models/environment'; import * as graphqlUtils from './graphql-utils'; import { setupDatabase, - domainId + domainId, + configId } from './fixtures/db_client'; +import GroupConfig from '../src/models/group-config'; afterAll(async () => { await new Promise(resolve => setTimeout(resolve, 1000)); @@ -82,41 +84,106 @@ describe('GitOps - Feature Toggle', () => { }); }); -describe('GitOps - Push Changes', () => { +describe('GitOps - Push New Changes', () => { beforeAll(setupDatabase); - test('GITOPS_SUITE - Should push changes to the domain - New Switcher', async () => { + test('GITOPS_SUITE - Should push changes - New Group', async () => { const token = generateToken('30s'); - const requestPayload = { - environment: EnvType.DEFAULT, - changes: [{ - action: 'NEW', - diff: 'CONFIG', - path: [ - 'Group Test' - ], - content: { - key: 'NEW_SWITCHER', - description: 'New Switcher', - activated: true, - strategies: [{ - strategy: 'VALUE_VALIDATION', - description: 'Test Strategy', - operation: 'EXIST', + const lastUpdate = Date.now(); + const req = await request(app) + .post('/gitops/v1/push') + .set('Authorization', `Bearer ${token}`) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'GROUP', + path: [], + content: { + name: 'New Group', + description: 'New Group Description', + activated: true + } + }] + }) + .expect(200); + + expect(req.body.message).toBe('Changes applied successfully'); + expect(req.body.version).toBeGreaterThan(lastUpdate); + + // Check if the changes were applied + const group = await GroupConfig.findOne({ name: 'New Group', domain: domainId }).lean().exec(); + expect(group).not.toBeNull(); + expect(group.activated[EnvType.DEFAULT]).toBe(true); + expect(group.description).toBe('New Group Description'); + }); + + test('GITOPS_SUITE - Should push changes - New Group and Switcher', async () => { + const token = generateToken('30s'); + + const lastUpdate = Date.now(); + const req = await request(app) + .post('/gitops/v1/push') + .set('Authorization', `Bearer ${token}`) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'GROUP', + path: [], + content: { + name: 'New Group and Switcher', + description: 'New Group Description', activated: true, - values: ['A', 'B'] - }], - components: ['TestApp'] - } - }] - }; + configs: [{ + key: 'NEW_SWITCHER_FROM_GROUP', + description: 'New Switcher', + activated: false, + components: ['TestApp'] + }] + } + }] + }) + .expect(200); + + expect(req.body.message).toBe('Changes applied successfully'); + expect(req.body.version).toBeGreaterThan(lastUpdate); + + // Check if the changes were applied + const group = await GroupConfig.findOne({ name: 'New Group and Switcher', domain: domainId }).lean().exec(); + expect(group).not.toBeNull(); + expect(group.activated[EnvType.DEFAULT]).toBe(true); + expect(group.description).toBe('New Group Description'); + + const config = await Config.findOne({ key: 'NEW_SWITCHER_FROM_GROUP', domain: domainId }).lean().exec(); + expect(config).not.toBeNull(); + expect(config.activated[EnvType.DEFAULT]).toBe(false); + expect(config.description).toBe('New Switcher'); + expect(config.components).toHaveLength(1); + }); + + test('GITOPS_SUITE - Should push changes - New Switcher', async () => { + const token = generateToken('30s'); const lastUpdate = Date.now(); const req = await request(app) .post('/gitops/v1/push') .set('Authorization', `Bearer ${token}`) - .send(requestPayload) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'CONFIG', + path: [ + 'Group Test' + ], + content: { + key: 'NEW_SWITCHER', + activated: true + } + }] + }) .expect(200); expect(req.body.message).toBe('Changes applied successfully'); @@ -126,54 +193,156 @@ describe('GitOps - Push Changes', () => { const config = await Config.findOne({ key: 'NEW_SWITCHER', domain: domainId }).lean().exec(); expect(config).not.toBeNull(); expect(config.activated[EnvType.DEFAULT]).toBe(true); + expect(config.components).toHaveLength(0); + }); + + test('GITOPS_SUITE - Should push changes - New Switcher and Strategy', async () => { + const token = generateToken('30s'); + + const lastUpdate = Date.now(); + const req = await request(app) + .post('/gitops/v1/push') + .set('Authorization', `Bearer ${token}`) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'CONFIG', + path: [ + 'Group Test' + ], + content: { + key: 'NEW_SWITCHER_STRATEGY', + description: 'New Switcher', + activated: true, + strategies: [{ + strategy: StrategiesType.VALUE, + description: 'Test Strategy', + operation: OperationsType.EXIST, + activated: true, + values: ['A', 'B'] + }], + components: ['TestApp'] + } + }] + }) + .expect(200); + + expect(req.body.message).toBe('Changes applied successfully'); + expect(req.body.version).toBeGreaterThan(lastUpdate); + + // Check if the changes were applied + const config = await Config.findOne({ key: 'NEW_SWITCHER_STRATEGY', domain: domainId }).lean().exec(); + expect(config).not.toBeNull(); + expect(config.activated[EnvType.DEFAULT]).toBe(true); expect(config.components).toHaveLength(1); const strategy = await ConfigStrategy.findOne({ config: config._id }).lean().exec(); - expect(strategy).not.toBeNull(); - expect(strategy.activated[EnvType.DEFAULT]).toBe(true); - expect(strategy.values).toEqual(['A', 'B']); - expect(strategy.operation).toBe('EXIST'); - expect(strategy.description).toBe('Test Strategy'); - expect(strategy.strategy).toBe('VALUE_VALIDATION'); + expect(strategy).toMatchObject({ + description: 'Test Strategy', + operation: OperationsType.EXIST, + strategy: StrategiesType.VALUE, + values: ['A', 'B'], + activated: { + [EnvType.DEFAULT]: true + }, + }); + }); + + test('GITOPS_SUITE - Should push changes - New Strategy', async () => { + const token = generateToken('30s'); + + const lastUpdate = Date.now(); + const req = await request(app) + .post('/gitops/v1/push') + .set('Authorization', `Bearer ${token}`) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'STRATEGY', + path: ['Group Test', 'TEST_CONFIG_KEY'], + content: { + strategy: StrategiesType.NUMERIC, + description: 'Test Strategy', + operation: OperationsType.EXIST, + activated: true, + values: ['100', '200'] + } + }] + }) + .expect(200); + + expect(req.body.message).toBe('Changes applied successfully'); + expect(req.body.version).toBeGreaterThan(lastUpdate); + + // Check if the changes were applied + const strategy = await ConfigStrategy.findOne({ config: configId, strategy: StrategiesType.NUMERIC }).lean().exec(); + expect(strategy).toMatchObject({ + description: 'Test Strategy', + operation: OperationsType.EXIST, + strategy: StrategiesType.NUMERIC, + values: ['100', '200'], + activated: { + [EnvType.DEFAULT]: true + }, + }); }); +}); + +describe('GitOps - Push Changes - Errors', () => { + beforeAll(setupDatabase); test('GITOPS_SUITE - Should return error when action is invalid', async () => { const token = generateToken('30s'); - const requestPayload = { - environment: EnvType.DEFAULT, - changes: [{ - action: 'INVALID' - }] - }; + const req = await request(app) + .post('/gitops/v1/push') + .set('Authorization', `Bearer ${token}`) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'INVALID' + }] + }) + .expect(400); + + expect(req.body.message).toBe('Request has invalid type of change'); + }); + + test('GITOPS_SUITE - Should return error when change type is invalid', async () => { + const token = generateToken('30s'); const req = await request(app) .post('/gitops/v1/push') .set('Authorization', `Bearer ${token}`) - .send(requestPayload) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'INVALID' + }] + }) .expect(400); - expect(req.body.message).toBe('Request has invalid actions'); + expect(req.body.message).toBe('Request has invalid type of diff'); }); test('GITOPS_SUITE - Should return error when content is malformed', async () => { const token = generateToken('30s'); - const requestPayload = { - environment: EnvType.DEFAULT, - changes: [{ - action: 'NEW', - diff: 'CONFIG', - }] - }; - const req = await request(app) .post('/gitops/v1/push') .set('Authorization', `Bearer ${token}`) - .send(requestPayload) + .send({ + environment: EnvType.DEFAULT, + changes: [{ + action: 'NEW', + diff: 'CONFIG', + }] + }) .expect(500); expect(req.body.error).not.toBeNull(); }); - }); \ No newline at end of file