Skip to content

Commit b65576f

Browse files
authored
feat: native composition report (#7007)
1 parent cefd6f2 commit b65576f

File tree

10 files changed

+294
-103
lines changed

10 files changed

+294
-103
lines changed

codegen.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ const config: CodegenConfig = {
3434
contextType: 'GraphQLModules.ModuleContext',
3535
enumValues: {
3636
ProjectType: '../shared/entities#ProjectType',
37-
NativeFederationCompatibilityStatus:
38-
'../shared/entities#NativeFederationCompatibilityStatus',
37+
NativeFederationCompatibilityStatusType:
38+
'../shared/entities#NativeFederationCompatibilityStatusType',
3939
TargetAccessScope: '../modules/auth/providers/scopes#TargetAccessScope',
4040
ProjectAccessScope: '../modules/auth/providers/scopes#ProjectAccessScope',
4141
OrganizationAccessScope: '../modules/auth/providers/scopes#OrganizationAccessScope',

packages/services/api/src/modules/auth/lib/authz.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,19 +281,25 @@ function isActionMatch(actionContainingWildcard: string, action: string) {
281281
if (actionContainingWildcard === '*') {
282282
return true;
283283
}
284+
284285
// exact match
285286
if (actionContainingWildcard === action) {
286287
return true;
287288
}
288289

289-
const [actionScope] = action.split(':');
290+
const [actionScope, actionId] = action.split(':');
290291
const [userSpecifiedActionScope, userSpecifiedActionId] = actionContainingWildcard.split(':');
291292

292293
// wildcard match "scope:*"
293294
if (actionScope === userSpecifiedActionScope && userSpecifiedActionId === '*') {
294295
return true;
295296
}
296297

298+
// wildcard match "*:scope"
299+
if (userSpecifiedActionScope === '*' && userSpecifiedActionId === actionId) {
300+
return true;
301+
}
302+
297303
return false;
298304
}
299305

@@ -500,7 +506,7 @@ type ActionDefinitionMap = {
500506
[key: `${string}:${string}`]: (args: any) => Array<string>;
501507
};
502508

503-
const actionDefinitions = {
509+
export const actionDefinitions = {
504510
...objectFromEntries(permissionsByLevel['organization'].map(t => [t.value, defaultOrgIdentity])),
505511
...objectFromEntries(permissionsByLevel['project'].map(t => [t.value, defaultProjectIdentity])),
506512
...objectFromEntries(permissionsByLevel['target'].map(t => [t.value, defaultTargetIdentity])),
@@ -514,7 +520,7 @@ const actionDefinitions = {
514520

515521
type Actions = keyof typeof actionDefinitions;
516522

517-
type ActionStrings = Actions | '*';
523+
type ActionStrings = Actions | '*' | '*:describe';
518524

519525
/** Unauthenticated session that is returned by default. */
520526
class UnauthenticatedSession extends Session {

packages/services/api/src/modules/auth/lib/supertokens-strategy.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ export class SuperTokensCookieBasedSession extends Session {
6767
organizationId,
6868
);
6969

70+
// Allow admins to use all describe actions within foreign organizations
71+
// This makes it much more pleasant to debug.
72+
if (user.isAdmin) {
73+
return [
74+
{
75+
action: '*:describe',
76+
effect: 'allow',
77+
resource: `hrn:${organizationId}:organization/${organizationId}`,
78+
},
79+
];
80+
}
81+
7082
return [];
7183
}
7284

packages/services/api/src/modules/project/providers/project-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export class ProjectManager {
198198
projectId: selector.projectId,
199199
},
200200
});
201+
201202
return this.storage.getProject(selector);
202203
}
203204

packages/services/api/src/modules/schema/module.graphql.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,40 @@ export default gql`
114114
externalSchemaComposition: ExternalSchemaComposition
115115
schemaVersionsCount(period: DateRangeInput): Int!
116116
isNativeFederationEnabled: Boolean!
117-
nativeFederationCompatibility: NativeFederationCompatibilityStatus!
117+
"""
118+
Get the status of the native federation compatability for the project.
119+
"""
120+
nativeFederationCompatibility: NativeCompositionCompatibility!
118121
}
119122
120123
extend type Target {
121124
schemaVersionsCount(period: DateRangeInput): Int!
122125
}
123126
124-
enum NativeFederationCompatibilityStatus {
127+
type NativeCompositionVersionStatus {
128+
"""
129+
The schema version we check against.
130+
"""
131+
schemaVersion: SchemaVersion!
132+
"""
133+
The native composition result. The supergraphSdl is sorted and normalized.
134+
"""
135+
nativeCompositionResult: SchemaCompositionResult!
136+
"""
137+
The supergraph of the latest valid schema version (sorted and normalized).
138+
"""
139+
currentSupergraphSdl: String!
140+
}
141+
142+
type NativeCompositionCompatibility {
143+
"""
144+
Whether the schema version is compatible.
145+
"""
146+
status: NativeFederationCompatibilityStatusType!
147+
results: [NativeCompositionVersionStatus]!
148+
}
149+
150+
enum NativeFederationCompatibilityStatusType {
125151
COMPATIBLE
126152
INCOMPATIBLE
127153
UNKNOWN

packages/services/api/src/modules/schema/providers/schema-manager.ts

Lines changed: 78 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SchemaChecksFilter } from '../../../__generated__/types';
1616
import * as GraphQLSchema from '../../../__generated__/types';
1717
import {
1818
DateRange,
19-
NativeFederationCompatibilityStatus,
19+
NativeFederationCompatibilityStatusType,
2020
Organization,
2121
Project,
2222
ProjectType,
@@ -1123,64 +1123,69 @@ export class SchemaManager {
11231123
return true;
11241124
}
11251125

1126-
async getNativeFederationCompatibilityStatus(project: Project) {
1126+
async getNativeFederationCompatibilityStatus(project: Project): Promise<{
1127+
status: NativeFederationCompatibilityStatusType;
1128+
results: Array<null | {
1129+
schemaVersion: SchemaVersion;
1130+
target: Target;
1131+
nativeCompositionResult: {
1132+
supergraphSdl: string | null;
1133+
errors: Array<{ message: string }> | null;
1134+
};
1135+
currentSupergraphSdl: string;
1136+
}>;
1137+
}> {
11271138
this.logger.debug(
11281139
'Get native Federation compatibility status (organization=%s, project=%s)',
11291140
project.orgId,
11301141
project.id,
11311142
);
11321143

11331144
if (project.type !== ProjectType.FEDERATION) {
1134-
return NativeFederationCompatibilityStatus.NOT_APPLICABLE;
1145+
return {
1146+
status: NativeFederationCompatibilityStatusType.NOT_APPLICABLE,
1147+
results: [],
1148+
};
11351149
}
11361150

11371151
const targets = await this.targetManager.getTargets({
11381152
organizationId: project.orgId,
11391153
projectId: project.id,
11401154
});
11411155

1142-
const possibleVersions = await Promise.all(
1143-
targets.map(target => this.getMaybeLatestValidVersion(target)),
1144-
);
1145-
1146-
const versions = possibleVersions.filter((v): v is SchemaVersion => !!v);
1147-
1148-
this.logger.debug('Found %s targets and %s versions', targets.length, versions.length);
1156+
const results = await Promise.all(
1157+
targets.map(async target => {
1158+
const schemaVersion = await this.getMaybeLatestValidVersion(target);
11491159

1150-
// If there are no composable versions available, we can't determine the compatibility status.
1151-
if (
1152-
versions.length === 0 ||
1153-
!versions.every(
1154-
version => version && version.isComposable && typeof version.supergraphSDL === 'string',
1155-
)
1156-
) {
1157-
this.logger.debug('No composable versions available (status: unknown)');
1158-
return NativeFederationCompatibilityStatus.UNKNOWN;
1159-
}
1160+
if (schemaVersion === null) {
1161+
return null;
1162+
}
11601163

1161-
const schemasPerVersion = await Promise.all(
1162-
versions.map(async version =>
1163-
this.getSchemasOfVersion({
1164-
organizationId: version.organizationId,
1165-
projectId: version.projectId,
1166-
targetId: version.targetId,
1167-
versionId: version.id,
1168-
}),
1169-
),
1170-
);
1164+
const currentSupergraphSdl = print(
1165+
removeDescriptions(
1166+
sortSDL(
1167+
parseGraphQLSource(
1168+
schemaVersion.supergraphSDL!,
1169+
'parsing native supergraph in getNativeFederationCompatibilityStatus',
1170+
),
1171+
),
1172+
),
1173+
);
11711174

1172-
this.logger.debug('Checking compatibility of %s versions', versions.length);
1175+
const schemas = await this.getSchemasOfVersion({
1176+
organizationId: target.orgId,
1177+
projectId: target.projectId,
1178+
targetId: target.id,
1179+
versionId: schemaVersion.id,
1180+
});
11731181

1174-
const compatibilityResults = await Promise.all(
1175-
versions.map(async (version, i) => {
1176-
if (schemasPerVersion[i].length === 0) {
1177-
this.logger.debug('No schemas (version=%s)', version.id);
1178-
return NativeFederationCompatibilityStatus.UNKNOWN;
1182+
if (schemas.length === 0) {
1183+
return null;
11791184
}
11801185

11811186
const compositionResult = await this.compositionOrchestrator.composeAndValidate(
11821187
'federation',
1183-
ensureCompositeSchemas(schemasPerVersion[i]).map(s =>
1188+
ensureCompositeSchemas(schemas).map(s =>
11841189
this.schemaHelper.createSchemaObject({
11851190
sdl: s.sdl,
11861191
service_name: s.service_name,
@@ -1196,53 +1201,52 @@ export class SchemaManager {
11961201
},
11971202
);
11981203

1199-
if (compositionResult.supergraph) {
1200-
const sortedExistingSupergraph = print(
1201-
removeDescriptions(
1202-
sortSDL(
1203-
parseGraphQLSource(
1204-
compositionResult.supergraph,
1205-
'parsing existing supergraph in getNativeFederationCompatibilityStatus',
1206-
),
1207-
),
1208-
),
1209-
);
1210-
const sortedNativeSupergraph = print(
1211-
removeDescriptions(
1212-
sortSDL(
1213-
parseGraphQLSource(
1214-
version.supergraphSDL!,
1215-
'parsing native supergraph in getNativeFederationCompatibilityStatus',
1204+
const supergraphSdl = compositionResult.supergraph
1205+
? print(
1206+
removeDescriptions(
1207+
sortSDL(
1208+
parseGraphQLSource(
1209+
compositionResult.supergraph,
1210+
'parsing native supergraph in getNativeFederationCompatibilityStatus',
1211+
),
12161212
),
12171213
),
1218-
),
1219-
);
1220-
1221-
if (sortedNativeSupergraph === sortedExistingSupergraph) {
1222-
return NativeFederationCompatibilityStatus.COMPATIBLE;
1223-
}
1224-
1225-
this.logger.debug('Produced different supergraph (version=%s)', version.id);
1226-
} else {
1227-
this.logger.debug('Failed to produce supergraph (version=%s)', version.id);
1228-
}
1214+
)
1215+
: null;
12291216

1230-
return NativeFederationCompatibilityStatus.INCOMPATIBLE;
1217+
return {
1218+
target,
1219+
schemaVersion,
1220+
currentSupergraphSdl,
1221+
nativeCompositionResult: {
1222+
supergraphSdl,
1223+
errors: compositionResult.errors,
1224+
},
1225+
};
12311226
}),
12321227
);
12331228

1234-
if (compatibilityResults.includes(NativeFederationCompatibilityStatus.UNKNOWN)) {
1235-
this.logger.debug('One of the versions seems empty (status: unknown)');
1236-
return NativeFederationCompatibilityStatus.UNKNOWN;
1237-
}
1229+
let status = NativeFederationCompatibilityStatusType.INCOMPATIBLE;
12381230

1239-
if (compatibilityResults.every(r => r === NativeFederationCompatibilityStatus.COMPATIBLE)) {
1231+
if (results.every(result => result === null)) {
1232+
this.logger.debug('No composable versions available (status: unknown)');
1233+
status = NativeFederationCompatibilityStatusType.UNKNOWN;
1234+
} else if (
1235+
results.every(
1236+
result =>
1237+
result === null ||
1238+
(result.nativeCompositionResult &&
1239+
result.currentSupergraphSdl === result.nativeCompositionResult.supergraphSdl),
1240+
)
1241+
) {
12401242
this.logger.debug('All versions are compatible (status: compatible)');
1241-
return NativeFederationCompatibilityStatus.COMPATIBLE;
1243+
status = NativeFederationCompatibilityStatusType.COMPATIBLE;
12421244
}
12431245

1244-
this.logger.debug('Some versions are incompatible (status: incompatible)');
1245-
return NativeFederationCompatibilityStatus.INCOMPATIBLE;
1246+
return {
1247+
status,
1248+
results,
1249+
};
12461250
}
12471251

12481252
async getGitHubMetadata(schemaVersion: SchemaVersion): Promise<null | {

packages/services/api/src/shared/entities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export enum ProjectType {
145145
SINGLE = 'SINGLE',
146146
}
147147

148-
export enum NativeFederationCompatibilityStatus {
148+
export enum NativeFederationCompatibilityStatusType {
149149
COMPATIBLE = 'COMPATIBLE',
150150
INCOMPATIBLE = 'INCOMPATIBLE',
151151
UNKNOWN = 'UNKNOWN',

0 commit comments

Comments
 (0)