Skip to content

Commit 4a06218

Browse files
committed
feat: feature gate api routes by organization, disabled by default
1 parent 551b01d commit 4a06218

File tree

7 files changed

+118
-1
lines changed

7 files changed

+118
-1
lines changed

services/api/database/migrations/20250925000000_routes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ exports.up = async function(knex) {
5959
table.text('autogenerated_path_routes') // would be JSON equivalent of what is in lagoon.yml, would override lagoon.yml if defined
6060
table.boolean('disable_request_verification'); // default to null to retain lagoon.yml as the source, would override lagoon.yml if defined
6161
})
62+
.alterTable('organization', function (table) {
63+
table.boolean('feature_api_routes'.defaultTo(0)); // disable api routes feature by default, future release of lagoon will remove this when routes api becomes generally available
64+
})
6265
}
6366
else {
6467
return knex.schema

services/api/src/resolvers.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ const {
192192
removeProjectMetadataByKey,
193193
getPrivateKey,
194194
getProjectDeployKey,
195-
getAutogeneratedRoutePrefixes
195+
getAutogeneratedRoutePrefixes,
196+
getFeatureApiRoutes
196197
} = require('./resources/project/resolvers');
197198

198199
const {
@@ -520,6 +521,7 @@ async function getResolvers() {
520521
apiRoutes: getRoutesByProjectId,
521522
autogeneratedRoutePrefixes: getAutogeneratedRoutePrefixes,
522523
autogeneratedPathRoutes: getAutogeneratedPathRoutes,
524+
featureApiRoutes: getFeatureApiRoutes,
523525
},
524526
GroupInterface: {
525527
__resolveType(group) {

services/api/src/resources/organization/resolvers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ export const updateOrganization: ResolverFn = async (
230230
await hasPermission('organization', 'updateOrganization', input.id);
231231
}
232232

233+
// if the beta feature flag for api routes is being updated, check if permission to enable this flag is set
234+
// this feature will become generally available in a future version and this flag will not
235+
// be required
236+
if (input.patch.featureApiRoutes) {
237+
await hasPermission('organization', 'update');
238+
} else {
239+
await hasPermission('organization', 'updateOrganization', input.id);
240+
}
241+
233242
if (input.patch.name) {
234243
// check if the name is valid
235244
isValidName(input.patch.name)

services/api/src/resources/project/helpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { query } from '../../util/db';
55
import { Sql } from './sql';
66
import { Sql as environmentSql } from '../environment/sql';
77
import { Sql as backupSql } from '../backup/sql';
8+
import { Helpers as organizationHelpers } from '../organization/helpers';
89
// import { logger } from '../../loggers/logger';
910

1011
export const Helpers = (sqlClientPool: Pool) => {
@@ -102,6 +103,14 @@ export const Helpers = (sqlClientPool: Pool) => {
102103
const getProjectsByIds = (projectIds: number[]) =>
103104
query(sqlClientPool, Sql.selectProjectsByIds(projectIds));
104105

106+
const checkApiRoutesFeature = async (organizationId: number) => {
107+
const organization = await organizationHelpers(sqlClientPool).getOrganizationById(organizationId);
108+
if (!organization) {
109+
return false;
110+
}
111+
return organization.featureApiRoutes;
112+
};
113+
105114
return {
106115
checkOrgProjectViewPermission,
107116
checkOrgProjectUpdatePermission,
@@ -111,6 +120,7 @@ export const Helpers = (sqlClientPool: Pool) => {
111120
getProjectsByIds,
112121
getProjectByEnvironmentId,
113122
getProjectByOrganizationId,
123+
checkApiRoutesFeature,
114124
getProjectIdByName: async (name: string): Promise<number> => {
115125
const pidResult = await query(
116126
sqlClientPool,

services/api/src/resources/project/resolvers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,3 +1138,18 @@ export const getAutogeneratedRoutePrefixes: ResolverFn = async (
11381138
) => {
11391139
return input.autogeneratedRoutePrefixes?.split(",");
11401140
};
1141+
1142+
1143+
/*
1144+
getFeatureApiRoutes is a field resolver
1145+
it has no permission checks as it isn't called directly
1146+
this is used to set the beta feature flag on a project from the organization
1147+
this feature will eventually be made generally available and the feature flag will be removed
1148+
*/
1149+
export const getFeatureApiRoutes: ResolverFn = async (
1150+
input,
1151+
args,
1152+
{ sqlClientPool }
1153+
) => {
1154+
return Helpers(sqlClientPool).checkApiRoutesFeature(input.organization);
1155+
};

services/api/src/resources/routes/resolvers.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export const addRouteToProject: ResolverFn = async (
5757
project: projectId
5858
});
5959

60+
// this is used to set the beta feature flag on a project from the organization
61+
// this feature will eventually be made generally available and the feature flag will be removed
62+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
63+
throw Error(`This feature is currently unavailable`)
64+
}
65+
6066
// trim spaces from domain name
6167
const domainName = domain.trim()
6268

@@ -267,6 +273,12 @@ export const updateRouteOnProject: ResolverFn = async (
267273
project: projectId
268274
});
269275

276+
// this is used to set the beta feature flag on a project from the organization
277+
// this feature will eventually be made generally available and the feature flag will be removed
278+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
279+
throw Error(`This feature is currently unavailable`)
280+
}
281+
270282
const existsProject = await query(
271283
sqlClientPool,
272284
Sql.selectRouteByDomainAndProjectID(domain, projectId)
@@ -357,6 +369,13 @@ export const addOrUpdateRouteOnEnvironment: ResolverFn = async (
357369
await hasPermission('route', 'add:environment', {
358370
project: projectId
359371
});
372+
373+
// this is used to set the beta feature flag on a project from the organization
374+
// this feature will eventually be made generally available and the feature flag will be removed
375+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
376+
throw Error(`This feature is currently unavailable`)
377+
}
378+
360379
const env = await environmentHelpers(sqlClientPool).getEnvironmentByNameAndProject(environment, projectId)
361380
const environmentData = env[0]
362381

@@ -567,6 +586,13 @@ export const removeRouteFromEnvironment: ResolverFn = async (
567586
await hasPermission('route', 'remove:environment', {
568587
project: projectId
569588
});
589+
590+
// this is used to set the beta feature flag on a project from the organization
591+
// this feature will eventually be made generally available and the feature flag will be removed
592+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
593+
throw Error(`This feature is currently unavailable`)
594+
}
595+
570596
const env = await environmentHelpers(sqlClientPool).getEnvironmentByNameAndProject(environment, projectId)
571597
const environmentData = env[0]
572598

@@ -650,6 +676,12 @@ export const addRouteAlternativeDomains: ResolverFn = async (
650676
project: projectData.id
651677
});
652678

679+
// this is used to set the beta feature flag on a project from the organization
680+
// this feature will eventually be made generally available and the feature flag will be removed
681+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
682+
throw Error(`This feature is currently unavailable`)
683+
}
684+
653685
if (alternativeNames !== undefined) {
654686
for (const d of alternativeNames) {
655687
if (d == route.domain) {
@@ -743,6 +775,12 @@ export const removeRouteAlternativeDomain: ResolverFn = async (
743775
project: projectData.id
744776
});
745777

778+
// this is used to set the beta feature flag on a project from the organization
779+
// this feature will eventually be made generally available and the feature flag will be removed
780+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
781+
throw Error(`This feature is currently unavailable`)
782+
}
783+
746784
const alternateDomain = await query(
747785
sqlClientPool,
748786
Sql.selectRouteAlternativeDomainsByDomainAndProjectID(domain, route.project)
@@ -804,6 +842,12 @@ export const addRouteAnnotation: ResolverFn = async (
804842
project: projectData.id
805843
});
806844

845+
// this is used to set the beta feature flag on a project from the organization
846+
// this feature will eventually be made generally available and the feature flag will be removed
847+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
848+
throw Error(`This feature is currently unavailable`)
849+
}
850+
807851
await Helpers(sqlClientPool).addRouteAnnotations(route.id, annotations)
808852

809853
await query(
@@ -865,6 +909,12 @@ export const removeRouteAnnotation: ResolverFn = async (
865909
project: projectData.id
866910
});
867911

912+
// this is used to set the beta feature flag on a project from the organization
913+
// this feature will eventually be made generally available and the feature flag will be removed
914+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
915+
throw Error(`This feature is currently unavailable`)
916+
}
917+
868918
await Helpers(sqlClientPool).deleteRouteAnnotation(route.id, key)
869919

870920
await query(
@@ -927,6 +977,12 @@ export const addPathRoutesToRoute: ResolverFn = async (
927977
project: projectData.id
928978
});
929979

980+
// this is used to set the beta feature flag on a project from the organization
981+
// this feature will eventually be made generally available and the feature flag will be removed
982+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
983+
throw Error(`This feature is currently unavailable`)
984+
}
985+
930986
let pr: PathRoutes = [];
931987

932988
pr = JSON.parse(route.pathRoutes)
@@ -1001,6 +1057,12 @@ export const removePathRouteFromRoute: ResolverFn = async (
10011057
project: projectData.id
10021058
});
10031059

1060+
// this is used to set the beta feature flag on a project from the organization
1061+
// this feature will eventually be made generally available and the feature flag will be removed
1062+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
1063+
throw Error(`This feature is currently unavailable`)
1064+
}
1065+
10041066
let pr: PathRoutes = [];
10051067

10061068
pr = JSON.parse(route.pathRoutes)
@@ -1064,6 +1126,12 @@ export const deleteRoute: ResolverFn = async (
10641126
project: projectData.id
10651127
});
10661128

1129+
// this is used to set the beta feature flag on a project from the organization
1130+
// this feature will eventually be made generally available and the feature flag will be removed
1131+
if (await projectHelpers(sqlClientPool).checkApiRoutesFeature(projectData.organization) === false) {
1132+
throw Error(`This feature is currently unavailable`)
1133+
}
1134+
10671135
if (!adminScopes.platformOwner) {
10681136
// general users can't delete routes with these sources from the api
10691137
if (route.source.toLowerCase() == RouteSource.AUTOGENERATED) {

services/api/src/typeDefs.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ const typeDefs = gql`
806806
autogeneratedRoutePrefixes: [String]
807807
autogeneratedPathRoutes: [AutogeneratedPathRoute]
808808
disableRequestVerification: Boolean
809+
featureApiRoutes: Boolean @deprecated(reason: "Beta API routes feature flag, will be generally available in a future release and this feature gate will be removed")
809810
}
810811
811812
"""
@@ -1313,6 +1314,7 @@ const typeDefs = gql`
13131314
retentionPolicies are the available retention policies to an organization
13141315
"""
13151316
retentionPolicies(type: RetentionPolicyType): [RetentionPolicy]
1317+
featureApiRoutes: Boolean @deprecated(reason: "Beta API routes feature flag, will be generally available in a future release and this feature gate will be removed")
13161318
}
13171319
13181320
input AddOrganizationInput {
@@ -1325,6 +1327,10 @@ const typeDefs = gql`
13251327
quotaNotification: Int
13261328
quotaEnvironment: Int
13271329
quotaRoute: Int
1330+
"""
1331+
@deprecated(reason: "Beta API routes feature flag, will be generally available in a future release and this feature gate will be removed")
1332+
"""
1333+
featureApiRoutes: Boolean
13281334
}
13291335
13301336
input DeleteOrganizationInput {
@@ -1340,6 +1346,10 @@ const typeDefs = gql`
13401346
quotaNotification: Int
13411347
quotaEnvironment: Int
13421348
quotaRoute: Int
1349+
"""
1350+
@deprecated(reason: "Beta API routes feature flag, will be generally available in a future release and this feature gate will be removed")
1351+
"""
1352+
featureApiRoutes: Boolean
13431353
}
13441354
13451355
input UpdateOrganizationInput {

0 commit comments

Comments
 (0)