diff --git a/services/api/database/migrations/20251019203621_add_updated_to_env_vars.js b/services/api/database/migrations/20251019203621_add_updated_to_env_vars.js new file mode 100644 index 0000000000..79640724ea --- /dev/null +++ b/services/api/database/migrations/20251019203621_add_updated_to_env_vars.js @@ -0,0 +1,25 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .alterTable('env_vars', (table) => { + table.datetime('updated').notNullable().defaultTo(knex.fn.now()); + }) + .raw("UPDATE env_vars SET updated='1970-01-01 00:00:00'"); + // Note, we do the above update so that all _existing_ env vars are + // not picked up as needing to be deployed (since we're using 'updated' to track new/updated vars) + // but any newly created items will get the _current_ date/time +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { +return knex.schema + .alterTable('env_vars', (table) => { + table.dropColumn('updated'); + }) +}; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 4883d1445b..027f495c3b 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -129,6 +129,10 @@ const { deleteEnvironmentService, } = require('./resources/environment/resolvers'); +const { + getPendingChangesByEnvironmentId, +} = require('./resources/environment/environment_redeploy') + const { getDeployTargetConfigById, getDeployTargetConfigsByProjectId, @@ -522,6 +526,7 @@ async function getResolvers() { facts: getFactsByEnvironmentId, openshift: getOpenshiftByEnvironmentId, kubernetes: getOpenshiftByEnvironmentId, + pendingChanges: getPendingChangesByEnvironmentId, }, Organization: { groups: getGroupsByOrganizationId, diff --git a/services/api/src/resources/env-variables/resolvers.ts b/services/api/src/resources/env-variables/resolvers.ts index 3d6fa5569d..4f5b2e8fbc 100644 --- a/services/api/src/resources/env-variables/resolvers.ts +++ b/services/api/src/resources/env-variables/resolvers.ts @@ -417,6 +417,9 @@ export const addOrUpdateEnvVariableByName: ResolverFn = async ( } } + // Let's set the updated value for the env var + updateData['updated'] = knex.fn.now(); + const createOrUpdateSql = knex('env_vars') .insert({ ...updateData, diff --git a/services/api/src/resources/environment/environment_redeploy.ts b/services/api/src/resources/environment/environment_redeploy.ts new file mode 100644 index 0000000000..23b87122f2 --- /dev/null +++ b/services/api/src/resources/environment/environment_redeploy.ts @@ -0,0 +1,104 @@ +// This file contains the logic to determine whether an environment requires a redeploy + +import * as R from 'ramda'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger'; +import { createRemoveTask, seedNamespace } from '@lagoon/commons/dist/tasks'; +import { ResolverFn } from '..'; +import { logger } from '../../loggers/logger'; +import { isPatchEmpty, query, knex } from '../../util/db'; +import { convertDateToMYSQLDateFormat } from '../../util/convertDateToMYSQLDateTimeFormat'; +import { Helpers } from './helpers'; +import { Sql } from './sql'; +import { Sql as projectSql } from '../project/sql'; +import { Helpers as projectHelpers } from '../project/helpers'; +import { Helpers as openshiftHelpers } from '../openshift/helpers'; +import { Helpers as organizationHelpers } from '../organization/helpers'; +import { getFactFilteredEnvironmentIds } from '../fact/resolvers'; +import { getUserProjectIdsFromRoleProjectIds } from '../../util/auth'; +import { RemoveData, DeployType, AuditType } from '@lagoon/commons/dist/types'; +import { AuditLog } from '../audit/types'; + + +export const environmentPendingChangeTypes = { + ENVVAR: "ENVVAR", +}; + +export const getPendingChangesByEnvironmentId: ResolverFn = async( +{ + id +}, +_, +{ sqlClientPool, hasPermission }, +) => { + // Note: as it stands, the only pending changes we have now have to do + // with env vars, but anything can be added in the form + // {type:"string", details:"string", date: "string"} + let pendingChanges = await getPendingEnvVarChanges(sqlClientPool, id); + return pendingChanges; +} + +const getPendingEnvVarChanges = async(sqlClientPool, envId) => { + const sql = ` +WITH last_completed AS ( + SELECT COALESCE(MAX(d.created), TIMESTAMP('1970-01-01 00:00:00')) AS ts + FROM deployment d + WHERE d.environment = ? AND d.status = 'complete' +) +SELECT * +FROM ( + -- Environment-scoped + SELECT + e.id AS env_id, + e.name AS env_name, + ev.name AS envvar_name, + ev.updated AS envvar_updated, + 'Environment' AS envvar_source + FROM environment e + JOIN env_vars ev ON ev.environment = e.id + CROSS JOIN last_completed lc + WHERE e.id = ? + AND ev.name IS NOT NULL + AND ev.updated > lc.ts + + UNION ALL + + -- Project-scoped + SELECT + e.id, e.name, + ev.name, ev.updated, + 'Project' AS envvar_source + FROM environment e + JOIN project p ON p.id = e.project + JOIN env_vars ev ON ev.project = p.id + CROSS JOIN last_completed lc + WHERE e.id = ? + AND ev.name IS NOT NULL + AND ev.updated > lc.ts + + UNION ALL + + -- Organization-scoped + SELECT + e.id, e.name, + ev.name, ev.updated, + 'Organization' AS envvar_source + FROM environment e + JOIN project p ON p.id = e.project + JOIN organization o ON o.id = p.organization + JOIN env_vars ev ON ev.organization = o.id + CROSS JOIN last_completed lc + WHERE e.id = ? + AND ev.name IS NOT NULL + AND ev.updated > lc.ts +) AS allenvs +ORDER BY allenvs.envvar_updated DESC; +`; + + const results = await query(sqlClientPool, sql, [envId, envId, envId, envId]); + + const pendingChanges = results.map(row => { + return {type:environmentPendingChangeTypes.ENVVAR, details: `Variable name: ${row.envvarName} (source: ${row.envvarSource} )`, date: row.envvarUpdated}; + }); + + return pendingChanges; +} \ No newline at end of file diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index c668ffb9fb..b709f013cc 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -897,6 +897,20 @@ const typeDefs = gql` openshiftProjectPattern: String @deprecated(reason: "No longer in use") kubernetes: Kubernetes kubernetesNamespacePattern: String @deprecated(reason: "No longer in use") + """ + Pending changes tell us if we need to redeploy an environment + """ + pendingChanges: [EnvironmentPendingChanges] + } + + type EnvironmentPendingChanges { + type: EnvironmentPendingChangeType + details: String + date: String + } + + enum EnvironmentPendingChangeType { + ENVVAR } type EnvironmentHitsMonth { @@ -1005,6 +1019,7 @@ const typeDefs = gql` scope: String name: String value: String + updated: String } type Task {