Added Qovery integration

qovery-integration
Vladyslav Matsiiako 8 months ago
parent 64d5a82e1b
commit 0b59a92dfb

@ -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,317 @@ 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
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/organization/${orgId}/project`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryProject {
id: string;
name: string;
}
const 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 a specific 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
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/project/${projectId}/environment`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryEnvironment {
id: string;
name: string;
}
const 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 a specific 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
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/application`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryApp {
id: string;
name: string;
}
const 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 a specific 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
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/container`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryContainer {
id: string;
name: string;
}
const 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 a specific 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
);
const { data } = await standardRequest.get(
`${INTEGRATION_QOVERY_API_URL}/environment/${environmentId}/job`,
{
headers: {
Authorization: `Token ${accessToken}`,
"Accept": "application/json",
},
}
);
interface QoveryJob {
id: string;
name: string;
}
const 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]

@ -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,96 @@ 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.metadata?.scope?.toLowerCase()}/${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.metadata?.scope?.toLowerCase()}/${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.metadata?.scope?.toLowerCase()}/${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

@ -18,6 +18,7 @@ import {
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_QOVERY,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
@ -63,6 +64,7 @@ export interface IIntegration {
| "travisci"
| "supabase"
| "checkly"
| "qovery"
| "terraform-cloud"
| "teamcity"
| "hashicorp-vault"
@ -162,6 +164,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_QOVERY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT,

@ -8,4 +8,5 @@ export type Metadata = {
labelName: string;
labelValue: string;
}
scope?: "Job" | "Application" | "Container";
}

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

@ -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({

@ -82,7 +82,14 @@ export const CreateIntegrationV1 = z.object({
secretGCPLabel: z.object({
labelName: z.string(),
labelValue: z.string()
}).optional()
}).optional(),
org: z.string().optional(),
orgId: z.string().optional(),
project: z.string().optional(),
projectId: z.string().optional(),
environment: z.string().optional(),
environmentId: z.string().optional(),
scope: z.string().optional()
}).optional()
})
});

@ -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()

@ -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,
@ -93,6 +95,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";
@ -272,6 +275,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",

@ -4,6 +4,12 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## September
- Released an update to access controls; every user role now clearly defines and enforces a certain set of conditions across Infisical.
- Updated UI/UX for integrations.
- Added a native integration with [Qovery](https://infisical.com/docs/integrations/cloud/qovery).
## August 2023
- Release Audit Logs V2.

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

@ -0,0 +1,39 @@
---
title: "Qovery"
description: "How to sync secrets from Infisical to Qovery"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Enter your Qovery API Token
Obtain a Qovery API Token in Settings > API Token.
![integrations qovery api token](../../images/integrations-qovery-api-token.png)
Press on the Qovery tile and input your Qovery API TOken to grant Infisical access to your Qovery account.
![integrations qovery authorization](../../images/integrations-qovery-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it is necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to Qovery and press create integration to start syncing secrets.
![integrations Infisial settings](../../images/integrations-qovery-infisical.png)
Select your Qovery organization, project, and environment to which you want to sync secrets to. Next to that, select which scope you want secrets to (Application, Job, or Container). After you are done, hit "Create Integration."
![integrations Qovery settings](../../images/integrations-qovery-qovery.png)

@ -27,6 +27,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |
| [Checkly](/integrations/cloud/checkly) | Cloud | Available |
| [Qovery](/integrations/cloud/qovery) | Cloud | Available |
| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |

@ -235,6 +235,7 @@
"integrations/cloud/teamcity",
"integrations/cloud/cloudflare-pages",
"integrations/cloud/checkly",
"integrations/cloud/qovery",
"integrations/cloud/hashicorp-vault",
"integrations/cloud/azure-key-vault",
"integrations/cloud/gcp-secret-manager",

@ -19,6 +19,7 @@ const integrationSlugNameMapping: Mapping = {
"travisci": "TravisCI",
"supabase": "Supabase",
"checkly": "Checkly",
"qovery": "Qovery",
"terraform-cloud": "Terraform Cloud",
"teamcity": "TeamCity",
"hashicorp-vault": "Vault",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

@ -9,6 +9,8 @@ import {
Environment,
IntegrationAuth,
NorthflankSecretGroup,
Org,
Project,
Service,
Team,
TeamCityBuildConfig} from "./types";
@ -27,6 +29,31 @@ const integrationAuthKeys = {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({
integrationAuthId,
orgId
}: {
integrationAuthId: string;
orgId: string;
}) => [{ integrationAuthId, orgId }, "integrationAuthQoveryProjects"] as const,
getIntegrationAuthQoveryEnvironments: ({
integrationAuthId,
projectId
}: {
integrationAuthId: string;
projectId: string;
}) => [{ integrationAuthId, projectId }, "integrationAuthQoveryEnvironments"] as const,
getIntegrationAuthQoveryScopes: ({
integrationAuthId,
environmentId,
scope
}: {
integrationAuthId: string;
environmentId: string;
scope: "Job" | "Application" | "Container";
}) => [{ integrationAuthId, environmentId, scope }, "integrationAuthQoveryScopes"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
@ -120,6 +147,115 @@ const fetchIntegrationAuthVercelBranches = async ({
return branches;
};
const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
const {
data: { orgs }
} = await apiRequest.get<{ orgs: Org[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/orgs`
);
return orgs;
};
const fetchIntegrationAuthQoveryProjects = async ({
integrationAuthId,
orgId
}: {
integrationAuthId: string;
orgId: string;
}) => {
const {
data: { projects }
} = await apiRequest.get<{ projects: Project[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/projects`,
{
params: {
orgId
}
}
);
return projects;
};
const fetchIntegrationAuthQoveryEnvironments = async ({
integrationAuthId,
projectId
}: {
integrationAuthId: string;
projectId: string;
}) => {
const {
data: { environments }
} = await apiRequest.get<{ environments: Environment[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/environments`,
{
params: {
projectId
}
}
);
return environments;
};
const fetchIntegrationAuthQoveryScopes = async ({
integrationAuthId,
environmentId,
scope
}: {
integrationAuthId: string;
environmentId: string;
scope: "Job" | "Application" | "Container";
}) => {
if (scope === "Application") {
const {
data: { apps }
} = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/apps`,
{
params: {
environmentId
}
}
);
return apps;
}
if (scope === "Container") {
const {
data: { containers }
} = await apiRequest.get<{ containers: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/containers`,
{
params: {
environmentId
}
}
);
return containers;
}
if (scope === "Job") {
const {
data: { jobs }
} = await apiRequest.get<{ jobs: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/qovery/jobs`,
{
params: {
environmentId
}
}
);
return jobs;
}
return undefined;
};
const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId,
appId
@ -269,6 +405,82 @@ export const useGetIntegrationAuthVercelBranches = ({
});
};
export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),
queryFn: () =>
fetchIntegrationAuthQoveryOrgs(integrationAuthId),
enabled: true
});
};
export const useGetIntegrationAuthQoveryProjects = ({
integrationAuthId,
orgId
}: {
integrationAuthId: string;
orgId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryProjects({
integrationAuthId,
orgId
}),
queryFn: () =>
fetchIntegrationAuthQoveryProjects({
integrationAuthId,
orgId
}),
enabled: true
});
};
export const useGetIntegrationAuthQoveryEnvironments = ({
integrationAuthId,
projectId
}: {
integrationAuthId: string;
projectId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryEnvironments({
integrationAuthId,
projectId
}),
queryFn: () =>
fetchIntegrationAuthQoveryEnvironments({
integrationAuthId,
projectId
}),
enabled: true
});
};
export const useGetIntegrationAuthQoveryScopes = ({
integrationAuthId,
environmentId,
scope
}: {
integrationAuthId: string;
environmentId: string;
scope: "Job" | "Application" | "Container";
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryScopes({
integrationAuthId,
environmentId,
scope
}),
queryFn: () =>
fetchIntegrationAuthQoveryScopes({
integrationAuthId,
environmentId,
scope
}),
enabled: true
});
};
export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId,
appId

@ -26,6 +26,21 @@ export type Environment = {
environmentId: string;
};
export type Container = {
name: string;
containerId: string;
};
export type Org = {
name: string;
orgId: string;
};
export type Project = {
name: string;
projectId: string;
};
export type Service = {
name: string;
serviceId: string;

@ -59,6 +59,13 @@ export const useCreateIntegration = () => {
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
org?: string;
orgId?: string;
project?: string;
projectId?: string;
environment?: string;
environmentId?: string;
scope?: string;
}
}) => {
const { data: { integration } } = await apiRequest.post("/api/v1/integration", {

@ -32,5 +32,9 @@ export type TIntegration = {
__v: number;
metadata?: {
secretSuffix?: string;
scope: string;
org: string;
project: string;
environment: string;
}
};

@ -0,0 +1,106 @@
import { useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
useSaveIntegrationAccessToken
} from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
export default function QoveryCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useSaveIntegrationAccessToken();
const [accessToken, setAccessToken] = useState("");
const [accessTokenErrorText, setAccessTokenErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setAccessTokenErrorText("");
if (accessToken.length === 0) {
setAccessTokenErrorText("Access token cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "qovery",
accessToken
});
setIsLoading(false);
router.push(`/integrations/qovery/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Authorize Qovery Integration</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600 mb-12">
<CardTitle
className="text-left px-6 text-xl"
subTitle="After adding your API key, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5">
<Image
src="/images/integrations/Qovery.png"
height={30}
width={30}
alt="Qovery logo"
/>
</div>
<span className="ml-2.5">Qovery Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/qovery" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5"/>
Docs
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="ml-1.5 text-xxs mb-[0.07rem]"/>
</div>
</a>
</Link>
</div>
</CardTitle>
<FormControl
label="Qovery API token"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== "" ?? false}
className="mx-6"
>
<Input
placeholder=""
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
colorSchema="primary"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isFullWidth={false}
isLoading={isLoading}
>
Connect to Qovery
</Button>
</Card>
</div>
);
}
QoveryCreateIntegrationPage.requireAuth = true;

@ -0,0 +1,403 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import {
useCreateIntegration
} from "@app/hooks/api";
import { useGetIntegrationAuthQoveryEnvironments, useGetIntegrationAuthQoveryOrgs, useGetIntegrationAuthQoveryProjects, useGetIntegrationAuthQoveryScopes } from "@app/hooks/api/integrationAuth/queries";
import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
enum TabSections {
InfisicalSettings = "infisicalSettings",
QoverySettings = "qoverySettings"
}
export default function QoveryCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const [scope, setScope] = useState("Application");
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const { data: integrationAuthOrgs } = useGetIntegrationAuthQoveryOrgs((integrationAuthId as string) ?? "");
const [targetOrg, setTargetOrg] = useState("");
const [targetOrgId, setTargetOrgId] = useState("");
const { data: integrationAuthProjects } = useGetIntegrationAuthQoveryProjects({
integrationAuthId: (integrationAuthId as string) ?? "",
orgId: targetOrgId
});
const [targetProject, setTargetProject] = useState("");
const [targetProjectId, setTargetProjectId] = useState("");
const { data: integrationAuthEnvironments } = useGetIntegrationAuthQoveryEnvironments({
integrationAuthId: (integrationAuthId as string) ?? "",
projectId: targetProjectId
});
const [targetEnvironment, setTargetEnvironment] = useState("");
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthQoveryScopes({
integrationAuthId: (integrationAuthId as string) ?? "",
environmentId: targetEnvironmentId,
scope: (scope as ("Job" | "Application" | "Container"))
});
const [targetApp, setTargetApp] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [isLoading, setIsLoading] = useState(false);
const scopes = ["Application", "Container", "Job"];
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
setTargetAppId(String(integrationAuthApps[0].appId));
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(String(integrationAuthApps.filter(app => app.name === targetApp)[0].appId));
}
}
}, [targetApp]);
useEffect(() => {
if (integrationAuthOrgs) {
if (integrationAuthOrgs.length > 0) {
setTargetOrg(integrationAuthOrgs[0].name);
setTargetOrgId(String(integrationAuthOrgs[0].orgId));
} else {
setTargetOrg("none");
}
}
}, [integrationAuthOrgs]);
useEffect(() => {
if (integrationAuthProjects) {
if (integrationAuthProjects.length > 0) {
setTargetProject(integrationAuthProjects[0].name);
setTargetProjectId(String(integrationAuthProjects[0].projectId));
} else {
setTargetProject("none");
}
}
}, [integrationAuthProjects]);
useEffect(() => {
if (integrationAuthEnvironments) {
if (integrationAuthEnvironments.length > 0) {
setTargetEnvironment(integrationAuthEnvironments[0].name);
setTargetEnvironmentId(String(integrationAuthEnvironments[0].environmentId));
} else {
setTargetEnvironment("none");
}
}
}, [integrationAuthEnvironments]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
secretPath,
metadata: {
scope,
org: targetOrg,
orgId: targetOrgId,
project: targetProject,
projectId: targetProjectId,
environment: targetEnvironment,
environmentId: targetEnvironmentId,
}
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment ? (
<div className="flex flex-col h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
<Head>
<title>Set Up Qovery Integration</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle
className="text-left px-6 text-xl"
subTitle="Choose which environment in Infisical you want to sync to Checkly environment variables."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5">
<Image
src="/images/integrations/Qovery.png"
height={30}
width={30}
alt="Qovery logo"
/>
</div>
<span className="ml-2.5">Qovery Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/qovery" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5"/>
Docs
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="ml-1.5 text-xxs mb-[0.07rem]"/>
</div>
</a>
</Link>
</div>
</CardTitle>
<Tabs defaultValue={TabSections.InfisicalSettings} className="px-6">
<TabList>
<div className="flex flex-row border-b border-mineshaft-600 w-full">
<Tab value={TabSections.InfisicalSettings}>Infisical Settings</Tab>
<Tab value={TabSections.QoverySettings}>Qovery Settings</Tab>
</div>
</TabList>
<TabPanel value={TabSections.InfisicalSettings}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Infisical Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path" className="pb-[14.68rem]">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.QoverySettings}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Qovery Scope">
<Select
value={scope}
onValueChange={(val) => setScope(val)}
className="w-full border border-mineshaft-500"
>
{scopes.map((tempScope) => (
<SelectItem
value={tempScope}
key={`target-app-${tempScope}`}
>
{tempScope}
</SelectItem>
))}
</Select>
</FormControl>
{integrationAuthOrgs && <FormControl label="Qovery Organization">
<Select
value={targetOrg}
onValueChange={(val) => setTargetOrg(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthOrgs.length === 0}
>
{integrationAuthOrgs.length > 0 ? (
integrationAuthOrgs.map((integrationAuthOrg) => (
<SelectItem
value={integrationAuthOrg.name}
key={`target-app-${integrationAuthOrg.name}`}
>
{integrationAuthOrg.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No organizaztions found
</SelectItem>
)}
</Select>
</FormControl>}
{integrationAuthProjects && <FormControl label="Qovery Project">
<Select
value={targetProject}
onValueChange={(val) => setTargetProject(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthProjects.length === 0}
>
{integrationAuthProjects.length > 0 ? (
integrationAuthProjects.map((integrationAuthProject) => (
<SelectItem
value={integrationAuthProject.name}
key={`target-app-${integrationAuthProject.name}`}
>
{integrationAuthProject.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>}
{integrationAuthEnvironments && <FormControl label="Qovery Environment">
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironment(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthEnvironments.length === 0}
>
{integrationAuthEnvironments.length > 0 ? (
integrationAuthEnvironments.map((integrationAuthEnvironment) => (
<SelectItem
value={integrationAuthEnvironment.name}
key={`target-app-${integrationAuthEnvironment.name}`}
>
{integrationAuthEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No environments found
</SelectItem>
)}
</Select>
</FormControl>}
{(scope && integrationAuthApps) && <FormControl label={`Qovery ${scope.charAt(0).toUpperCase() + scope.slice(1)}`}>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No {scope.toLowerCase()}s found
</SelectItem>
)}
</Select>
</FormControl>}
</motion.div>
</TabPanel>
</Tabs>
<Button
onClick={handleButtonClick}
color="mineshaft"
variant="outline_bg"
className="mb-6 ml-auto mr-6"
isFullWidth={false}
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tips</span></div>
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in Qovery with secrets from Infisical.</span>
</div> */}
</div>
) : (
<div className="flex justify-center items-center w-full h-full">
<Head>
<title>Set Up Qovery Integration</title>
<link rel='icon' href='/infisical.ico' />
</Head>
{isIntegrationAuthAppsLoading ? <img src="/images/loading/loading.gif" height={70} width={120} alt="infisical loading indicator" /> : <div className="max-w-md h-max p-6 border border-mineshaft-600 rounded-md bg-mineshaft-800 text-mineshaft-200 flex flex-col text-center">
<FontAwesomeIcon icon={faBugs} className="text-6xl my-2 inlineli"/>
<p>
Something went wrong. Please contact <a
className="inline underline underline-offset-4 decoration-primary-500 opacity-80 hover:opacity-100 text-mineshaft-100 duration-200 cursor-pointer"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@infisical.com"
>
support@infisical.com
</a> if the issue persists.
</p>
</div>}
</div>
);
}
QoveryCreateIntegrationPage.requireAuth = true;

@ -87,6 +87,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "qovery":
link = `${window.location.origin}/integrations/qovery/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;

@ -96,8 +96,30 @@ export const IntegrationsSection = ({
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
{(integration.integration === "qovery") && (
<div className="flex flex-row">
<div className="ml-2 flex flex-col">
<FormLabel label="Org" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.org || "-"}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Project" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.project || "-"}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Env" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.environment || "-"}
</div>
</div>
</div>
)}
<div className="ml-2 flex flex-col">
<FormLabel label="App" />
<FormLabel label={integration?.metadata?.scope || "App"} />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
@ -131,7 +153,7 @@ export const IntegrationsSection = ({
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Integrations}
>
{(isAllowed) => (
{(isAllowed: boolean) => (
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton

Loading…
Cancel
Save