Merge pull request #3268 from Infisical/feat/addCustomDomainToWindmillIntegration

Add Windmill custom api url domain
This commit is contained in:
carlosmonastyrski
2025-03-21 14:19:39 -03:00
committed by GitHub
5 changed files with 92 additions and 31 deletions
@@ -923,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => {
/** /**
* Return list of projects for Windmill integration * Return list of projects for Windmill integration
*/ */
const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string | null }) => {
const { data } = await request.get<{ id: string; name: string }[]>( const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL;
`${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`, const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, {
{ headers: {
headers: { Authorization: `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json"
"Accept-Encoding": "application/json"
}
} }
); });
// check for write access of secrets in windmill workspaces // check for write access of secrets in windmill workspaces
const writeAccessCheck = data.map(async (app) => { const writeAccessCheck = data.map(async (app) => {
@@ -941,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
const folderPath = "f/folder/variable"; const folderPath = "f/folder/variable";
const { data: writeUser } = await request.post<object>( const { data: writeUser } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, `${apiUrl}/w/${app.id}/variables/create`,
{ {
path: userPath, path: userPath,
value: "variable", value: "variable",
@@ -957,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
); );
const { data: writeFolder } = await request.post<object>( const { data: writeFolder } = await request.post<object>(
`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, `${apiUrl}/w/${app.id}/variables/create`,
{ {
path: folderPath, path: folderPath,
value: "variable", value: "variable",
@@ -974,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => {
// is write access is allowed then delete the created secrets from workspace // is write access is allowed then delete the created secrets from workspace
if (writeUser && writeFolder) { if (writeUser && writeFolder) {
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${userPath}`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
}); });
await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${folderPath}`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
@@ -1316,7 +1314,8 @@ export const getApps = async ({
case Integrations.WINDMILL: case Integrations.WINDMILL:
return getAppsWindmill({ return getAppsWindmill({
accessToken accessToken,
url
}); });
case Integrations.DIGITAL_OCEAN_APP_PLATFORM: case Integrations.DIGITAL_OCEAN_APP_PLATFORM:
@@ -4127,10 +4127,10 @@ const syncSecretsWindmill = async ({
is_secret: boolean; is_secret: boolean;
description?: string; description?: string;
} }
const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL;
// get secrets stored in windmill workspace // get secrets stored in windmill workspace
const res = ( const res = (
await request.get<WindmillSecret[]>(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { await request.get<WindmillSecret[]>(`${apiUrl}/w/${integration.appId}/variables/list`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
@@ -4146,7 +4146,6 @@ const syncSecretsWindmill = async ({
// eslint-disable-next-line // eslint-disable-next-line
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
if (!(key in res)) { if (!(key in res)) {
@@ -4154,7 +4153,7 @@ const syncSecretsWindmill = async ({
// -> create secret // -> create secret
await request.post( await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`, `${apiUrl}/w/${integration.appId}/variables/create`,
{ {
path: key, path: key,
value: secrets[key].value, value: secrets[key].value,
@@ -4171,7 +4170,7 @@ const syncSecretsWindmill = async ({
} else { } else {
// -> update secret // -> update secret
await request.post( await request.post(
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, `${apiUrl}/w/${integration.appId}/variables/update/${res[key].path}`,
{ {
path: key, path: key,
value: secrets[key].value, value: secrets[key].value,
@@ -4192,16 +4191,13 @@ const syncSecretsWindmill = async ({
for await (const key of Object.keys(res)) { for await (const key of Object.keys(res)) {
if (!(key in secrets)) { if (!(key in secrets)) {
// -> delete secret // -> delete secret
await request.delete( await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, {
`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`, headers: {
{ Authorization: `Bearer ${accessToken}`,
headers: { "Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json"
"Content-Type": "application/json",
"Accept-Encoding": "application/json"
}
} }
); });
} }
} }
}; };
@@ -192,6 +192,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
); );
} }
if (integration.integration === "windmill" && integration.url) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Instance URL" />
<div className="text-sm text-mineshaft-300">{integration.url}</div>
</div>
);
}
return null; return null;
}; };
@@ -3,31 +3,75 @@ import { useNavigate } from "@tanstack/react-router";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useSaveIntegrationAccessToken } from "@app/hooks/api"; import { useSaveIntegrationAccessToken } from "@app/hooks/api";
export const WindmillAuthorizePage = () => { export const WindmillAuthorizePage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { mutateAsync } = useSaveIntegrationAccessToken(); const { mutateAsync } = useSaveIntegrationAccessToken();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [apiUrl, setApiUrl] = useState<string | null>(null);
const [apiUrlErrorText, setApiUrlErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const isLocalOrPrivateIpAddress = (url: string): boolean => {
try {
const validUrl = new URL(url);
// Check for localhost
if (validUrl.hostname === "localhost" || validUrl.hostname === "127.0.0.1") {
return true;
}
// Check for 10.x.x.x
if (validUrl.hostname.match(/^10\.\d+\.\d+\.\d+/)) {
return true;
}
// Check for host.docker.internal
if (validUrl.hostname === "host.docker.internal") {
return true;
}
// Check for 192.168.x.x
if (validUrl.hostname.match(/^192\.168\.\d+\.\d+/)) {
return true;
}
return false;
} catch (err) {
console.error(err);
return true;
}
};
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
setApiKeyErrorText(""); setApiKeyErrorText("");
setApiUrlErrorText("");
if (apiKey.length === 0) { if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank"); setApiKeyErrorText("API Key cannot be blank");
return; return;
} }
if (apiUrl) {
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
setApiUrlErrorText("API URL must start with http:// or https://");
return;
}
if (isInfisicalCloud() && isLocalOrPrivateIpAddress(apiUrl)) {
setApiUrlErrorText("Local IPs not allowed as URL");
return;
}
}
setIsLoading(true); setIsLoading(true);
const integrationAuth = await mutateAsync({ const integrationAuth = await mutateAsync({
workspaceId: currentWorkspace.id, workspaceId: currentWorkspace.id,
integration: "windmill", integration: "windmill",
accessToken: apiKey accessToken: apiKey,
url: apiUrl ?? undefined
}); });
setIsLoading(false); setIsLoading(false);
@@ -57,6 +101,18 @@ export const WindmillAuthorizePage = () => {
> >
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} /> <Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl> </FormControl>
<FormControl
label="Windmill Instance URL"
errorText={apiUrlErrorText}
isError={apiUrlErrorText !== ""}
tooltipText="If you are using a custom domain, enter it here. Otherwise, leave it blank."
>
<Input
value={apiUrl ?? ""}
onChange={(e) => setApiUrl(e.target.value.trim() === "" ? null : e.target.value.trim())}
placeholder="https://xxxx.windmill.dev"
/>
</FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
@@ -67,7 +67,8 @@ export const WindmillConfigurePage = () => {
(integrationAuthApp) => integrationAuthApp.name === targetApp (integrationAuthApp) => integrationAuthApp.name === targetApp
)?.appId, )?.appId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
secretPath secretPath,
url: integrationAuth.url ?? undefined
}); });
setIsLoading(false); setIsLoading(false);