Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-09-25 13:26:09 +01:00
45 changed files with 1542 additions and 102 deletions

View File

@@ -17,9 +17,9 @@ jobs:
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2

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]
@@ -713,11 +1070,12 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
*/
export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => {
const {
params: { integrationAuthId, appId }
params: { integrationAuthId },
query: { appId }
} = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1, req);
// TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions
const { integrationAuth } = await getIntegrationAuthAccessHelper({
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
integrationAuthId: new ObjectId(integrationAuthId)
});
@@ -749,13 +1107,13 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res:
const {
data: { buildType }
} = await standardRequest.get<GetTeamCityBuildConfigsRes>(
`${req.integrationAuth.url}/app/rest/buildTypes`,
`${integrationAuth.url}/app/rest/buildTypes`,
{
params: {
locator: `project:${appId}`
},
headers: {
Authorization: `Bearer ${req.accessToken}`,
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}

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

@@ -15,6 +15,7 @@ import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update {
workspace: string;
integration: string;
url?: string;
teamId?: string;
accountId?: string;
metadata?: IntegrationAuthMetadata
@@ -63,6 +64,10 @@ export const handleOAuthExchangeHelper = async ({
workspace: workspaceId,
integration
};
if (res.url) {
update.url = res.url;
}
switch (integration) {
case INTEGRATION_VERCEL:
@@ -160,7 +165,7 @@ export const getIntegrationAuthAccessHelper = async ({
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata teamId"
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag metadata teamId url"
);
if (!integrationAuth)

View File

@@ -423,6 +423,7 @@ const exchangeCodeGitlab = async ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
url
};
};

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,
@@ -941,7 +950,11 @@ const syncSecretsVercel = async ({
? {
teamId: integrationAuth.teamId
}
: {})
: {}),
...(integration?.path
? {
gitBranch: integration?.path
} : {})
};
const vercelSecrets: VercelSecret[] = (
@@ -960,7 +973,7 @@ const syncSecretsVercel = async ({
if (
integration.targetEnvironment === "preview" &&
integration.path &&
secret.gitBranch &&
integration.path !== secret.gitBranch
) {
// case: secret on preview environment does not have same target git branch
@@ -969,7 +982,7 @@ const syncSecretsVercel = async ({
return true;
});
const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) {
@@ -2126,6 +2139,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

@@ -21,9 +21,8 @@ import {
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { ACCEPTED, INVITED, MEMBER } from "../variables";
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
import { standardRequest } from "../config/request";
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -146,11 +145,27 @@ const initializePassport = async () => {
clientID: clientIdGitHubLogin,
clientSecret: clientSecretGitHubLogin,
callbackURL: "/api/v1/sso/github",
scope: [ 'user:email' ]
scope: ["user:email"]
},
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
interface GitHubEmail {
email: string;
primary: boolean;
verified: boolean;
visibility: null | string;
}
const email = profile.emails[0].value;
const { data }: { data: GitHubEmail[] } = await standardRequest.get(
`${INTEGRATION_GITHUB_API_URL}/user/emails`,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0];
const email = primaryEmail.email;
let user = await User.findOne({
email

View File

@@ -80,13 +80,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

@@ -117,6 +117,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()
@@ -157,9 +190,11 @@ export const DeleteIntegrationAuthV1 = z.object({
});
export const GetIntegrationAuthTeamCityBuildConfigsV1 = z.object({
params:z.object({
appId:z.string().trim().optional(),
params: z.object({
integrationAuthId:z.string().trim()
}),
query: z.object({
appId:z.string().trim()
})
})

View File

@@ -274,7 +274,7 @@ export const ToggleAutoCapitalizationV2 = z.object({
workspaceId: z.string().trim()
}),
body: z.object({
autoCapitalization: z.string().trim()
autoCapitalization: z.boolean()
})
});

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,
@@ -83,6 +85,7 @@ export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth
export const INTEGRATION_GCP_API_URL = "https://cloudresourcemanager.googleapis.com";
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
export const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api";
export const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
export const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com";
export const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com";
export const INTEGRATION_RENDER_API_URL = "https://api.render.com";
@@ -93,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";
@@ -272,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",

View File

@@ -1,6 +1,6 @@
module github.com/Infisical/infisical-merge
go 1.19
go 1.21
require (
github.com/charmbracelet/lipgloss v0.5.0

View File

@@ -198,7 +198,7 @@ var secretsSetCmd = &cobra.Command{
}
// Key and value from argument
key := strings.ToUpper(splitKeyValueFromArg[0])
key := splitKeyValueFromArg[0]
value := splitKeyValueFromArg[1]
hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
@@ -417,7 +417,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
secretsMap := getSecretsByKeys(secrets)
for _, secretKeyFromArg := range args {
if value, ok := secretsMap[strings.ToUpper(secretKeyFromArg)]; ok {
if value, ok := secretsMap[secretKeyFromArg]; ok {
requestedSecrets = append(requestedSecrets, value)
} else {
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
@@ -625,7 +625,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
func CenterString(s string, numStars int) string {
stars := strings.Repeat("*", numStars)
padding := (numStars - len(s)) / 2
cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:]
cenetredTextWithStar := stars[:padding] + " " + s + " " + stars[padding:]
hashes := strings.Repeat("#", len(cenetredTextWithStar)+2)
return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes)

View File

@@ -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: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -0,0 +1,43 @@
---
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/integrations-qovery-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/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 qovery create](../../images/integrations/qovery/integrations-qovery-create-1.png)
![integrations qovery create](../../images/integrations/qovery/integrations-qovery-create-2.png)
<Note>
Infisical supports syncing secrets to various Qovery scopes including applications, jobs, or containers.
</Note>
![integrations qovery settings](../../images/integrations/qovery/integrations-qovery.png)

View File

@@ -0,0 +1,34 @@
---
title: "Bun"
description: "How to use Infisical to inject environment variables and secrets into a Bun app."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- [Install the CLI](/cli/overview)
## Initialize Infisical for your [Bun](https://bun.sh)
```bash
# navigate to the root of your of your project
cd /path/to/project
# then initialize infisical
infisical init
```
## Start your application as usual but with Infisical
```bash
infisical run -- <your application start command>
# Example
infisical run -- bun run dev
```
<Info>
Bun environment variables can be called as either `Bun.env.SECRET` or `process.env.SECRET`. We also recommend you check this more in-depth [guide to environment variables in Bun](https://infisical.com/blog/bun-environment-variables).
</Info>
<iframe width="560" height="315" src="https://www.youtube.com/embed/m1NNVuiP07M?si=wJtdQdhQ2ylGeRI8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

View File

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

View File

@@ -238,6 +238,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",

View File

@@ -72,7 +72,6 @@
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-code-input": "^3.10.1",
"react-contenteditable": "^3.3.7",
"react-day-picker": "^8.8.0",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",
@@ -13340,7 +13339,8 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-diff": {
"version": "1.3.0",
@@ -19594,18 +19594,6 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-contenteditable": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"prop-types": "^15.7.1"
},
"peerDependencies": {
"react": ">=16.3"
}
},
"node_modules/react-day-picker": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz",
@@ -33272,7 +33260,8 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-diff": {
"version": "1.3.0",
@@ -37807,15 +37796,6 @@
"dev": true,
"requires": {}
},
"react-contenteditable": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
"requires": {
"fast-deep-equal": "^3.1.3",
"prop-types": "^15.7.1"
}
},
"react-day-picker": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz",

View File

@@ -80,7 +80,6 @@
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-code-input": "^3.10.1",
"react-contenteditable": "^3.3.7",
"react-day-picker": "^8.8.0",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",

View File

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

View File

@@ -1,6 +1,5 @@
/* eslint-disable react/no-danger */
import { forwardRef, HTMLAttributes } from "react";
import ContentEditable from "react-contenteditable";
import sanitizeHtml, { DisallowedTagsModes } from "sanitize-html";
import { useToggle } from "@app/hooks";
@@ -35,58 +34,52 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
`<span class="ph-no-capture text-yellow">&#36;&#123;<span class="ph-no-capture text-yello-200/80">${b}</span>&#125;</span>`
);
return newContent;
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
// when break is added a line break works properly
return `${newContent}<br/>`;
};
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onBlur"> & {
type Props = HTMLAttributes<HTMLTextAreaElement> & {
value?: string | null;
isVisible?: boolean;
isDisabled?: boolean;
onChange?: (val: string) => void;
onBlur?: () => void;
};
export const SecretInput = forwardRef<HTMLDivElement, Props>(
({ value, isVisible, onChange, onBlur, isDisabled, ...props }, ref) => {
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
({ value, isVisible, onBlur, isDisabled, onFocus, ...props }, ref) => {
const [isSecretFocused, setIsSecretFocused] = useToggle();
return (
<div
className="thin-scrollbar relative overflow-y-auto overflow-x-hidden"
style={{ maxHeight: `${21 * 7}px` }}
>
<div
dangerouslySetInnerHTML={{
__html: syntaxHighlight(value, isVisible || isSecretFocused)
}}
className={`absolute top-0 left-0 z-0 h-full w-full inline-block text-ellipsis whitespace-pre-wrap break-all ${
!value && value !== "" && "italic text-red-600/70"
}`}
ref={ref}
/>
<ContentEditable
className="relative z-10 h-full w-full text-ellipsis inline-block whitespace-pre-wrap break-all text-transparent caret-white outline-none"
role="textbox"
onChange={(evt) => {
if (onChange) onChange(evt.currentTarget.innerText.trim());
}}
onFocus={() => setIsSecretFocused.on()}
disabled={isDisabled}
spellCheck={false}
onBlur={() => {
if (onBlur) onBlur();
setIsSecretFocused.off();
}}
html={
isVisible || isSecretFocused
? sanitizeHtml(
value?.replaceAll("<", "&lt;").replaceAll(">", "&gt;") || "",
sanitizeConf
)
: syntaxHighlight(value, false)
}
{...props}
/>
<div className="overflow-auto w-full" style={{ maxHeight: `${21 * 7}px` }}>
<div className="relative overflow-hidden">
<pre aria-hidden className="m-0 ">
<code className={`inline-block w-full ${commonClassName}`}>
<span
style={{ whiteSpace: "break-spaces" }}
dangerouslySetInnerHTML={{
__html: syntaxHighlight(value, isVisible || isSecretFocused) ?? ""
}}
/>
</code>
</pre>
<textarea
style={{ whiteSpace: "break-spaces" }}
aria-label="secret value"
ref={ref}
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
onFocus={() => setIsSecretFocused.on()}
disabled={isDisabled}
spellCheck={false}
onBlur={(evt) => {
onBlur?.(evt);
setIsSecretFocused.off();
}}
value={value || ""}
{...props}
/>
</div>
</div>
);
}

View File

@@ -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,121 @@ 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;
}) => {
if (orgId === "none") return [];
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;
}) => {
if (projectId === "none") return [];
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 (environmentId === "none") return [];
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
@@ -197,6 +339,8 @@ const fetchIntegrationAuthTeamCityBuildConfigs = async ({
integrationAuthId: string;
appId: string;
}) => {
if (appId === "") return [];
const {
data: { buildConfigs }
} = await apiRequest.get<{ buildConfigs: TeamCityBuildConfig[] }>(
@@ -269,6 +413,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

View File

@@ -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;

View File

@@ -40,6 +40,7 @@ export const useCreateIntegration = () => {
owner,
path,
region,
scope,
secretPath,
metadata
}: {
@@ -56,6 +57,7 @@ export const useCreateIntegration = () => {
owner?: string;
path?: string;
region?: string;
scope?: string;
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
@@ -73,6 +75,7 @@ export const useCreateIntegration = () => {
targetServiceId,
owner,
path,
scope,
region,
secretPath,
metadata

View File

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

View File

@@ -487,7 +487,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
{/* <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a>
<MenuItem
isSelected={
@@ -498,7 +498,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
Audit Logs
</MenuItem>
</a>
</Link> */}
</Link>
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
<a>
<MenuItem

View File

@@ -84,6 +84,7 @@ export default function GitLabCreateIntegrationPage() {
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
const targetEntity = watch("targetEntity");
const targetTeamId = watch("targetTeamId");
const targetAppIdValue = watch("targetAppId");
const { mutateAsync } = useCreateIntegration();
@@ -440,6 +441,7 @@ export default function GitLabCreateIntegrationPage() {
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={targetAppIdValue === "none"}
>
Create Integration
</Button>

View File

@@ -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;

View File

@@ -0,0 +1,409 @@
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";
const qoveryScopes = [
{ label: "Application", value: "application" },
{ label: "Container", value: "container" },
{ label: "Job", value: "job" }
];
enum TabSections {
InfisicalSettings = "infisicalSettings",
QoverySettings = "qoverySettings"
}
/**
* Follow the logic:
* - Access token
* -> Get organizations (orgId) - select an organization
* -> Get projects belonging to the organization - select project
* -> Get environments belonging to the project - select environmnet
* -> Get qovery scopes / application (this is the application to sync to)
*
* Do we even need to get organizations?
*/
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 [targetOrgId, setTargetOrgId] = useState("");
const { data: integrationAuthProjects } = useGetIntegrationAuthQoveryProjects({
integrationAuthId: (integrationAuthId as string) ?? "",
orgId: targetOrgId
});
const [targetProjectId, setTargetProjectId] = useState("");
const { data: integrationAuthEnvironments } = useGetIntegrationAuthQoveryEnvironments({
integrationAuthId: (integrationAuthId as string) ?? "",
projectId: targetProjectId
});
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthQoveryScopes({
integrationAuthId: (integrationAuthId as string) ?? "",
environmentId: targetEnvironmentId,
scope: (scope as ("job" | "application" | "container"))
});
const [targetAppId, setTargetAppId] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(String(integrationAuthApps[0].appId));
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
useEffect(() => {
if (integrationAuthOrgs) {
if (integrationAuthOrgs.length > 0) {
setTargetOrgId(String(integrationAuthOrgs[0].orgId));
} else {
setTargetOrgId("none");
}
}
}, [integrationAuthOrgs]);
useEffect(() => {
if (integrationAuthProjects) {
if (integrationAuthProjects.length > 0) {
setTargetProjectId(String(integrationAuthProjects[0].projectId));
} else {
setTargetProjectId("none");
}
}
}, [integrationAuthProjects]);
useEffect(() => {
if (integrationAuthEnvironments) {
if (integrationAuthEnvironments.length > 0) {
setTargetEnvironmentId(String(integrationAuthEnvironments[0].environmentId));
} else {
setTargetEnvironmentId("none");
}
}
}, [integrationAuthEnvironments]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
const targetOrg = integrationAuthOrgs?.find((integrationAuthOrg) => integrationAuthOrg.orgId === targetOrgId)?.name;
const targetProject = integrationAuthProjects?.find((integrationAuthProject) => integrationAuthProject.projectId === targetProjectId)?.name;
const targetEnvironment = integrationAuthEnvironments?.find((integrationAuthEnvironment) => integrationAuthEnvironment.environmentId === targetEnvironmentId)?.name;
const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name;
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: targetAppId,
scope,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService: targetProject,
targetServiceId: targetProjectId,
owner: targetOrg,
secretPath
});
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"
>
{qoveryScopes.map((qoveryScope) => (
<SelectItem
value={qoveryScope.value}
key={`target-app-${qoveryScope.value}`}
>
{qoveryScope.label}
</SelectItem>
))}
</Select>
</FormControl>
{integrationAuthOrgs && <FormControl label="Qovery Organization">
<Select
value={targetOrgId}
onValueChange={(val) => setTargetOrgId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthOrgs.length === 0}
>
{integrationAuthOrgs.length > 0 ? (
integrationAuthOrgs.map((integrationAuthOrg) => (
<SelectItem
value={integrationAuthOrg.orgId}
key={`target-app-${integrationAuthOrg.orgId}`}
>
{integrationAuthOrg.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No organizaztions found
</SelectItem>
)}
</Select>
</FormControl>}
{integrationAuthProjects && <FormControl label="Qovery Project">
<Select
value={targetProjectId}
onValueChange={(val) => setTargetProjectId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthProjects.length === 0}
>
{integrationAuthProjects.length > 0 ? (
integrationAuthProjects.map((integrationAuthProject) => (
<SelectItem
value={integrationAuthProject.projectId}
key={`target-app-${integrationAuthProject.projectId}`}
>
{integrationAuthProject.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>}
{integrationAuthEnvironments && <FormControl label="Qovery Environment">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthEnvironments.length === 0}
>
{integrationAuthEnvironments.length > 0 ? (
integrationAuthEnvironments.map((integrationAuthEnvironment) => (
<SelectItem
value={integrationAuthEnvironment.environmentId}
key={`target-app-${integrationAuthEnvironment.environmentId}`}
>
{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={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={(integrationAuthApp.appId as string)}
key={`target-app-${integrationAuthApp.appId}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No {scope}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;

View File

@@ -49,7 +49,7 @@ export default function TeamCityCreateIntegrationPage() {
integrationAuthId: (integrationAuthId as string) ?? "",
appId: targetAppId
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);

View File

@@ -270,7 +270,7 @@ export const SecretInputRow = memo(
className="flex w-full flex-grow flex-row border-r border-none border-red"
style={{ padding: "0.5rem 0 0.5rem 1rem" }}
>
<div className="w-full">
<div className="w-full flex items-center">
{isOverridden ? (
<Controller
control={control}

View File

@@ -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;

View File

@@ -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?.owner || "-"}
</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?.targetService || "-"}
</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?.targetEnvironment || "-"}
</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

View File

@@ -3,6 +3,7 @@ import { faElementor } from "@fortawesome/free-brands-svg-icons";
import {
faAnchorLock,
faArrowLeft,
faBook,
faCog,
faKey,
faLock,
@@ -82,6 +83,12 @@ const SINGLE_PERMISSION_LIST = [
icon: faTags,
formName: "tags"
},
{
title: "Audit Logs",
subtitle: "Audit log management control",
icon: faBook,
formName: "audit-logs"
},
{
title: "IP Allowlist",
subtitle: "IP allowlist management control",