Merge pull request #998 from Infisical/qovery-integration

Added Qovery integration
This commit is contained in:
vmatsiiako
2023-09-22 12:44:06 -07:00
committed by GitHub
29 changed files with 1401 additions and 7 deletions

View File

@@ -12,6 +12,7 @@ import {
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL,
@@ -344,6 +345,362 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
});
};
/**
* Return list of Qovery Orgs for a specific user
* @param req
* @param res
*/
export const getIntegrationAuthQoveryOrgs = async (req: Request, res: Response) => {
const {
params: { integrationAuthId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryOrgsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/organization`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryOrg {
id: string;
name: string;
}
const orgs = data.results.map((a: QoveryOrg) => {
return {
name: a.name,
orgId: a.id,
};
});
return res.status(200).send({
orgs
});
};
/**
* Return list of Qovery Projects for a specific orgId
* @param req
* @param res
*/
export const getIntegrationAuthQoveryProjects = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { orgId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryProjectsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Project {
name: string;
projectId: string;
}
interface QoveryProject {
id: string;
name: string;
}
let projects: Project[] = [];
if (orgId && orgId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/organization/${orgId}/project`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
projects = data.results.map((a: QoveryProject) => {
return {
name: a.name,
projectId: a.id,
};
});
}
return res.status(200).send({
projects
});
};
/**
* Return list of Qovery environments for project with id [projectId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryEnvironments = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { projectId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryEnvironmentsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Environment {
name: string;
environmentId: string;
}
interface QoveryEnvironment {
id: string;
name: string;
}
let environments: Environment[] = [];
if (projectId && projectId !== "" && projectId !== "none") { // TODO: fix
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/project/${projectId}/environment`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
environments = data.results.map((a: QoveryEnvironment) => {
return {
name: a.name,
environmentId: a.id,
};
});
}
return res.status(200).send({
environments
});
};
/**
* Return list of Qovery apps for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryApps = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface App {
name: string;
appId: string;
}
interface QoveryApp {
id: string;
name: string;
}
let apps: App[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/application`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
apps = data.results.map((a: QoveryApp) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
apps
});
};
/**
* Return list of Qovery containers for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryContainers = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Container {
name: string;
appId: string;
}
interface QoveryContainer {
id: string;
name: string;
}
let containers: Container[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/container`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
containers = data.results.map((a: QoveryContainer) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
containers
});
};
/**
* Return list of Qovery jobs for environment with id [environmentId]
* @param req
* @param res
*/
export const getIntegrationAuthQoveryJobs = async (req: Request, res: Response) => {
const {
params: { integrationAuthId },
query: { environmentId }
} = await validateRequest(reqValidator.GetIntegrationAuthQoveryScopesV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
interface Job {
name: string;
appId: string;
}
interface QoveryJob {
id: string;
name: string;
}
let jobs: Job[] = [];
if (environmentId && environmentId !== "") {
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/job`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
jobs = data.results.map((a: QoveryJob) => {
return {
name: a.name,
appId: a.id,
};
});
}
return res.status(200).send({
jobs
});
};
/**
* Return list of Railway environments for Railway project with
* id [appId]

View File

@@ -34,6 +34,7 @@ export const createIntegration = async (req: Request, res: Response) => {
appId,
owner,
region,
scope,
targetService,
targetServiceId,
integrationAuthId,
@@ -42,7 +43,7 @@ export const createIntegration = async (req: Request, res: Response) => {
metadata
}
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>("workspace")
.select(
@@ -90,6 +91,7 @@ export const createIntegration = async (req: Request, res: Response) => {
owner,
path,
region,
scope,
secretPath,
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),

View File

@@ -40,6 +40,8 @@ import {
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER,
@@ -219,6 +221,13 @@ const syncSecrets = async ({
accessToken
});
break;
case INTEGRATION_QOVERY:
await syncSecretsQovery({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({
integration,
@@ -2126,6 +2135,97 @@ const syncSecretsCheckly = async ({
}
};
/**
* Sync/push [secrets] to Qovery app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Qovery integration
*/
const syncSecretsQovery = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, {
headers: {
Authorization: `Token ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.results.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.key]: {"id": secret.id, "value": secret.value}
}),
{}
);
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in qovery
// -> add secret
await standardRequest.post(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
}
);
} else {
// case: secret exists in qovery
// -> update/set secret
if (secrets[key].value !== getSecretsRes[key].value) {
await standardRequest.put(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
}
}
}
// This one is dangerous because there might be a lot of qovery-specific secrets
// for await (const key of Object.keys(getSecretsRes)) {
// if (!(key in secrets)) {
// console.log(3)
// // delete secret
// await standardRequest.delete(`${INTEGRATION_QOVERY_API_URL}/application/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`, {
// headers: {
// Authorization: `Token ${accessToken}`,
// Accept: "application/json",
// "X-Qovery-Account": integration.appId
// }
// });
// }
// }
};
/**
* Sync/push [secrets] to Terraform Cloud project with id [integration.appId]
* @param {Object} obj

View File

@@ -18,6 +18,7 @@ import {
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_QOVERY,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
@@ -45,6 +46,7 @@ export interface IIntegration {
targetServiceId: string;
path: string;
region: string;
scope: string;
secretPath: string;
integration:
| "azure-key-vault"
@@ -63,6 +65,7 @@ export interface IIntegration {
| "travisci"
| "supabase"
| "checkly"
| "qovery"
| "terraform-cloud"
| "teamcity"
| "hashicorp-vault"
@@ -119,11 +122,13 @@ const integrationSchema = new Schema<IIntegration>(
},
targetService: {
// railway-specific service
// qovery-specific project
type: String,
default: null,
},
targetServiceId: {
// railway-specific service
// qovery specific project
type: String,
default: null,
},
@@ -143,6 +148,11 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null,
},
scope: {
// qovery-specific scope
type: String,
default: null
},
integration: {
type: String,
enum: [
@@ -162,6 +172,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_QOVERY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT,

View File

@@ -1,6 +1,3 @@
// TODO: in the future separate metadata
// into distinct types by integration
export type Metadata = {
secretPrefix?: string;
secretSuffix?: string;

View File

@@ -52,6 +52,7 @@ import {
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "qovery"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"

View File

@@ -60,6 +60,54 @@ router.get(
integrationAuthController.getIntegrationAuthVercelBranches
);
router.get(
"/:integrationAuthId/qovery/orgs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryOrgs
);
router.get(
"/:integrationAuthId/qovery/projects",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryProjects
);
router.get(
"/:integrationAuthId/qovery/environments",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryEnvironments
);
router.get(
"/:integrationAuthId/qovery/apps",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryApps
);
router.get(
"/:integrationAuthId/qovery/containers",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryContainers
);
router.get(
"/:integrationAuthId/qovery/jobs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
integrationAuthController.getIntegrationAuthQoveryJobs
);
router.get(
"/:integrationAuthId/railway/environments",
requireAuth({

View File

@@ -76,13 +76,14 @@ export const CreateIntegrationV1 = z.object({
owner: z.string().trim().optional(),
path: z.string().trim().optional(),
region: z.string().trim().optional(),
scope: z.string().trim().optional(),
metadata: z.object({
secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(),
secretGCPLabel: z.object({
labelName: z.string(),
labelValue: z.string()
}).optional()
}).optional(),
}).optional()
})
});

View File

@@ -113,6 +113,39 @@ export const GetIntegrationAuthVercelBranchesV1 = z.object({
})
});
export const GetIntegrationAuthQoveryOrgsV1 = z.object({
params: z.object({
integrationAuthId: z.string().trim()
})
});
export const GetIntegrationAuthQoveryProjectsV1 = z.object({
params: z.object({
integrationAuthId: z.string().trim()
}),
query: z.object({
orgId: z.string().trim()
})
});
export const GetIntegrationAuthQoveryEnvironmentsV1 = z.object({
params: z.object({
integrationAuthId: z.string().trim()
}),
query: z.object({
projectId: z.string().trim()
})
});
export const GetIntegrationAuthQoveryScopesV1 = z.object({
params: z.object({
integrationAuthId: z.string().trim()
}),
query: z.object({
environmentId: z.string().trim()
})
});
export const GetIntegrationAuthRailwayEnvironmentsV1 = z.object({
params: z.object({
integrationAuthId: z.string().trim()

View File

@@ -28,6 +28,7 @@ export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_TEAMCITY = "teamcity";
export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_QOVERY = "qovery";
export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
@@ -53,6 +54,7 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_QOVERY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
@@ -94,6 +96,7 @@ export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_QOVERY_API_URL = "https://api.qovery.com";
export const INTEGRATION_TERRAFORM_CLOUD_API_URL = "https://app.terraform.io";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org";
@@ -273,6 +276,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: "",
},
{
name: "Qovery",
slug: "qovery",
image: "Qovery.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
{
name: "HashiCorp Vault",
slug: "hashicorp-vault",