Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new project and group importer for organizations #3598

Merged
merged 2 commits into from Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion services/api/src/models/group.ts
Expand Up @@ -137,7 +137,7 @@ export const Group = (clients: {
path: keycloakGroup.path,
attributes: keycloakGroup.attributes,
subGroups: keycloakGroup.subGroups,
organization: parseInt(attributeKVOrNull('lagoon-organization', keycloakGroup)),
organization: parseInt(attributeKVOrNull('lagoon-organization', keycloakGroup), 10) || null, // if it exists set it or null
})
);

Expand Down
4 changes: 4 additions & 0 deletions services/api/src/resolvers.js
Expand Up @@ -269,6 +269,8 @@ const {
getNotificationsForOrganizationProjectId,
getEnvironmentsByOrganizationId,
removeUserFromOrganizationGroups,
checkBulkImportProjectsAndGroupsToOrganization,
bulkImportProjectsAndGroupsToOrganization
} = require('./resources/organization/resolvers');

const {
Expand Down Expand Up @@ -582,6 +584,7 @@ const resolvers = {
getGroupProjectOrganizationAssociation,
getProjectGroupOrganizationAssociation,
getEnvVariablesByProjectEnvironmentName,
checkBulkImportProjectsAndGroupsToOrganization
},
Mutation: {
addProblem,
Expand Down Expand Up @@ -714,6 +717,7 @@ const resolvers = {
removeDeployTargetFromOrganization,
updateEnvironmentDeployTarget,
removeUserFromOrganizationGroups,
bulkImportProjectsAndGroupsToOrganization
},
Subscription: {
backupChanged: backupSubscriber,
Expand Down
224 changes: 224 additions & 0 deletions services/api/src/resources/organization/resolvers.ts
Expand Up @@ -1043,3 +1043,227 @@ export const deleteOrganization: ResolverFn = async (
});
return 'success';
};

const checkBulkProjectGroupAssociation = async (oid, pid, projectsToMove, groupsToMove, projectsInOtherOrgs, groupsInOtherOrgs, sqlClientPool, models, keycloakGroups) => {
const groupProjectIds = [];
const projectGroups = await models.GroupModel.loadGroupsByProjectIdFromGroups(pid, keycloakGroups);
// get all the groups the requested project is in
for (const group of projectGroups) {
// for each group the project is in, get the list of projects that are also in this group
if (R.prop('lagoon-projects', group.attributes)) {
const groupProjects = R.prop('lagoon-projects', group.attributes).toString().split(',')
for (const project of groupProjects) {
groupProjectIds.push({group: group.name, project: project})
}
}
}

// for all the projects in the first projects group, iterate through the projects and the groups attached
// to these projects and try to build out a map of all the groups and projects that are linked by the primary project
if (groupProjectIds.length > 0) {
for (const pGroup of groupProjectIds) {
const project = await projectHelpers(sqlClientPool).getProjectById(pGroup.project)
const projectGroups = await models.GroupModel.loadGroupsByProjectIdFromGroups(project.id, keycloakGroups);
// check if the project is already in the requested organization
if (project.organization != oid && project.organization == null) {
let alreadyAdded = false
for (const f of projectsToMove) {
if (f.id == project.id) {
alreadyAdded = true
}
}
if (!alreadyAdded) {
// if it isn't already in the requested organization, add it to the list of projects that should be moved
projectsToMove.push(project)
}
} else {
// if the project is in a completely different organization
if (project.organization != oid) {
let alreadyAdded = false
for (const f of projectsInOtherOrgs) {
if (f.id == project.id) {
alreadyAdded = true
}
}
if (!alreadyAdded) {
// add it to the lsit of projects that will cause this check to fail
projectsInOtherOrgs.push(project)
}
}
}
for (const group of projectGroups) {
// for every group that the project is in, check if the group is already in the requested organization
if (group.organization != oid && group.organization == null) {
let alreadyAdded = false
for (const f of groupsToMove) {
if (f.id == group.id) {
alreadyAdded = true
}
}
if (!alreadyAdded) {
// if it isn't already in the requested organization, add it to the list of groups that should be moved
groupsToMove.push(group)
}
} else {
// if the group is in a completely different organization
if (group.organization != oid) {
let alreadyAdded = false
for (const f of groupsInOtherOrgs) {
if (f.id == group.id) {
alreadyAdded = true
}
}
if (!alreadyAdded) {
// add it to the lsit of projects that will cause this check to fail
groupsInOtherOrgs.push(group)
}
}
}
}
}
}
}

export const checkBulkImportProjectsAndGroupsToOrganization: ResolverFn = async (
_root,
{ input },
{ sqlClientPool, models, hasPermission, keycloakGroups }
) => {
let pid = input.project;
let oid = input.organization;

// platform admin only as it potentially reveals information about projects/orgs/groups
await hasPermission('organization', 'add');

const projectsToMove = []
const groupsToMove = []
const projectsInOtherOrgs = []
const groupsInOtherOrgs = []

// get all the groups the requested project is in
await checkBulkProjectGroupAssociation(oid, pid, projectsToMove, groupsToMove, projectsInOtherOrgs, groupsInOtherOrgs, sqlClientPool, models, keycloakGroups)

return { projects: projectsToMove, groups: groupsToMove, otherOrgProjects: projectsInOtherOrgs, otherOrgGroups: groupsInOtherOrgs };
};

// given a project, collect all the groups that this project has, and all the projects that those groups have and their associated projects
// and import them into the given organization
export const bulkImportProjectsAndGroupsToOrganization: ResolverFn = async (
root,
{ input, detachNotifications },
{ sqlClientPool, hasPermission, userActivityLogger, models, keycloakGroups }
) => {

let pid = input.project;
let oid = input.organization;

// platform admin only as it potentially reveals information about projects/orgs/groups
await hasPermission('organization', 'add');

const projectsToMove = []
const groupsToMove = []
const projectsInOtherOrgs = []
const groupsInOtherOrgs = []

// get all the groups the requested project is in
await checkBulkProjectGroupAssociation(oid, pid, projectsToMove, groupsToMove, projectsInOtherOrgs, groupsInOtherOrgs, sqlClientPool, models, keycloakGroups)

// if anything comes back in projectsInOtherOrgs or groupsInOtherOrgs, then this mutation should fail and inform the user
// to run the query first and return the fields that contain information about why it can't move the projects
if (projectsInOtherOrgs.length > 0 || groupsInOtherOrgs.length > 0) {
throw new Error(
`The process detected projects or groups that are in another organization already, you should run checkBulkImportProjectsAndGroupsToOrganization and return otherOrgProjects and otherOrgGroups fields`
)
}

// update all projects to be in the organization
const groupsDone = [];
const projectsDone = [];
for (const group of groupsToMove) {
// update the groups of the project to be in the organization
if (!groupsDone.includes(group.id)) {
if (group.organization != oid && group.organization == null) {
await models.GroupModel.updateGroup({
id: group.id,
name: group.name,
attributes: {
...group.attributes,
"lagoon-organization": [input.organization]
}
});
groupsDone.push(group.id)

// log this activity
userActivityLogger(`User added a group to organization`, {
project: '',
organization: input.organization,
event: 'api:addGroupToOrganization',
payload: {
data: {
group: group.name,
organization: oid
}
}
});
}
}
}
for (const project of projectsToMove) {
if (!projectsDone.includes(project.id)) {
if (project.organization != oid && project.organization == null) {
if (detachNotifications) {
// remove all notifications from projects before adding them to the organizations
try {
await notificationHelpers(sqlClientPool).removeAllNotificationsFromProject({project: project.id})
userActivityLogger(`User removed all notifications from project`, {
project: '',
organization: input.organization,
event: 'api:removeNotificationsFromProject',
payload: {
data: {
project: project.id,
patch:{
organization: oid,
}
}
}
});
} catch (err) {
throw new Error(
`Unable to remove all notifications from the project`
)
}
}

// set project.organization
await query(
sqlClientPool,
Sql.updateProjectOrganization({
pid: project.id,
patch:{
organization: oid,
}
})
);
projectsDone.push(project.id)

// log this activity
userActivityLogger(`User added a project to organization`, {
project: '',
organization: input.organization,
event: 'api:addExistingProjectToOrganization',
payload: {
data: {
project: project.id,
patch:{
organization: oid,
}
}
}
});
}
}
}

return { projects: projectsToMove, groups: groupsToMove, otherOrgProjects: projectsInOtherOrgs, otherOrgGroups: groupsInOtherOrgs };
}
25 changes: 20 additions & 5 deletions services/api/src/typeDefs.js
Expand Up @@ -1417,9 +1417,17 @@ const typeDefs = gql`
"""
organizationById(id: Int!): Organization
organizationByName(name: String!): Organization
getGroupProjectOrganizationAssociation(input: AddGroupToOrganizationInput!): String
getProjectGroupOrganizationAssociation(input: ProjectOrgGroupsInput!): String
getGroupProjectOrganizationAssociation(input: AddGroupToOrganizationInput!): String @deprecated(reason: "Use checkBulkImportProjectsAndGroupsToOrganization instead")
getProjectGroupOrganizationAssociation(input: ProjectOrgGroupsInput!): String @deprecated(reason: "Use checkBulkImportProjectsAndGroupsToOrganization instead")
getEnvVariablesByProjectEnvironmentName(input: EnvVariableByProjectEnvironmentNameInput!): [EnvKeyValue]
checkBulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput!): ProjectGroupsToOrganization
}

type ProjectGroupsToOrganization {
projects: [Project]
groups: [GroupInterface]
otherOrgProjects: [Project]
otherOrgGroups: [GroupInterface]
}

# Must provide id OR name
Expand Down Expand Up @@ -2425,15 +2433,15 @@ const typeDefs = gql`
"""
Add a group to an organization
"""
addGroupToOrganization(input: AddGroupToOrganizationInput!): OrgGroupInterface
addGroupToOrganization(input: AddGroupToOrganizationInput!): OrgGroupInterface @deprecated(reason: "Use bulkImportProjectsAndGroupsToOrganization instead")
"""
Add an existing group to an organization
"""
addExistingGroupToOrganization(input: AddGroupToOrganizationInput!): OrgGroupInterface
addExistingGroupToOrganization(input: AddGroupToOrganizationInput!): OrgGroupInterface @deprecated(reason: "Use bulkImportProjectsAndGroupsToOrganization instead")
"""
Add an existing project to an organization
"""
addExistingProjectToOrganization(input: AddProjectToOrganizationInput): Project
addExistingProjectToOrganization(input: AddProjectToOrganizationInput): Project @deprecated(reason: "Use bulkImportProjectsAndGroupsToOrganization instead")
"""
Remove a project from an organization, this will return the project to a state where it has no groups or notifications associated to it
"""
Expand All @@ -2446,6 +2454,13 @@ const typeDefs = gql`
Remove a deploytarget from an organization
"""
removeDeployTargetFromOrganization(input: RemoveDeployTargetFromOrganizationInput): Organization
"""
Run the query getProjectGroupOrganizationAssociation first to see the changes that would be made before executing this, as it may contain undesirable changes
shreddedbacon marked this conversation as resolved.
Show resolved Hide resolved
Add an existing project to an organization, this will include all the groups and all the projects that those groups contain
Optionally detach any notifications attached to the projects, they will be need to be recreated within the organization afterwards
This mutation performs a lot of actions, on big project and group imports, if it times out, subsequent runs will perform only the changes necessary
"""
bulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput, detachNotification: Boolean): ProjectGroupsToOrganization
}

type Subscription {
Expand Down