feat: add cloud 66 integration

This commit is contained in:
Afrie Irham
2023-07-23 16:36:26 +08:00
parent 565f234921
commit 94a23bfa23
10 changed files with 404 additions and 16 deletions
+66 -3
View File
@@ -1,6 +1,3 @@
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import { standardRequest } from "../config/request";
import { import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
@@ -13,6 +10,8 @@ import {
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL, INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
@@ -37,6 +36,9 @@ import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL INTEGRATION_VERCEL_API_URL
} from "../variables"; } from "../variables";
import { IIntegrationAuth } from "../models";
import { Octokit } from "@octokit/rest";
import { standardRequest } from "../config/request";
interface App { interface App {
name: string; name: string;
@@ -162,6 +164,11 @@ const getApps = async ({
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_CLOUD_66:
apps = await getAppsCloud66({
accessToken,
});
break;
} }
return apps; return apps;
@@ -842,4 +849,60 @@ const getAppsCodefresh = async ({
return apps; return apps;
}; };
/**
* Return list of projects for Cloud 66 integration
*/
const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
interface Cloud66Apps {
uid: string;
name: string;
account_id: number;
git: string;
git_branch: string;
environment: string;
cloud: string;
fqdn: string;
language: string;
framework: string;
status: number;
health: number;
last_activity: string;
last_activity_iso: string;
maintenance_mode: boolean;
has_loadbalancer: boolean;
created_at: string;
updated_at: string;
deploy_directory: string;
cloud_status: string;
backend: string;
version: string;
revision: string;
is_busy: boolean;
account_name: string;
is_cluster: boolean;
is_inside_cluster: boolean;
cluster_name: any;
application_address: string;
configstore_namespace: string;
}
const stacks = (
await standardRequest.get(`${INTEGRATION_CLOUD_66_API_URL}/3/stacks`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.response as Cloud66Apps[]
const apps = stacks.map((app) => ({
name: app.name,
appId: app.uid
}));
return apps;
};
export { getApps }; export { getApps };
+89 -5
View File
@@ -1,14 +1,10 @@
import _ from "lodash";
import AWS from "aws-sdk";
import { import {
CreateSecretCommand, CreateSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
ResourceNotFoundException, ResourceNotFoundException,
SecretsManagerClient, SecretsManagerClient,
UpdateSecretCommand, UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager"; } from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from "../models"; import { IIntegration, IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -22,6 +18,8 @@ import {
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL, INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
@@ -47,6 +45,10 @@ import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL INTEGRATION_VERCEL_API_URL
} from "../variables"; } from "../variables";
import AWS from "aws-sdk";
import { Octokit } from "@octokit/rest";
import _ from "lodash";
import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
/** /**
@@ -220,6 +222,13 @@ const syncSecrets = async ({
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_CLOUD_66:
await syncSecretsCloud66({
integration,
secrets,
accessToken
});
break;
} }
}; };
@@ -2101,4 +2110,79 @@ const syncSecretsCodefresh = async ({
); );
}; };
const syncSecretsCloud66 = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface Cloud66Secret {
id: number;
key: string;
value: string;
readonly: boolean;
created_at: string;
updated_at: string;
is_password: boolean;
is_generated: boolean;
history: any[];
}
// get all current secrets
const currentSecrets = (
await standardRequest.get(
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
).data.response as Cloud66Secret[];
// filter out generated secrets
const secretsToDelete = currentSecrets.filter((s) => !s.is_generated);
// delete all current secrets
await Promise.all(
secretsToDelete.map(
async (s) =>
await standardRequest.delete(
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${s.key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
)
);
// add new secrets
await Promise.all(
Object.entries(secrets).map(
async ([key, value]) =>
await standardRequest.post(
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`,
{
key,
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
)
)
);
};
export { syncSecrets }; export { syncSecrets };
+5 -2
View File
@@ -1,4 +1,3 @@
import { Schema, Types, model } from "mongoose";
import { import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
@@ -7,6 +6,7 @@ import {
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
@@ -21,6 +21,7 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL INTEGRATION_VERCEL
} from "../variables"; } from "../variables";
import { Schema, Types, model } from "mongoose";
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -59,6 +60,7 @@ export interface IIntegration {
| "cloudflare-pages" | "cloudflare-pages"
| "bitbucket" | "bitbucket"
| "codefresh" | "codefresh"
| "cloud-66"
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -149,7 +151,8 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
], ],
required: true, required: true,
}, },
+6 -3
View File
@@ -1,4 +1,3 @@
import { Document, Schema, Types, model } from "mongoose";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
@@ -9,6 +8,7 @@ import {
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
@@ -23,6 +23,7 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL INTEGRATION_VERCEL
} from "../variables"; } from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -46,7 +47,8 @@ export interface IIntegrationAuth extends Document {
| "checkly" | "checkly"
| "cloudflare-pages" | "cloudflare-pages"
| "codefresh" | "codefresh"
| "bitbucket"; | "bitbucket"
| "cloud-66";
teamId: string; teamId: string;
accountId: string; accountId: string;
url: string; url: string;
@@ -93,7 +95,8 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
], ],
required: true, required: true,
}, },
+14 -2
View File
@@ -29,6 +29,7 @@ export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket"; export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_CODEFRESH = "codefresh"; export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_CLOUD_66 = "cloud-66";
export const INTEGRATION_SET = new Set([ export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -46,7 +47,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66
]); ]);
// integration types // integration types
@@ -79,6 +81,7 @@ export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org"; export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api"; export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api";
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
@@ -271,7 +274,16 @@ export const getIntegrationOptions = async () => {
type: "pat", type: "pat",
clientId: "", clientId: "",
docsLink: "", docsLink: "",
} },
{
name: "Cloud 66",
slug: "cloud-66",
image: "Cloud 66.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
] ]
return INTEGRATION_OPTIONS; return INTEGRATION_OPTIONS;
+2 -1
View File
@@ -22,7 +22,8 @@ const integrationSlugNameMapping: Mapping = {
"hashicorp-vault": "Vault", "hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages", "cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh", "codefresh": "Codefresh",
bitbucket: "BitBucket" bitbucket: "BitBucket",
"cloud-66": "Cloud 66"
}; };
const envMapping: Mapping = { const envMapping: Mapping = {
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function Cloud66CreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "cloud-66",
accessId: null,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/cloud-66/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
<FormControl
label="Cloud 66 API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Cloud 66
</Button>
</Card>
</div>
);
}
Cloud66CreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function Cloud66CreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<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">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Cloud 66 Service" className="mt-4">
<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 services found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
Cloud66CreateIntegrationPage.requireAuth = true;
@@ -98,6 +98,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "codefresh": case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`; link = `${window.location.origin}/integrations/codefresh/authorize`;
break; break;
case "cloud-66":
link = `${window.location.origin}/integrations/cloud-66/authorize`;
break;
default: default:
break; break;
} }