diff --git a/local-dev/api-data-watcher-pusher/api-data/03-populate-api-data-ci-local-control-k8s.gql b/local-dev/api-data-watcher-pusher/api-data/03-populate-api-data-ci-local-control-k8s.gql index 51d5df5460..370a8cdb7f 100644 --- a/local-dev/api-data-watcher-pusher/api-data/03-populate-api-data-ci-local-control-k8s.gql +++ b/local-dev/api-data-watcher-pusher/api-data/03-populate-api-data-ci-local-control-k8s.gql @@ -128,92 +128,78 @@ mutation PopulateApi { id } - RetPol1: createRetentionPolicy(input:{ + RetPol1: createHarborRetentionPolicy(input:{ name: "harbor-policy" - type: HARBOR - harbor: { - enabled: true - rules: [ - { - name: "all branches, excluding pullrequests" - pattern: "[^pr-]*/*" - latestPulled: 3 - }, - { - name: "pullrequests" - pattern: "pr-*" - latestPulled: 1 - } - ] - schedule: "3 * * * *" - } + enabled: true + rules: [ + { + name: "all branches, excluding pullrequests" + pattern: "[^pr-]*/*" + latestPulled: 3 + }, + { + name: "pullrequests" + pattern: "pr-*" + latestPulled: 1 + } + ] + schedule: "3 * * * *" }) { id name configuration { - ... on HarborRetentionPolicy { - enabled - rules { - name - pattern - latestPulled - } - schedule + enabled + rules { + name + pattern + latestPulled } + schedule } - type created updated } - RetPol2: createRetentionPolicy(input:{ + RetPol2: createHistoryRetentionPolicy(input:{ name: "history-policy" - type: HISTORY - history: { - enabled: true - deploymentHistory: 5 - deploymentType: COUNT - taskHistory: 10 - taskType: COUNT - } + enabled: true + deploymentHistory: 5 + deploymentType: COUNT + taskHistory: 10 + taskType: COUNT }) { id name configuration { - ... on HistoryRetentionPolicy { - enabled - deploymentHistory - deploymentType - taskHistory - taskType - } + enabled + deploymentHistory + deploymentType + taskHistory + taskType } - type created updated } - RetPolLink1: addRetentionPolicyLink(input:{ - id: 1 + RetPolLink1: addHarborRetentionPolicyLink(input:{ + name: "harbor-policy" scope: GLOBAL scopeName: "global", }) { id name - type source created updated } - RetPolLink2: addRetentionPolicyLink(input:{ - id: 2 + RetPolLink2: addHistoryRetentionPolicyLink(input:{ + name: "history-policy" scope: GLOBAL scopeName: "global", }) { id name - type source created updated diff --git a/services/api/database/migrations/20240708000000_retention_policy.js b/services/api/database/migrations/20240708000000_retention_policy.js index 0ff5ba450c..0b9e73ecdd 100644 --- a/services/api/database/migrations/20240708000000_retention_policy.js +++ b/services/api/database/migrations/20240708000000_retention_policy.js @@ -6,11 +6,12 @@ exports.up = async function(knex) { return knex.schema .createTable('retention_policy', function (table) { table.increments('id').notNullable().primary(); - table.string('name', 300).unique({indexName: 'name'}); + table.string('name', 300); table.enu('type',['harbor','history']).notNullable(); table.text('configuration'); table.timestamp('updated').notNullable().defaultTo(knex.fn.now()); table.timestamp('created').notNullable().defaultTo(knex.fn.now()); + table.unique(['name', 'type'], {indexName: 'retention_policy'}); }) .createTable('retention_policy_reference', function (table) { table.integer('retention_policy'); diff --git a/services/api/src/models/retentionpolicy.ts b/services/api/src/models/retentionpolicy.ts deleted file mode 100644 index 3e6c74eecf..0000000000 --- a/services/api/src/models/retentionpolicy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { logger } from "../loggers/logger" - -export interface HarborRetentionPolicy { - enabled: boolean - branchRetention: number - pullrequestRetention: number - schedule: string -} - -export interface HistoryRetentionPolicy { - enabled: boolean - deploymentHistory: number - taskHistory: number -} - -export const RetentionPolicy = () => { - const convertHarborRetentionPolicyToJSON = async ( - harbor: HarborRetentionPolicy - ): Promise => { - const c = JSON.stringify(harbor) - return c - }; - - const convertHistoryRetentionPolicyToJSON = async ( - history: HistoryRetentionPolicy - ): Promise => { - const c = JSON.stringify(history) - return c - }; - - const convertJSONToHarborRetentionPolicy = async ( - configuration: string - ): Promise => { - const c = JSON.parse(configuration) - if (typeof c.enabled != "boolean") { - throw new Error("enabled must be a boolean"); - } - if (typeof c.branchRetention != "number") { - throw new Error("branchRetention must be a number"); - } - if (typeof c.pullrequestRetention != "number") { - throw new Error("pullrequestRetention must be a number"); - } - if (typeof c.schedule != "string") { - throw new Error("schedule must be a string"); - } - return c - }; - - const convertJSONToHistoryRetentionPolicy = async ( - configuration: string - ): Promise => { - const c = JSON.parse(configuration) - if (typeof c.enabled != "boolean") { - throw new Error("enabled must be a boolean"); - } - if (typeof c.deploymentHistory != "number") { - throw new Error("deploymentHistory must be a number"); - } - if (typeof c.taskHistory != "number") { - throw new Error("taskHistory must be a number"); - } - return c - }; - - // run the configuration patches through the validation process - const returnValidatedConfiguration = async (type: string, patch: any): Promise => { - const c = JSON.stringify(patch[type]) - switch (type) { - case "harbor": - try { - await convertJSONToHarborRetentionPolicy(c) - return c - } catch (e) { - throw new Error( - `Provided configuration is not valid for type ${type}: ${e}` - ); - } - case "history": - try { - await convertJSONToHistoryRetentionPolicy(c) - return c - } catch (e) { - throw new Error( - `Provided configuration is not valid for type ${type}: ${e}` - ); - } - default: - throw new Error( - `Provided configuration is not valid for type ${type}` - ); - } - } - - return { - convertHarborRetentionPolicyToJSON, - convertHistoryRetentionPolicyToJSON, - convertJSONToHarborRetentionPolicy, - convertJSONToHistoryRetentionPolicy, - returnValidatedConfiguration - }; -}; \ No newline at end of file diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index d61b19011b..6cf6bbf864 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -276,14 +276,22 @@ const { } = require('./resources/backup/resolvers'); const { - createRetentionPolicy, - updateRetentionPolicy, - deleteRetentionPolicy, - getRetentionPoliciesByProjectId, - getRetentionPoliciesByOrganizationId, - listRetentionPolicies, - addRetentionPolicyLink, - removeRetentionPolicyLink, + createHarborRetentionPolicy, + updateHarborRetentionPolicy, + createHistoryRetentionPolicy, + updateHistoryRetentionPolicy, + deleteHarborRetentionPolicy, + deleteHistoryRetentionPolicy, + getHarborRetentionPoliciesByProjectId, + getHarborRetentionPoliciesByOrganizationId, + getHistoryRetentionPoliciesByProjectId, + getHistoryRetentionPoliciesByOrganizationId, + listHarborRetentionPolicies, + listHistoryRetentionPolicies, + addHarborRetentionPolicyLink, + addHistoryRetentionPolicyLink, + removeHarborRetentionPolicyLink, + removeHistoryRetentionPolicyLink, } = require('./resources/retentionpolicy/resolvers'); const { @@ -387,10 +395,6 @@ const resolvers = { ACTIVE: 'active', SUCCEEDED: 'succeeded', }, - RetentionPolicyType: { - HARBOR: 'harbor', - HISTORY: 'history', - }, RetentionPolicyScope: { GLOBAL: 'global', ORGANIZATION: 'organization', @@ -423,7 +427,8 @@ const resolvers = { groups: getGroupsByProjectId, privateKey: getPrivateKey, publicKey: getProjectDeployKey, - retentionPolicies: getRetentionPoliciesByProjectId, + harborRetentionPolicies: getHarborRetentionPoliciesByProjectId, + historyRetentionPolicies: getHistoryRetentionPoliciesByProjectId, }, GroupInterface: { __resolveType(group) { @@ -475,13 +480,15 @@ const resolvers = { owners: getOwnersByOrganizationId, deployTargets: getDeployTargetsByOrganizationId, notifications: getNotificationsByOrganizationId, - retentionPolicies: getRetentionPoliciesByOrganizationId + harborRetentionPolicies: getHarborRetentionPoliciesByOrganizationId, + historyRetentionPolicies: getHistoryRetentionPoliciesByOrganizationId }, OrgProject: { groups: getGroupsByOrganizationsProject, groupCount: getGroupCountByOrganizationProject, notifications: getNotificationsForOrganizationProjectId, - retentionPolicies: getRetentionPoliciesByProjectId, + harborRetentionPolicies: getHarborRetentionPoliciesByProjectId, + historyRetentionPolicies: getHistoryRetentionPoliciesByProjectId, }, OrgEnvironment: { project: getProjectById, @@ -529,18 +536,6 @@ const resolvers = { } } }, - RetentionPolicyConfiguration: { - __resolveType(obj) { - switch (obj.type) { - case 'harbor': - return 'HarborRetentionPolicy'; - case 'history': - return 'HistoryRetentionPolicy'; - default: - return null; - } - } - }, AdvancedTaskDefinition: { __resolveType (obj) { switch(obj.type) { @@ -622,7 +617,8 @@ const resolvers = { getProjectGroupOrganizationAssociation, getEnvVariablesByProjectEnvironmentName, checkBulkImportProjectsAndGroupsToOrganization, - listRetentionPolicies + listHarborRetentionPolicies, + listHistoryRetentionPolicies }, Mutation: { addProblem, @@ -747,11 +743,16 @@ const resolvers = { bulkImportProjectsAndGroupsToOrganization, addOrUpdateEnvironmentService, deleteEnvironmentService, - createRetentionPolicy, - updateRetentionPolicy, - deleteRetentionPolicy, - addRetentionPolicyLink, - removeRetentionPolicyLink + createHarborRetentionPolicy, + updateHarborRetentionPolicy, + deleteHarborRetentionPolicy, + addHarborRetentionPolicyLink, + removeHarborRetentionPolicyLink, + createHistoryRetentionPolicy, + updateHistoryRetentionPolicy, + deleteHistoryRetentionPolicy, + addHistoryRetentionPolicyLink, + removeHistoryRetentionPolicyLink, }, Subscription: { backupChanged: backupSubscriber, diff --git a/services/api/src/resources/retentionpolicy/README.md b/services/api/src/resources/retentionpolicy/README.md index e974c5cb93..cad9e5ff88 100644 --- a/services/api/src/resources/retentionpolicy/README.md +++ b/services/api/src/resources/retentionpolicy/README.md @@ -43,25 +43,22 @@ If the organization based policy is removed from the organization, then the enfo ``` mutation createHarborPolicy { - createRetentionPolicy(input:{ + createHarborRetentionPolicy(input:{ name: "custom-harbor-policy" - type: HARBOR - harbor: { - enabled: true - rules: [ - { - name: "all branches, excluding pullrequests" - pattern: "[^pr\\-]*/*" - latestPulled: 3 - }, - { - name: "pullrequests" - pattern: "pr-*" - latestPulled: 1 - } - ] - schedule: "3 3 * * 3" - } + enabled: true + rules: [ + { + name: "all branches, excluding pullrequests" + pattern: "[^pr\\-]*/*" + latestPulled: 3 + }, + { + name: "pullrequests" + pattern: "pr-*" + latestPulled: 1 + } + ] + schedule: "3 3 * * 3" }) { id name @@ -76,7 +73,6 @@ mutation createHarborPolicy { schedule } } - type created updated } @@ -107,16 +103,13 @@ history policies are enforced on demand. For example, when a new task or deploym ``` mutation createHistoryPolicy { - createRetentionPolicy(input:{ + createHistoryRetentionPolicy(input:{ name: "custom-history-policy" - type: HISTORY - history: { - enabled: true - deploymentHistory: 15 - deploymentType: DAYS - taskHistory: 3 - taskType: MONTHS - } + enabled: true + deploymentHistory: 15 + deploymentType: DAYS + taskHistory: 3 + taskType: MONTHS }) { id name @@ -129,7 +122,6 @@ mutation createHistoryPolicy { taskType } } - type created updated } diff --git a/services/api/src/resources/retentionpolicy/helpers.ts b/services/api/src/resources/retentionpolicy/helpers.ts index fa8d8efae9..78fea05e9a 100644 --- a/services/api/src/resources/retentionpolicy/helpers.ts +++ b/services/api/src/resources/retentionpolicy/helpers.ts @@ -18,6 +18,10 @@ export const Helpers = (sqlClientPool: Pool) => { const rows = await query(sqlClientPool, Sql.selectRetentionPolicyByName(name)); return R.prop(0, rows); }; + const getRetentionPolicyByNameAndType = async (name: string, type: string) => { + const rows = await query(sqlClientPool, Sql.selectRetentionPolicyByNameAndType(name, type)); + return R.prop(0, rows); + }; const getRetentionPolicyByTypeAndLink = async (type: string, sid: number, scope: string) => { const rows = await query(sqlClientPool, Sql.selectRetentionPoliciesByTypeAndLink(type, sid, scope)); return R.prop(0, rows); // ? R.prop(0, rows) : null; @@ -325,6 +329,7 @@ export const Helpers = (sqlClientPool: Pool) => { return { getRetentionPolicy, getRetentionPolicyByName, + getRetentionPolicyByNameAndType, getRetentionPoliciesByProjectWithType, getRetentionPoliciesByOrganizationWithType, getRetentionPoliciesByGlobalWithType, diff --git a/services/api/src/resources/retentionpolicy/resolvers.ts b/services/api/src/resources/retentionpolicy/resolvers.ts index cec63b151b..398aaab494 100644 --- a/services/api/src/resources/retentionpolicy/resolvers.ts +++ b/services/api/src/resources/retentionpolicy/resolvers.ts @@ -1,5 +1,4 @@ -import * as R from 'ramda'; import { ResolverFn } from '..'; import { logger } from '../../loggers/logger'; import { isPatchEmpty, query, knex } from '../../util/db'; @@ -9,30 +8,10 @@ import { Helpers as organizationHelpers } from '../organization/helpers'; import { Helpers as projectHelpers } from '../project/helpers'; import { Sql } from './sql'; -export const createRetentionPolicy: ResolverFn = async ( - _root, - { input }, - { sqlClientPool, hasPermission, userActivityLogger } -) => { +const createRetentionPolicy = async (sqlClientPool, hasPermission, userActivityLogger, input, type) => { await hasPermission('retention_policy', 'add'); - if (input.id) { - const retpol = await Helpers(sqlClientPool).getRetentionPolicy(input.id) - if (retpol) { - throw new Error( - `Retention policy with ID ${input.id} already exists` - ); - } - } - - // @ts-ignore - if (!input.type) { - throw new Error( - 'Must provide type' - ); - } - - const retpol = await Helpers(sqlClientPool).getRetentionPolicyByName(input.name) + const retpol = await Helpers(sqlClientPool).getRetentionPolicyByNameAndType(input.name, type) if (retpol) { throw new Error( `Retention policy with name ${input.name} already exists` @@ -40,8 +19,22 @@ export const createRetentionPolicy: ResolverFn = async ( } // convert the type to the configuration json on import after passing through the validator + let event try { - input.configuration = await RetentionPolicy().returnValidatedConfiguration(input.type, input) + switch (type) { + case "harbor": + event = 'api:createHarborRetentionPolicy' + input.configuration = await RetentionPolicy().returnValidatedHarborConfiguration(input) + break; + case "history": + event = 'api:createHistoryRetentionPolicy' + input.configuration = await RetentionPolicy().returnValidatedHistoryConfiguration(input) + break; + default: + throw new Error( + `No matching type` + ); + } } catch (e) { throw new Error( `${e}` @@ -51,14 +44,15 @@ export const createRetentionPolicy: ResolverFn = async ( const { insertId } = await query( sqlClientPool, Sql.createRetentionPolicy({ + type: type, ...input, })); const row = await Helpers(sqlClientPool).getRetentionPolicy(insertId); - userActivityLogger(`User created a retention policy`, { + userActivityLogger(`User created a ${type} retention policy`, { project: '', - event: 'api:createRetentionPolicy', + event: event, payload: { patch: { name: input.name, @@ -68,23 +62,33 @@ export const createRetentionPolicy: ResolverFn = async ( } }); - return { ...row, configuration: {type: row.type, ...JSON.parse(row.configuration)} }; - // return row; +} + +export const createHarborRetentionPolicy: ResolverFn = async ( + _root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await createRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, input, 'harbor'); }; -export const updateRetentionPolicy: ResolverFn = async ( - root, +export const createHistoryRetentionPolicy: ResolverFn = async ( + _root, { input }, { sqlClientPool, hasPermission, userActivityLogger } ) => { + return await createRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, input, 'history'); +}; + +const updateRetentionPolicy = async (sqlClientPool, hasPermission, userActivityLogger, input, type) => { await hasPermission('retention_policy', 'update'); if (isPatchEmpty(input)) { throw new Error('input.patch requires at least 1 attribute'); } - const retpol = await Helpers(sqlClientPool).getRetentionPolicy(input.id) + const retpol = await Helpers(sqlClientPool).getRetentionPolicyByNameAndType(input.name, type) if (!retpol) { throw new Error( `Retention policy does not exist` @@ -95,28 +99,36 @@ export const updateRetentionPolicy: ResolverFn = async ( name: input.patch.name } - if (!input.patch[retpol.type]) { - throw new Error( - `Missing configuration for type ${retpol.type}, patch not provided` - ); - } - // convert the type to the configuration json on import after passing through the validator + let event try { - patch["configuration"] = await RetentionPolicy().returnValidatedConfiguration(retpol.type, input.patch) + switch (type) { + case "harbor": + event = 'api:updateHarborRetentionPolicy' + patch["configuration"] = await RetentionPolicy().returnValidatedHarborConfiguration(input.patch) + break; + case "history": + event = 'api:updateHistoryRetentionPolicy' + patch["configuration"] = await RetentionPolicy().returnValidatedHistoryConfiguration(input.patch) + break; + default: + throw new Error( + `No matching type` + ); + } } catch (e) { throw new Error( `${e}` ); } - await Helpers(sqlClientPool).updateRetentionPolicy(input.id, patch); + await Helpers(sqlClientPool).updateRetentionPolicy(retpol.id, patch); - const row = await Helpers(sqlClientPool).getRetentionPolicy(input.id); + const row = await Helpers(sqlClientPool).getRetentionPolicy(retpol.id); - userActivityLogger(`User updated retention policy`, { + userActivityLogger(`User updated ${type} retention policy`, { project: '', - event: 'api:updateRetentionPolicy', + event: event, payload: { patch: patch, data: row @@ -127,54 +139,89 @@ export const updateRetentionPolicy: ResolverFn = async ( // if a policy is updated, and the configuration is not the same as before the update // then run postRetentionPolicyUpdateHook to make sure that the policy enforcer does // any policy updates for any impacted projects - const policyEnabled = input.patch[retpol.type].enabled + const policyEnabled = input.patch.enabled await Helpers(sqlClientPool).postRetentionPolicyUpdateHook(retpol.type, retpol.id, null, !policyEnabled) } return { ...row, configuration: {type: row.type, ...JSON.parse(row.configuration)} }; - // return row; +} + +export const updateHarborRetentionPolicy: ResolverFn = async ( + root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await updateRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, input, 'harbor'); }; -export const deleteRetentionPolicy: ResolverFn = async ( - _root, - { id: rid }, +export const updateHistoryRetentionPolicy: ResolverFn = async ( + root, + { input }, { sqlClientPool, hasPermission, userActivityLogger } ) => { + return await updateRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, input, 'history'); +}; + +const deleteRetentionPolicy = async (sqlClientPool, hasPermission, userActivityLogger, name, type) => { await hasPermission('retention_policy', 'delete'); - const retpol = await Helpers(sqlClientPool).getRetentionPolicy(rid) + const retpol = await Helpers(sqlClientPool).getRetentionPolicyByNameAndType(name, type) if (!retpol) { throw new Error( `Retention policy does not exist` ); } - await Helpers(sqlClientPool).deleteRetentionPolicy(rid); + let event + switch (type) { + case "harbor": + event = 'api:deleteHarborRetentionPolicy' + break; + case "history": + event = 'api:deleteHistoryRetentionPolicy' + break; + default: + throw new Error( + `No matching type` + ); + } + + await Helpers(sqlClientPool).deleteRetentionPolicy(retpol.id); - userActivityLogger(`User deleted a retention policy '${retpol.name}'`, { + userActivityLogger(`User deleted a ${type} retention policy '${retpol.name}'`, { project: '', - event: 'api:deleteRetentionPolicy', + event: event, payload: { input: { - retentionPolicy: rid + retentionPolicy: retpol.id } } }); return 'success'; +} + +export const deleteHarborRetentionPolicy: ResolverFn = async ( + _root, + { name }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await deleteRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, name, 'harbor'); }; -export const listRetentionPolicies: ResolverFn = async ( - root, - { type, name }, - { sqlClientPool, hasPermission } +export const deleteHistoryRetentionPolicy: ResolverFn = async ( + _root, + { name }, + { sqlClientPool, hasPermission, userActivityLogger } ) => { + return await deleteRetentionPolicy(sqlClientPool, hasPermission, userActivityLogger, name, 'history'); +}; + +const listRetentionPolicies = async (sqlClientPool, hasPermission, name, type) => { await hasPermission('retention_policy', 'viewAll'); let queryBuilder = knex('retention_policy'); - if (type) { - queryBuilder = queryBuilder.and.where('type', type); - } + queryBuilder = queryBuilder.and.where('type', type); if (name) { queryBuilder = queryBuilder.where('name', name); @@ -182,19 +229,39 @@ export const listRetentionPolicies: ResolverFn = async ( const rows = await query(sqlClientPool, queryBuilder.toString()); return rows.map(row => ({ ...row, source: null, configuration: {type: row.type, ...JSON.parse(row.configuration)} })); -}; +} +export const listHarborRetentionPolicies: ResolverFn = async ( + root, + { name }, + { sqlClientPool, hasPermission } +) => { + return await listRetentionPolicies(sqlClientPool, hasPermission, name, 'harbor') +}; -export const addRetentionPolicyLink: ResolverFn = async ( - _root, - { input }, - { sqlClientPool, hasPermission, userActivityLogger } +export const listHistoryRetentionPolicies: ResolverFn = async ( + root, + { name }, + { sqlClientPool, hasPermission } ) => { + return await listRetentionPolicies(sqlClientPool, hasPermission, name, 'history') +}; +const addRetentionPolicyLink = async (sqlClientPool, hasPermission, userActivityLogger, input, type) => { let scopeId = 0 + let event, prefix + switch (type) { + case "harbor": + prefix = 'api:addHarbor' + break; + case "history": + prefix = 'api:addHistory' + break; + } switch (input.scope) { case "global": await hasPermission('retention_policy', 'addGlobal'); + event = `${prefix}RetentionPolicyGlobal` break; case "organization": const organization = await organizationHelpers(sqlClientPool).getOrganizationByName(input.scopeName) @@ -205,6 +272,7 @@ export const addRetentionPolicyLink: ResolverFn = async ( } await hasPermission('retention_policy', 'addOrganization'); scopeId = organization.id + event = `${prefix}RetentionPolicyOrganization` break; case "project": const project = await projectHelpers(sqlClientPool).getProjectByProjectInput({name: input.scopeName}) @@ -215,6 +283,7 @@ export const addRetentionPolicyLink: ResolverFn = async ( } await hasPermission('retention_policy', 'addProject'); scopeId = project.id + event = `${prefix}RetentionPolicyProject` break; default: throw new Error( @@ -222,7 +291,7 @@ export const addRetentionPolicyLink: ResolverFn = async ( ); } - const retpol = await Helpers(sqlClientPool).getRetentionPolicy(input.id) + const retpol = await Helpers(sqlClientPool).getRetentionPolicyByNameAndType(input.name, type) if (!retpol) { throw new Error( `Retention policy does not exist` @@ -239,7 +308,7 @@ export const addRetentionPolicyLink: ResolverFn = async ( await query( sqlClientPool, Sql.addRetentionPolicyLink( - input.id, + retpol.id, input.scope, scopeId, ) @@ -250,9 +319,9 @@ export const addRetentionPolicyLink: ResolverFn = async ( // any policy updates for any impacted projects await Helpers(sqlClientPool).postRetentionPolicyLinkHook(scopeId, input.scope, retpol.type, retpol.id, false) - userActivityLogger(`User added a retention policy '${retpol.name}' to ${input.scope}`, { + userActivityLogger(`User added a ${type} retention policy '${retpol.name}' to ${input.scope}`, { project: '', - event: 'api:addRetentionPolicyOrganization', + event: event, payload: { input: { retentionPolicy: retpol.id, @@ -262,19 +331,41 @@ export const addRetentionPolicyLink: ResolverFn = async ( } }); - const row = await Helpers(sqlClientPool).getRetentionPolicy(input.id) + const row = await Helpers(sqlClientPool).getRetentionPolicy(retpol.id) return { ...row, configuration: {type: row.type, ...JSON.parse(row.configuration)} }; +} + +export const addHarborRetentionPolicyLink: ResolverFn = async ( + _root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await addRetentionPolicyLink(sqlClientPool, hasPermission, userActivityLogger, input, 'harbor') }; -export const removeRetentionPolicyLink: ResolverFn = async ( +export const addHistoryRetentionPolicyLink: ResolverFn = async ( _root, { input }, { sqlClientPool, hasPermission, userActivityLogger } ) => { + return await addRetentionPolicyLink(sqlClientPool, hasPermission, userActivityLogger, input, 'history') +}; + +const removeRetentionPolicyLink = async (sqlClientPool, hasPermission, userActivityLogger, input, type) => { let scopeId = 0 + let event, prefix + switch (type) { + case "harbor": + prefix = 'api:removeHarbor' + break; + case "history": + prefix = 'api:removeHistory' + break; + } switch (input.scope) { case "global": await hasPermission('retention_policy', 'addGlobal'); + event = `${prefix}RetentionPolicyGlobal` break; case "organization": const organization = await organizationHelpers(sqlClientPool).getOrganizationByName(input.scopeName) @@ -285,6 +376,7 @@ export const removeRetentionPolicyLink: ResolverFn = async ( } await hasPermission('retention_policy', 'addOrganization'); scopeId = organization.id + event = `${prefix}RetentionPolicyOrganization` break; case "project": const project = await projectHelpers(sqlClientPool).getProjectByProjectInput({name: input.scopeName}) @@ -295,6 +387,7 @@ export const removeRetentionPolicyLink: ResolverFn = async ( } await hasPermission('retention_policy', 'addProject'); scopeId = project.id + event = `${prefix}RetentionPolicyProject` break; default: throw new Error( @@ -302,14 +395,14 @@ export const removeRetentionPolicyLink: ResolverFn = async ( ); } - const retpol = await Helpers(sqlClientPool).getRetentionPolicy(input.id); + const retpol = await Helpers(sqlClientPool).getRetentionPolicyByNameAndType(input.name, type); if (!retpol) { throw new Error( `Retention policy does not exist` ); } - const retpoltypes = await Helpers(sqlClientPool).getRetentionPoliciesByTypePolicyIDAndLink(retpol.type, input.id, scopeId, input.scope); + const retpoltypes = await Helpers(sqlClientPool).getRetentionPoliciesByTypePolicyIDAndLink(retpol.type, retpol.id, scopeId, input.scope); if (retpoltypes.length == 0) { throw new Error( `No matching retention policy attached to this ${input.scope}` @@ -326,7 +419,7 @@ export const removeRetentionPolicyLink: ResolverFn = async ( await query( sqlClientPool, Sql.deleteRetentionPolicyLink( - input.id, + retpol.id, input.scope, scopeId, ) @@ -346,9 +439,9 @@ export const removeRetentionPolicyLink: ResolverFn = async ( await Helpers(sqlClientPool).postRetentionPolicyUpdateHook(retpol.type, retpol.id, preDeleteProjectIds, true) } - userActivityLogger(`User removed a retention policy '${retpol.name}' from organization`, { + userActivityLogger(`User removed a ${type} retention policy '${retpol.name}' from organization`, { project: '', - event: 'api:removeRetentionPolicyOrganization', + event: event, payload: { input: { retentionPolicy: retpol.id, @@ -359,10 +452,42 @@ export const removeRetentionPolicyLink: ResolverFn = async ( }); return "success" +} + +export const removeHarborRetentionPolicyLink: ResolverFn = async ( + _root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await removeRetentionPolicyLink(sqlClientPool, hasPermission, userActivityLogger, input, 'harbor') }; -// This is only called by the project resolver, so there is no need to do any permission checks -export const getRetentionPoliciesByProjectId: ResolverFn = async ( +export const removeHistoryRetentionPolicyLink: ResolverFn = async ( + _root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + return await removeRetentionPolicyLink(sqlClientPool, hasPermission, userActivityLogger, input, 'history') +}; + +// This is only called by the project resolver, so there is no need to do any permission checks as they're already done by the project +export const getHarborRetentionPoliciesByProjectId: ResolverFn = async ( + project, + args, + { sqlClientPool } +) => { + + let pid = args.project; + if (project) { + pid = project.id; + } + let rows = [] + rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink('harbor', 'project', project.id); + return rows; +}; + +// This is only called by the project resolver, so there is no need to do any permission checks as they're already done by the project +export const getHistoryRetentionPoliciesByProjectId: ResolverFn = async ( project, args, { sqlClientPool } @@ -373,12 +498,28 @@ export const getRetentionPoliciesByProjectId: ResolverFn = async ( pid = project.id; } let rows = [] - rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink(args.type, "project", project.id); + rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink('history', 'project', project.id); + return rows; +}; + +// This is only called by the organization resolver, so there is no need to do any permission checks as they're already done by the organization +export const getHarborRetentionPoliciesByOrganizationId: ResolverFn = async ( + organization, + args, + { sqlClientPool } +) => { + + let oid = args.organization; + if (organization) { + oid = organization.id; + } + let rows = [] + rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink('harbor', 'organization', oid); return rows; }; -// This is only called by the organization resolver, so there is no need to do any permission checks -export const getRetentionPoliciesByOrganizationId: ResolverFn = async ( +// This is only called by the organization resolver, so there is no need to do any permission checks as they're already done by the organization +export const getHistoryRetentionPoliciesByOrganizationId: ResolverFn = async ( organization, args, { sqlClientPool } @@ -389,6 +530,6 @@ export const getRetentionPoliciesByOrganizationId: ResolverFn = async ( oid = organization.id; } let rows = [] - rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink(args.type, "organization", oid); + rows = await Helpers(sqlClientPool).getRetentionPoliciesByScopeWithTypeAndLink('history', 'organization', oid); return rows; }; \ No newline at end of file diff --git a/services/api/src/resources/retentionpolicy/sql.ts b/services/api/src/resources/retentionpolicy/sql.ts index 58ae419caf..84cd64613c 100644 --- a/services/api/src/resources/retentionpolicy/sql.ts +++ b/services/api/src/resources/retentionpolicy/sql.ts @@ -19,6 +19,11 @@ export const Sql = { knex('retention_policy') .where('name', '=', name) .toString(), + selectRetentionPolicyByNameAndType: (name: string, type: string) => + knex('retention_policy') + .where('name', '=', name) + .where('type', '=', type) + .toString(), selectRetentionPoliciesByType: (type: string) => knex('retention_policy') .where('type', '=', type) diff --git a/services/api/src/resources/retentionpolicy/types.ts b/services/api/src/resources/retentionpolicy/types.ts index 1494d9b692..1f3f79d23c 100644 --- a/services/api/src/resources/retentionpolicy/types.ts +++ b/services/api/src/resources/retentionpolicy/types.ts @@ -101,32 +101,29 @@ export const RetentionPolicy = () => { return c }; + // run the configuration patches through the validation process + const returnValidatedHarborConfiguration = async (patch: any): Promise => { + const c = JSON.stringify(patch) + try { + await convertJSONToHarborRetentionPolicy(c) + return c + } catch (e) { + throw new Error( + `Provided harbor configuration is not valid: ${e}` + ); + } + } + // run the configuration patches through the validation process - const returnValidatedConfiguration = async (type: string, patch: any): Promise => { - const c = JSON.stringify(patch[type]) - switch (type) { - case "harbor": - try { - await convertJSONToHarborRetentionPolicy(c) - return c - } catch (e) { - throw new Error( - `Provided configuration is not valid for type ${type}: ${e}` - ); - } - case "history": - try { - await convertJSONToHistoryRetentionPolicy(c) - return c - } catch (e) { - throw new Error( - `Provided configuration is not valid for type ${type}: ${e}` - ); - } - default: - throw new Error( - `Provided configuration is not valid for type ${type}` - ); + const returnValidatedHistoryConfiguration = async (patch: any): Promise => { + const c = JSON.stringify(patch) + try { + await convertJSONToHistoryRetentionPolicy(c) + return c + } catch (e) { + throw new Error( + `Provided history configuration is not valid: ${e}` + ); } } @@ -135,6 +132,7 @@ export const RetentionPolicy = () => { convertHistoryRetentionPolicyToJSON, convertJSONToHarborRetentionPolicy, convertJSONToHistoryRetentionPolicy, - returnValidatedConfiguration + returnValidatedHarborConfiguration, + returnValidatedHistoryConfiguration }; }; \ No newline at end of file diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 421d41161f..8fcab2d395 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -810,10 +810,15 @@ const typeDefs = gql` buildImage: String sharedBaasBucket: Boolean """ - retentionPolicies are the available retention policies to a project, this will also include inherited policies from an organization + harborRetentionPolicies are the available harbor retention policies to a project, this will also include inherited policies from an organization if the project is associated to an organization, and the organization has any retention policies """ - retentionPolicies(type: RetentionPolicyType): [RetentionPolicy] + harborRetentionPolicies: [HarborRetentionPolicy] + """ + historyRetentionPolicies are the available history retention policies to a project, this will also include inherited policies from an organization + if the project is associated to an organization, and the organization has any retention policies + """ + historyRetentionPolicies: [HistoryRetentionPolicy] } """ @@ -1105,9 +1110,13 @@ const typeDefs = gql` notifications(type: NotificationType): [Notification] created: String """ - retentionPolicies are the available retention policies to an organization + harborRetentionPolicies are the available harbor retention policies to an organization + """ + harborRetentionPolicies: [HarborRetentionPolicy] + """ + historyRetentionPolicies are the available history retention policies to an organization """ - retentionPolicies(type: RetentionPolicyType): [RetentionPolicy] + historyRetentionPolicies: [HistoryRetentionPolicy] } input AddOrganizationInput { @@ -1153,10 +1162,15 @@ const typeDefs = gql` groupCount: Int notifications: [OrganizationNotification] """ - retentionPolicies are the available retention policies to a project, this will also include inherited policies from an organization + harborRetentionPolicies are the available harbor retention policies to a project, this will also include inherited policies from an organization if the project is associated to an organization, and the organization has any retention policies """ - retentionPolicies(type: RetentionPolicyType): [RetentionPolicy] + harborRetentionPolicies: [HarborRetentionPolicy] + """ + historyRetentionPolicies are the available history retention policies to a project, this will also include inherited policies from an organization + if the project is associated to an organization, and the organization has any retention policies + """ + historyRetentionPolicies: [HistoryRetentionPolicy] } """ @@ -1454,7 +1468,8 @@ const typeDefs = gql` getProjectGroupOrganizationAssociation(input: ProjectOrgGroupsInput!): String @deprecated(reason: "Use checkBulkImportProjectsAndGroupsToOrganization instead") getEnvVariablesByProjectEnvironmentName(input: EnvVariableByProjectEnvironmentNameInput!): [EnvKeyValue] checkBulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput!): ProjectGroupsToOrganization - listRetentionPolicies(type: RetentionPolicyType, name: String): [RetentionPolicy] + listHarborRetentionPolicies(name: String): [HarborRetentionPolicy] + listHistoryRetentionPolicies(name: String): [HistoryRetentionPolicy] } type ProjectGroupsToOrganization { @@ -2346,22 +2361,15 @@ const typeDefs = gql` name: String } - """ - RetentionPolicyType is the types of retention policies supported in Lagoon - """ - enum RetentionPolicyType { - HARBOR - HISTORY - } - """ HarborRetentionPolicy is the type for harbor retention policies """ - type HarborRetentionPolicy { + type HarborRetentionPolicyConfiguration { enabled: Boolean rules: [HarborRetentionRule] schedule: String } + type HarborRetentionRule { name: String """ @@ -2374,32 +2382,9 @@ const typeDefs = gql` } """ - HarborRetentionPolicyInput is the input for a HarborRetentionPolicy - """ - input HarborRetentionPolicyInput { - enabled: Boolean! - rules: [HarborRetentionRuleInput!] - schedule: String! - } - input HarborRetentionRuleInput { - name: String! - pattern: String! - latestPulled: Int! - } - - """ - HistoryRetentionType is the types of retention policies supported in Lagoon - """ - enum HistoryRetentionType { - COUNT - DAYS - MONTHS - } - - """ - HistoryRetentionPolicy is the type for history retention policies + HistoryRetentionPolicyConfiguration is the type for history retention policies """ - type HistoryRetentionPolicy { + type HistoryRetentionPolicyConfiguration { enabled: Boolean deploymentHistory: Int """ @@ -2420,93 +2405,143 @@ const typeDefs = gql` } """ - HistoryRetentionPolicyInput is the input for a HistoryRetentionPolicy + HistoryRetentionType is the types of retention policies supported in Lagoon """ - input HistoryRetentionPolicyInput { - enabled: Boolean! - deploymentHistory: Int! - deploymentType: HistoryRetentionType! - taskHistory: Int! - taskType: HistoryRetentionType! + enum HistoryRetentionType { + COUNT + DAYS + MONTHS } """ - RetentionPolicyConfiguration is a union type of different retention policies supported in Lagoon + HarborRetentionPolicy is the return type for harbor retention policies in Lagoon """ - union RetentionPolicyConfiguration = HarborRetentionPolicy | HistoryRetentionPolicy + type HarborRetentionPolicy { + id: Int + name: String + configuration: HarborRetentionPolicyConfiguration + created: String + updated: String + """ + source is where the retention policy source is coming from, this field is only populated when a project or organization + lists the available retention polices, and is used to indicate if a project is consuiming a retention policy from the project directly + or from an organization itself + """ + source: String + } """ - RetentionPolicy is the return type for retention policies in Lagoon + HistoryRetentionPolicy is the return type for history retention policies in Lagoon """ - type RetentionPolicy { + type HistoryRetentionPolicy { id: Int name: String - type: String - """ - configuration is the return type of union based retention policy configurations, the type of retention policy - influences the return type needed here - """ - configuration: RetentionPolicyConfiguration + configuration: HistoryRetentionPolicyConfiguration created: String updated: String """ source is where the retention policy source is coming from, this field is only populated when a project or organization lists the available retention polices, and is used to indicate if a project is consuiming a retention policy from the project directly - or from the organization itself + or from an organization itself """ source: String } """ - AddRetentionPolicyInput is used as the input for updating a retention policy, this is a union type - Currently only the 'harbor' type is supported as an input, if other retention policies are added in the future - They will be subfields of this input, the RetentionPolicyType must match the subfield input type + AddHarborRetentionPolicyInput is used as the input for creating a harbor retention policy + """ + input AddHarborRetentionPolicyInput { + id: Int + name: String! + enabled: Boolean! + rules: [HarborRetentionRuleInput!] + schedule: String! + } + + input HarborRetentionRuleInput { + name: String! + pattern: String! + latestPulled: Int! + } + + """ + AddHistoryRetentionPolicyInput is used as the input for creating a history retention policy """ - input AddRetentionPolicyInput { + input AddHistoryRetentionPolicyInput { id: Int name: String! - type: RetentionPolicyType! - harbor: HarborRetentionPolicyInput - history: HistoryRetentionPolicyInput + enabled: Boolean! + deploymentHistory: Int! + deploymentType: HistoryRetentionType! + taskHistory: Int! + taskType: HistoryRetentionType! } """ - UpdateRetentionPolicyPatchInput is used as the input for updating a retention policy, this is a union type - Currently only the 'harbor' type is supported as a patch input, if other retention policies are added in the future - They will be subfields of this patch input + UpdateHarborRetentionPolicyInput is used as the input for updating a harbor retention policy """ - input UpdateRetentionPolicyPatchInput { + input UpdateHarborRetentionPolicyInput { + name: String! + patch: UpdateHarborRetentionPolicyPatchInput + } + + """ + UpdateHarborRetentionPolicyPatchInput is used as the patch for updating a harbor retention policy + """ + input UpdateHarborRetentionPolicyPatchInput { name: String - harbor: HarborRetentionPolicyInput - history: HistoryRetentionPolicyInput + enabled: Boolean! + rules: [HarborRetentionRuleInput!] + schedule: String! } """ - UpdateRetentionPolicyInput is used as the input for updating a retention policy + UpdateHistoryRetentionPolicyInput is used as the input for updating a history retention policy """ - input UpdateRetentionPolicyInput { - id: Int! - patch: UpdateRetentionPolicyPatchInput + input UpdateHistoryRetentionPolicyInput { + name: String! + patch: UpdateHistoryRetentionPolicyPatchInput } """ - RetentionPolicyScope is the types of retention policies scopes in Lagoon + UpdateHistoryRetentionPolicyPatchInput is used as the patch for updating a history retention policy """ - enum RetentionPolicyScope { - GLOBAL - ORGANIZATION - PROJECT + input UpdateHistoryRetentionPolicyPatchInput { + name: String + enabled: Boolean! + deploymentHistory: Int! + deploymentType: HistoryRetentionType! + taskHistory: Int! + taskType: HistoryRetentionType! } """ AddRetentionPolicyLinkInput is used as the input for associating a retention policy with a scope """ input AddRetentionPolicyLinkInput { - id: Int! + name: String! scope: RetentionPolicyScope! scopeName: String } + """ + RemoveRetentionPolicyLinkInput is used as the input for removing a harbor retention policy with a scope + """ + input RemoveRetentionPolicyLinkInput { + name: String! + scope: RetentionPolicyScope! + scopeName: String + } + + """ + RetentionPolicyScope is the types of retention policies scopes in Lagoon + """ + enum RetentionPolicyScope { + GLOBAL + ORGANIZATION + PROJECT + } + type Mutation { """ Add Environment or update if it is already existing @@ -2723,26 +2758,48 @@ const typeDefs = gql` bulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput, detachNotification: Boolean): ProjectGroupsToOrganization addOrUpdateEnvironmentService(input: AddEnvironmentServiceInput!): EnvironmentService deleteEnvironmentService(input: DeleteEnvironmentServiceInput!): String + + """ + Create a harbor retention policy + """ + createHarborRetentionPolicy(input: AddHarborRetentionPolicyInput!): HarborRetentionPolicy + """ + Update a harbor retention policy + """ + updateHarborRetentionPolicy(input: UpdateHarborRetentionPolicyInput!): HarborRetentionPolicy + """ + Delete a harbor retention policy + """ + deleteHarborRetentionPolicy(name: String!): String + """ + Add an existing harbor retention policy to a resource type + """ + addHarborRetentionPolicyLink(input: AddRetentionPolicyLinkInput!): HarborRetentionPolicy + """ + Remove an existing harbor retention policy from a resource type + """ + removeHarborRetentionPolicyLink(input: RemoveRetentionPolicyLinkInput!): String + """ - Create a retention policy + Create an environment history retention policy """ - createRetentionPolicy(input: AddRetentionPolicyInput!): RetentionPolicy + createHistoryRetentionPolicy(input: AddHistoryRetentionPolicyInput!): HistoryRetentionPolicy """ - Update a retention policy + Update an environment history retention policy """ - updateRetentionPolicy(input: UpdateRetentionPolicyInput!): RetentionPolicy + updateHistoryRetentionPolicy(input: UpdateHistoryRetentionPolicyInput!): HistoryRetentionPolicy """ - Delete a retention policy + Delete an environment history retention policy """ - deleteRetentionPolicy(id: Int!): String + deleteHistoryRetentionPolicy(name: String!): String """ Add an existing retention policy to a resource type """ - addRetentionPolicyLink(input: AddRetentionPolicyLinkInput!): RetentionPolicy + addHistoryRetentionPolicyLink(input: AddRetentionPolicyLinkInput!): HistoryRetentionPolicy """ - Remove an existing retention policy from a resource type + Remove an existing history retention policy from a resource type """ - removeRetentionPolicyLink(input: AddRetentionPolicyLinkInput!): String + removeHistoryRetentionPolicyLink(input: RemoveRetentionPolicyLinkInput!): String } type Subscription {