ianedwards 2 лет назад
Родитель
Сommit
b765d7e6ae

+ 40 - 5
api/server/handlers/environment_groups/are_external_providers_enabled.go

@@ -34,8 +34,20 @@ func NewAreExternalProvidersEnabledHandler(
 	}
 }
 
-// AreExternalProvidersEnabledResponse is the response object for the /environment-group/are-external-providers-enabled endpoint
-type AreExternalProvidersEnabledResponse struct {
+// ExternalEnvGroupOperator is the type of external env group operator, which syncs secrets from external sources
+type ExternalEnvGroupOperator string
+
+const (
+	// ExternalEnvGroupOperator_ExternalSecrets is the external secrets operator
+	ExternalEnvGroupOperator_ExternalSecrets ExternalEnvGroupOperator = "external-secrets"
+	// ExternalEnvGroupOperator_Infisical is the infisical secrets operator
+	ExternalEnvGroupOperator_Infisical ExternalEnvGroupOperator = "infisical"
+)
+
+// ExternalEnvGroupOperatorEnabledStatus is the status of an external env group operator
+type ExternalEnvGroupOperatorEnabledStatus struct {
+	// Type is the type of external provider
+	Type ExternalEnvGroupOperator `json:"type"`
 	// Enabled is true if external providers are enabled
 	Enabled bool `json:"enabled"`
 	// ReprovisionRequired is true if the cluster needs to be reprovisioned to enable external providers
@@ -44,6 +56,11 @@ type AreExternalProvidersEnabledResponse struct {
 	K8SUpgradeRequired bool `json:"k8s_upgrade_required"`
 }
 
+// AreExternalProvidersEnabledResponse is the response object for the /environment-group/are-external-providers-enabled endpoint
+type AreExternalProvidersEnabledResponse struct {
+	Operators []ExternalEnvGroupOperatorEnabledStatus `json:"operators"`
+}
+
 // ServeHTTP checks if external providers are enabled
 func (c *AreExternalProvidersEnabledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-are-external-providers-enabled")
@@ -62,9 +79,27 @@ func (c *AreExternalProvidersEnabledHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
+	var operators []ExternalEnvGroupOperatorEnabledStatus
+	for _, operator := range resp.Msg.Operators {
+		var operatorType ExternalEnvGroupOperator
+		switch operator.Operator {
+		case porterv1.EnumExternalEnvGroupOperatorType_ENUM_EXTERNAL_ENV_GROUP_OPERATOR_TYPE_EXTERNAL_SECRETS:
+			operatorType = ExternalEnvGroupOperator_ExternalSecrets
+		case porterv1.EnumExternalEnvGroupOperatorType_ENUM_EXTERNAL_ENV_GROUP_OPERATOR_TYPE_INFISICAL:
+			operatorType = ExternalEnvGroupOperator_Infisical
+		default:
+			continue
+		}
+
+		operators = append(operators, ExternalEnvGroupOperatorEnabledStatus{
+			Type:                operatorType,
+			Enabled:             operator.Enabled,
+			ReprovisionRequired: operator.ReprovisionRequired,
+			K8SUpgradeRequired:  operator.K8SUpgradeRequired,
+		})
+	}
+
 	c.WriteResult(w, r, &AreExternalProvidersEnabledResponse{
-		Enabled:             resp.Msg.Enabled,
-		ReprovisionRequired: resp.Msg.ReprovisionRequired,
-		K8SUpgradeRequired:  resp.Msg.K8SUpgradeRequired,
+		Operators: operators,
 	})
 }

+ 12 - 2
api/server/handlers/environment_groups/delete.go

@@ -61,11 +61,19 @@ func (c *DeleteEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http
 	)
 
 	switch request.Type {
-	case "doppler":
+	case "doppler", "infisical":
+		var provider porterv1.EnumEnvGroupProviderType
+		switch request.Type {
+		case "doppler":
+			provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER
+		case "infisical":
+			provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL
+		}
+
 		_, err := c.Config().ClusterControlPlaneClient.DeleteEnvGroup(ctx, connect.NewRequest(&porterv1.DeleteEnvGroupRequest{
 			ProjectId:            int64(cluster.ProjectID),
 			ClusterId:            int64(cluster.ID),
-			EnvGroupProviderType: porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER,
+			EnvGroupProviderType: provider,
 			EnvGroupName:         request.Name,
 		}))
 		if err != nil {
@@ -88,4 +96,6 @@ func (c *DeleteEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http
 			return
 		}
 	}
+
+	c.WriteResult(w, r, nil)
 }

+ 1 - 0
api/server/handlers/environment_groups/list.go

@@ -218,5 +218,6 @@ func (c *ListEnvironmentGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 var translateProtoTypeToEnvGroupType = map[porterv1.EnumEnvGroupProviderType]string{
 	porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DATASTORE: "datastore",
 	porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER:   "doppler",
+	porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL: "infisical",
 	porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_PORTER:    "porter",
 }

+ 55 - 3
api/server/handlers/environment_groups/update.go

@@ -34,6 +34,22 @@ func NewUpdateEnvironmentGroupHandler(
 	}
 }
 
+// EnvironmentGroupType is the env_groups-level environment group type
+type EnvironmentGroupType string
+
+const (
+	// EnvironmentGroupType_Unspecified is the nil environment group type
+	EnvironmentGroupType_Unspecified EnvironmentGroupType = ""
+	// EnvironmentGroupType_Doppler is the doppler environment group type
+	EnvironmentGroupType_Doppler EnvironmentGroupType = "doppler"
+	// EnvironmentGroupType_Porter is the porter environment group type
+	EnvironmentGroupType_Porter EnvironmentGroupType = "porter"
+	// EnvironmentGroupType_Datastore is the datastore environment group type
+	EnvironmentGroupType_Datastore EnvironmentGroupType = "datastore"
+	// EnvironmentGroupType_Infisical is the infisical environment group type
+	EnvironmentGroupType_Infisical EnvironmentGroupType = "infisical"
+)
+
 // EnvVariableDeletions is the set of keys to delete from the environment group
 type EnvVariableDeletions struct {
 	// Variables is a set of variable keys to delete from the environment group
@@ -42,12 +58,20 @@ type EnvVariableDeletions struct {
 	Secrets []string `json:"secrets"`
 }
 
+// InfisicalEnv is the Infisical environment to pull secret values from, only required for the Infisical external provider type
+type InfisicalEnv struct {
+	// Slug is the slug referring to the Infisical environment to pull secret values from
+	Slug string `json:"slug"`
+	// Path is the relative path in the Infisical environment to pull secret values from
+	Path string `json:"path"`
+}
+
 type UpdateEnvironmentGroupRequest struct {
 	// Name of the env group to create or update
 	Name string `json:"name"`
 
 	// Type of the env group to create or update
-	Type string `json:"type"`
+	Type EnvironmentGroupType `json:"type"`
 
 	// AuthToken for the env group
 	AuthToken string `json:"auth_token"`
@@ -69,6 +93,9 @@ type UpdateEnvironmentGroupRequest struct {
 
 	// SkipAppAutoDeploy is a flag to determine if the app should be auto deployed
 	SkipAppAutoDeploy bool `json:"skip_app_auto_deploy"`
+
+	// InfisicalEnv is the Infisical environment to pull secret values from, only required for the Infisical external provider type
+	InfisicalEnv InfisicalEnv `json:"infisical_env"`
 }
 type UpdateEnvironmentGroupResponse struct {
 	// Name of the env group to create or update
@@ -99,13 +126,38 @@ func (c *UpdateEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http
 	)
 
 	switch request.Type {
-	case "doppler":
+	case EnvironmentGroupType_Doppler, EnvironmentGroupType_Infisical:
+		var provider porterv1.EnumEnvGroupProviderType
+		var infisicalEnv *porterv1.InfisicalEnv
+		if request.Type == EnvironmentGroupType_Doppler {
+			provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER
+		}
+		if request.Type == EnvironmentGroupType_Infisical {
+			if request.InfisicalEnv.Slug == "" {
+				err := telemetry.Error(ctx, span, nil, "infisical env slug is required")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+				return
+			}
+			if request.InfisicalEnv.Path == "" {
+				err := telemetry.Error(ctx, span, nil, "infisical env path is required")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+				return
+			}
+
+			provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL
+			infisicalEnv = &porterv1.InfisicalEnv{
+				EnvironmentSlug: request.InfisicalEnv.Slug,
+				EnvironmentPath: request.InfisicalEnv.Path,
+			}
+		}
+
 		_, err := c.Config().ClusterControlPlaneClient.CreateOrUpdateEnvGroup(ctx, connect.NewRequest(&porterv1.CreateOrUpdateEnvGroupRequest{
 			ProjectId:            int64(cluster.ProjectID),
 			ClusterId:            int64(cluster.ID),
-			EnvGroupProviderType: porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER,
+			EnvGroupProviderType: provider,
 			EnvGroupName:         request.Name,
 			EnvGroupAuthToken:    request.AuthToken,
+			InfisicalEnv:         infisicalEnv,
 		}))
 		if err != nil {
 			err := telemetry.Error(ctx, span, err, "unable to create environment group")

+ 65 - 0
dashboard/src/assets/infisical.svg

@@ -0,0 +1,65 @@
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
+<path fill="#E1EB55" opacity="1.000000" stroke="none" 
+	d="
+M101.000000,201.000000 
+	C67.354073,201.000000 34.208145,201.000000 1.031109,201.000000 
+	C1.031109,134.395767 1.031109,67.791527 1.031109,1.093641 
+	C67.562431,1.093641 134.125046,1.093641 200.843826,1.093641 
+	C200.843826,67.666382 200.843826,134.333145 200.843826,201.000000 
+	C167.791809,201.000000 134.645905,201.000000 101.000000,201.000000 
+M62.711216,131.974060 
+	C78.336243,134.902283 90.414047,128.189545 99.755890,118.701469 
+	C107.918739,123.210167 114.946938,128.682541 122.908867,131.189850 
+	C146.220703,138.531036 168.025192,118.488327 162.997498,95.131706 
+	C158.762787,75.458992 135.184311,65.065819 116.553886,74.996025 
+	C110.921356,77.998222 105.943024,82.227791 100.569542,85.961502 
+	C97.236786,83.296654 93.701416,80.009048 89.727425,77.385971 
+	C80.840302,71.519928 71.113907,69.507828 60.702728,72.524307 
+	C47.481014,76.355103 38.325142,88.268257 38.161259,101.612381 
+	C37.989353,115.610016 46.869514,126.914375 62.711216,131.974060 
+z"/>
+<path fill="#030401" opacity="1.000000" stroke="none" 
+	d="
+M62.304436,131.901337 
+	C46.869514,126.914375 37.989353,115.610016 38.161259,101.612381 
+	C38.325142,88.268257 47.481014,76.355103 60.702728,72.524307 
+	C71.113907,69.507828 80.840302,71.519928 89.727425,77.385971 
+	C93.701416,80.009048 97.236786,83.296654 100.569542,85.961502 
+	C105.943024,82.227791 110.921356,77.998222 116.553886,74.996025 
+	C135.184311,65.065819 158.762787,75.458992 162.997498,95.131706 
+	C168.025192,118.488327 146.220703,138.531036 122.908867,131.189850 
+	C114.946938,128.682541 107.918739,123.210167 99.755890,118.701469 
+	C90.414047,128.189545 78.336243,134.902283 62.304436,131.901337 
+M138.804184,112.696808 
+	C145.058762,107.992989 146.829971,102.079491 143.707397,96.326591 
+	C140.617416,90.633736 133.388214,87.668671 126.935295,90.740112 
+	C121.606331,93.276573 117.040749,97.416870 112.212875,100.793343 
+	C119.345627,112.132149 128.638016,116.201546 138.804184,112.696808 
+M84.293167,107.816414 
+	C86.010841,105.883865 87.728508,103.951317 90.217514,101.150940 
+	C85.096687,97.447762 80.867920,93.643356 75.993286,91.055565 
+	C70.047211,87.898964 63.009277,89.850060 59.299061,94.636635 
+	C56.122128,98.735222 55.903999,104.368179 58.757915,108.611610 
+	C62.172878,113.689262 68.740486,115.772331 75.193657,113.284981 
+	C78.232635,112.113625 80.898720,109.974854 84.293167,107.816414 
+z"/>
+<path fill="#DDE754" opacity="1.000000" stroke="none" 
+	d="
+M138.436310,112.857315 
+	C128.638016,116.201546 119.345627,112.132149 112.212875,100.793343 
+	C117.040749,97.416870 121.606331,93.276573 126.935295,90.740112 
+	C133.388214,87.668671 140.617416,90.633736 143.707397,96.326591 
+	C146.829971,102.079491 145.058762,107.992989 138.436310,112.857315 
+z"/>
+<path fill="#DEE854" opacity="1.000000" stroke="none" 
+	d="
+M84.013474,108.045441 
+	C80.898720,109.974854 78.232635,112.113625 75.193657,113.284981 
+	C68.740486,115.772331 62.172878,113.689262 58.757915,108.611610 
+	C55.903999,104.368179 56.122128,98.735222 59.299061,94.636635 
+	C63.009277,89.850060 70.047211,87.898964 75.993286,91.055565 
+	C80.867920,93.643356 85.096687,97.447762 90.217514,101.150940 
+	C87.728508,103.951317 86.010841,105.883865 84.013474,108.045441 
+z"/>
+</svg>

+ 4 - 1
dashboard/src/lib/env-groups/types.ts

@@ -32,10 +32,13 @@ export const envGroupValidator = z.object({
   variables: z.record(z.string()).optional().default({}),
   secret_variables: z.record(z.string()).optional().default({}),
   created_at: z.string(),
+  linked_applications: z.array(z.string()).optional().default([]),
   type: z
     .string()
     .pipe(
-      z.enum(["UNKNOWN", "datastore", "doppler", "porter"]).catch("UNKNOWN")
+      z
+        .enum(["UNKNOWN", "datastore", "doppler", "porter", "infisical"])
+        .catch("UNKNOWN")
     ),
 });
 

+ 15 - 10
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx

@@ -14,6 +14,7 @@ import { Context } from "shared/Context";
 import { envGroupPath } from "shared/util";
 import database from "assets/database.svg";
 import doppler from "assets/doppler.png";
+import infisical from "assets/infisical.svg";
 import key from "assets/key.svg";
 
 type Props = {
@@ -65,22 +66,26 @@ const EnvGroupRow: React.FC<Props> = ({
     return [...normalVariables, ...secretVariables];
   }, [envGroup]);
 
+  const envGroupIcon = useMemo(() => {
+    if (envGroup.type === "doppler") {
+      return doppler;
+    }
+    if (envGroup.type === "datastore") {
+      return database;
+    }
+    if (envGroup.type === "infisical") {
+      return infisical;
+    }
+    return key;
+  }, [envGroup.type]);
+
   return (
     <Expandable
       maxHeight={maxHeight}
       header={
         <Container row spaced>
           <Container row>
-            <Image
-              size={20}
-              src={
-                envGroup.type === "doppler"
-                  ? doppler
-                  : envGroup.type === "datastore"
-                  ? database
-                  : key
-              }
-            />
+            <Image size={20} src={envGroupIcon} />
             <Spacer inline x={1} />
             <Text size={14}>{envGroup.name}</Text>
           </Container>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -613,7 +613,7 @@ export const ExpandedEnvGroupFC = ({
 
           const linkedApp: string[] = currentEnvGroup?.linked_applications;
           // doppler env groups update themselves, and we don't want to increment the version
-          if (currentEnvGroup?.type !== "doppler") {
+          if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") {
             await api.createEnvironmentGroups(
                 "<token>",
                 {

+ 48 - 50
dashboard/src/main/home/env-dashboard/EnvDashboard.tsx

@@ -1,8 +1,10 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
+import React, { useContext, useMemo, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
 import _ from "lodash";
 import { withRouter, type RouteComponentProps } from "react-router";
 import { Link } from "react-router-dom";
 import styled from "styled-components";
+import { z } from "zod";
 
 import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
 import Loading from "components/Loading";
@@ -17,6 +19,7 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import Toggle from "components/porter/Toggle";
 import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
+import { envGroupValidator, type ClientEnvGroup } from "lib/env-groups/types";
 
 import api from "shared/api";
 import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
@@ -27,6 +30,7 @@ import database from "assets/database.svg";
 import doppler from "assets/doppler.png";
 import envGroupGrad from "assets/env-group-grad.svg";
 import grid from "assets/grid.png";
+import infisical from "assets/infisical.svg";
 import key from "assets/key.svg";
 import list from "assets/list.png";
 import notFound from "assets/not-found.png";
@@ -40,10 +44,32 @@ const EnvDashboard: React.FC<Props> = (props) => {
   const { currentProject, currentCluster } = useContext(Context);
 
   const [searchValue, setSearchValue] = useState("");
-  const [envGroups, setEnvGroups] = useState<[]>([]);
-  const [isLoading, setIsLoading] = useState<boolean>(true);
   const [view, setView] = useState<"grid" | "list">("grid");
-  const [hasError, setHasError] = useState<boolean>(false);
+
+  const { data: { environment_groups: envGroups = [] } = {}, status } =
+    useQuery(
+      ["envGroups", currentProject?.id, currentCluster?.id],
+      async () => {
+        if (!currentProject || !currentCluster) {
+          return { environment_groups: [] };
+        }
+        const res = await api.getAllEnvGroups(
+          "<token>",
+          {},
+          {
+            id: currentProject?.id || -1,
+            cluster_id: currentCluster?.id || -1,
+          }
+        );
+
+        const data = await z
+          .object({
+            environment_groups: z.array(envGroupValidator),
+          })
+          .parseAsync(res.data);
+        return data;
+      }
+    );
 
   const filteredEnvGroups = useMemo(() => {
     const filteredBySearch = search(envGroups, searchValue, {
@@ -55,31 +81,18 @@ const EnvDashboard: React.FC<Props> = (props) => {
     return sortedFilteredBySearch;
   }, [envGroups, searchValue]);
 
-  const updateEnvGroups = async (): Promise<void> => {
-    try {
-      const res = await api.getAllEnvGroups(
-        "<token>",
-        {},
-        {
-          id: currentProject?.id || -1,
-          cluster_id: currentCluster?.id || -1,
-        }
-      );
-      setEnvGroups(res.data.environment_groups);
-      setIsLoading(false);
-    } catch (err) {
-      setHasError(true);
-      setIsLoading(false);
+  const getIconFromType = (type: ClientEnvGroup["type"]): string => {
+    if (type === "doppler") {
+      return doppler;
+    } else if (type === "datastore") {
+      return database;
+    } else if (type === "infisical") {
+      return infisical;
+    } else {
+      return key;
     }
   };
 
-  useEffect(() => {
-    setIsLoading(true);
-    if ((currentProject?.id ?? -1) > -1 && (currentCluster?.id ?? -1) > -1) {
-      void updateEnvGroups();
-    }
-  }, [currentProject, currentCluster]);
-
   const renderContents = (): React.ReactNode => {
     if (currentProject?.sandbox_enabled) {
       return (
@@ -105,7 +118,11 @@ const EnvDashboard: React.FC<Props> = (props) => {
       return <ClusterProvisioningPlaceholder />;
     }
 
-    if (!isLoading && (!envGroups || envGroups.length === 0)) {
+    if (status === "loading") {
+      return <Loading offset="-150px" />;
+    }
+
+    if (envGroups.length === 0) {
       return (
         <DashboardPlaceholder>
           <Text size={16}>No environment groups found</Text>
@@ -178,7 +195,7 @@ const EnvDashboard: React.FC<Props> = (props) => {
         </Container>
         <Spacer y={1} />
 
-        {!isLoading && filteredEnvGroups.length === 0 ? (
+        {status === "success" && filteredEnvGroups.length === 0 ? (
           <Fieldset>
             <Container row>
               <Image src={notFound} size={13} opacity={0.65} />
@@ -188,8 +205,6 @@ const EnvDashboard: React.FC<Props> = (props) => {
               </Text>
             </Container>
           </Fieldset>
-        ) : isLoading ? (
-          <Loading offset="-150px" />
         ) : view === "grid" ? (
           <GridList>
             {(filteredEnvGroups ?? []).map((envGroup, i: number) => {
@@ -199,16 +214,7 @@ const EnvDashboard: React.FC<Props> = (props) => {
                   key={i}
                 >
                   <Container row>
-                    <Image
-                      src={
-                        envGroup.type === "doppler"
-                          ? doppler
-                          : envGroup.type === "datastore"
-                          ? database
-                          : key
-                      }
-                      size={20}
-                    />
+                    <Image src={getIconFromType(envGroup.type)} size={20} />
                     <Spacer inline x={0.7} />
                     <Text size={14}>{envGroup.name}</Text>
                   </Container>
@@ -225,22 +231,14 @@ const EnvDashboard: React.FC<Props> = (props) => {
           </GridList>
         ) : (
           <List>
-            {(filteredEnvGroups ?? []).map((envGroup: any, i: number) => {
+            {(filteredEnvGroups ?? []).map((envGroup, i) => {
               return (
                 <Row
                   to={envGroupPath(currentProject, `/${envGroup.name}`)}
                   key={i}
                 >
                   <Container row>
-                    <Image
-                      src={
-                        envGroup.type === "doppler"
-                          ? doppler
-                          : envGroup.type === "datastore"
-                          ? database
-                          : key
-                      }
-                    />
+                    <Image src={getIconFromType(envGroup.type)} />
                     <Spacer inline x={0.7} />
                     <Text size={14}>{envGroup.name}</Text>
                   </Container>

+ 3 - 3
dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx

@@ -21,14 +21,14 @@ export type KeyValueType = {
 type PropsType = {
   label?: string;
   values: KeyValueType[];
-  setValues: (x: KeyValueType[]) => void;
+  setValues?: (x: KeyValueType[]) => void;
   disabled?: boolean;
   fileUpload?: boolean;
   secretOption?: boolean;
   setButtonDisabled?: (x: boolean) => void;
 };
 
-const EnvGroupArray = ({
+const EnvGroupArray: React.FC<PropsType> = ({
   label,
   values,
   setValues = () => {},
@@ -36,7 +36,7 @@ const EnvGroupArray = ({
   fileUpload,
   secretOption,
   setButtonDisabled,
-}: PropsType): React.ReactElement => {
+}) => {
   const [showEditorModal, setShowEditorModal] = useState(false);
   const blankValues = (): void => {
     const isAnyEnvVariableBlank = values.some(

+ 41 - 34
dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx

@@ -1,7 +1,9 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
+import React, { useContext, useEffect, useMemo } from "react";
+import { useQuery } from "@tanstack/react-query";
 import { useHistory, useParams } from "react-router";
 import styled from "styled-components";
 import { match } from "ts-pattern";
+import { z } from "zod";
 
 import Loading from "components/Loading";
 import Back from "components/porter/Back";
@@ -11,11 +13,13 @@ import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import TabSelector from "components/TabSelector";
+import { envGroupValidator } from "lib/env-groups/types";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
 import database from "assets/database.svg";
 import doppler from "assets/doppler.png";
+import infisical from "assets/infisical.svg";
 import key from "assets/key.svg";
 import notFound from "assets/not-found.png";
 import time from "assets/time.png";
@@ -43,9 +47,6 @@ const ExpandedEnv: React.FC = () => {
   }>();
   const history = useHistory();
 
-  const [isLoading, setIsLoading] = useState(true);
-  const [envGroup, setEnvGroup] = useState(null);
-
   const tabs = useMemo(() => {
     return [
       { label: "Environment variables", value: "env-vars" },
@@ -54,30 +55,34 @@ const ExpandedEnv: React.FC = () => {
     ];
   }, []);
 
-  const fetchEnvGroup = async () => {
-    try {
+  const {
+    data: envGroup,
+    isLoading,
+    refetch,
+  } = useQuery(
+    ["envGroups", currentProject?.id, currentCluster?.id, envGroupName],
+    async () => {
+      if (!currentProject || !currentCluster) {
+        return null;
+      }
       const res = await api.getAllEnvGroups(
         "<token>",
         {},
         {
-          id: currentProject?.id ?? -1,
-          cluster_id: currentCluster?.id ?? -1,
+          id: currentProject?.id || -1,
+          cluster_id: currentCluster?.id || -1,
         }
       );
-      const matchedEnvGroup = res.data.environment_groups.find((x) => {
-        return x.name === envGroupName;
-      });
-      setIsLoading(false);
-      setEnvGroup(matchedEnvGroup);
-    } catch (err) {
-      setIsLoading(false);
-    }
-  };
 
-  useEffect(() => {
-    setIsLoading(true);
-    void fetchEnvGroup();
-  }, [currentProject, currentCluster, envGroupName]);
+      const data = await z
+        .object({
+          environment_groups: z.array(envGroupValidator),
+        })
+        .parseAsync(res.data);
+
+      return data.environment_groups.find((eg) => eg.name === envGroupName);
+    }
+  );
 
   useEffect(() => {
     if (!tab) {
@@ -85,6 +90,19 @@ const ExpandedEnv: React.FC = () => {
     }
   }, [tab]);
 
+  const envGroupIcon = useMemo(() => {
+    if (envGroup?.type === "doppler") {
+      return doppler;
+    }
+    if (envGroup?.type === "datastore") {
+      return database;
+    }
+    if (envGroup?.type === "infisical") {
+      return infisical;
+    }
+    return key;
+  }, [envGroup?.type]);
+
   return (
     <>
       {isLoading && <Loading />}
@@ -108,16 +126,7 @@ const ExpandedEnv: React.FC = () => {
           <Back to={envGroupPath(currentProject, "")} />
 
           <Container row>
-            <Image
-              src={
-                envGroup.type === "doppler"
-                  ? doppler
-                  : envGroup.type === "datastore"
-                  ? database
-                  : key
-              }
-              size={28}
-            />
+            <Image src={envGroupIcon} size={28} />
             <Spacer inline x={1} />
             <Text size={21}>{envGroupName}</Text>
           </Container>
@@ -145,9 +154,7 @@ const ExpandedEnv: React.FC = () => {
           <Spacer y={1} />
           {match(tab)
             .with("env-vars", () => {
-              return (
-                <EnvVarsTab envGroup={envGroup} fetchEnvGroup={fetchEnvGroup} />
-              );
+              return <EnvVarsTab envGroup={envGroup} fetchEnvGroup={refetch} />;
             })
             .with("synced-apps", () => <SyncedAppsTab envGroup={envGroup} />)
             .with("settings", () => <SettingsTab envGroup={envGroup} />)

+ 20 - 25
dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx

@@ -17,7 +17,7 @@ import {
 import api from "shared/api";
 import { Context } from "shared/Context";
 
-import EnvGroupArray from "../EnvGroupArray";
+import EnvGroupArray, { type KeyValueType } from "../EnvGroupArray";
 
 type Props = {
   envGroup: {
@@ -46,13 +46,7 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
     resolver: zodResolver(envGroupFormValidator),
     reValidateMode: "onSubmit",
   });
-  const {
-    formState: { isValidating, isSubmitting },
-    watch,
-    trigger,
-    handleSubmit,
-    setValue,
-  } = envGroupFormMethods;
+  const { watch, trigger, handleSubmit, setValue } = envGroupFormMethods;
 
   const [submitErrorMessage, setSubmitErrorMessage] = useState<string>("");
   const [isValid, setIsValid] = useState<boolean>(false);
@@ -108,6 +102,14 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
     setValue("envFiles", envGroup.files || []);
   }, [envGroup]);
 
+  const isUpdatable = useMemo(() => {
+    return (
+      envGroup.type !== "doppler" &&
+      envGroup.type !== "datastore" &&
+      envGroup.type !== "infisical"
+    );
+  }, [envGroup.type]);
+
   const onSubmit = handleSubmit(async (data) => {
     setButtonStatus("loading");
     setSubmitErrorMessage("");
@@ -149,7 +151,7 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
           apiEnvVariables[envVar.key] = envVar.value;
         });
 
-      if (envGroup?.type !== "doppler") {
+      if (envGroup?.type !== "doppler" && envGroup?.type !== "infisical") {
         await api.createEnvironmentGroups(
           "<token>",
           {
@@ -178,24 +180,14 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
     }
   });
 
-  const submitButtonStatus = useMemo(() => {
-    if (isSubmitting || isValidating) {
-      return "loading";
-    }
-    if (submitErrorMessage) {
-      return <Error message={submitErrorMessage} />;
-    }
-    return undefined;
-  }, [isSubmitting, submitErrorMessage, isValidating]);
-
   return (
     <>
       <Text size={16}>Environment variables</Text>
       <Spacer y={0.5} />
-      {envGroup.type === "doppler" ? (
+      {envGroup.type === "doppler" || envGroup.type === "infisical" ? (
         <Text color="helper">
-          Doppler environment variables can only be updated from the Doppler
-          dashboard.
+          {envGroup.type === "doppler" ? "Doppler" : "Infisical"} environment
+          variables can only be updated from the Doppler dashboard.
         </Text>
       ) : (
         <Text color="helper">
@@ -216,7 +208,7 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
             }}
             fileUpload={true}
             secretOption={true}
-            disabled={envGroup.type === "doppler"}
+            disabled={!isUpdatable}
           />
           <Spacer y={1} />
           <Text size={16}>Environment files</Text>
@@ -232,7 +224,7 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
               setValue("envFiles", x);
             }}
           />
-          {envGroup.type !== "doppler" && envGroup.type !== "datastore" && (
+          {isUpdatable ? (
             <>
               <Spacer y={1} />
               <Button
@@ -243,17 +235,20 @@ const EnvVarsTab: React.FC<Props> = ({ envGroup, fetchEnvGroup }) => {
                       <i className="material-icons">done</i>
                       Successfully created
                     </StatusWrapper>
+                  ) : submitErrorMessage ? (
+                    "error"
                   ) : (
                     buttonStatus
                   )
                 }
+                errorText={submitErrorMessage}
                 loadingText="Updating env group . . ."
                 disabled={!isValid}
               >
                 Update
               </Button>
             </>
-          )}
+          ) : null}
         </form>
       </FormProvider>
     </>

+ 1 - 1
dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx

@@ -47,7 +47,7 @@ const SettingsTab: React.FC<Props> = ({ envGroup }) => {
   };
 
   const handleDeletionSubmit = async (): Promise<void> => {
-    if (envGroup?.linked_applications) {
+    if (envGroup?.linked_applications.length) {
       setButtonStatus(
         <Error message="Remove this env group from all synced applications to delete." />
       );

+ 30 - 6
dashboard/src/main/home/integrations/DopplerIntegrationList.tsx

@@ -43,6 +43,10 @@ const DopplerIntegrationList: React.FC = (_) => {
       currentCluster?.id,
     ],
     async () => {
+      if (!currentProject || !currentCluster) {
+        return;
+      }
+
       const res = await api.areExternalEnvGroupProvidersEnabled(
         "<token>",
         {},
@@ -50,13 +54,26 @@ const DopplerIntegrationList: React.FC = (_) => {
       );
       const externalEnvGroupProviderStatus = await z
         .object({
-          enabled: z.boolean(),
-          reprovision_required: z.boolean(),
-          k8s_upgrade_required: z.boolean(),
+          operators: z.array(
+            z.object({
+              type: z.enum(["infisical", "external-secrets"]),
+              enabled: z.boolean(),
+              reprovision_required: z.boolean(),
+              k8s_upgrade_required: z.boolean(),
+            })
+          ),
         })
         .parseAsync(res.data);
 
-      return externalEnvGroupProviderStatus;
+      return (
+        externalEnvGroupProviderStatus.operators.find(
+          (o) => o.type === "external-secrets"
+        ) || {
+          enabled: false,
+          reprovision_required: true,
+          k8s_upgrade_required: false,
+        }
+      );
     },
     {
       enabled: !!currentProject && !!currentCluster,
@@ -114,7 +131,7 @@ const DopplerIntegrationList: React.FC = (_) => {
       )
       .then(() => {
         setShowServiceTokenModal(false);
-        history.push("/env-groups");
+        history.push("/environment-groups");
       })
       .catch((err) => {
         let message =
@@ -148,7 +165,14 @@ const DopplerIntegrationList: React.FC = (_) => {
         ) : externalProviderStatus?.reprovision_required ? (
           <Placeholder>
             To enable integration with Doppler, <Spacer inline x={0.5} />
-            <Link to={`/cluster-dashboard`} hasunderline>
+            <Link
+              to={
+                currentCluster?.id
+                  ? `/infrastructure/${currentCluster.id}`
+                  : "/infrastructure"
+              }
+              hasunderline
+            >
               re-provision your cluster
             </Link>
             .

+ 6 - 3
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -14,6 +14,7 @@ import TitleSection from "components/TitleSection";
 import GitlabIntegrationList from "./GitlabIntegrationList";
 import leftArrow from "assets/left-arrow.svg";
 import Spacer from "components/porter/Spacer";
+import InfisicalIntegrationList from "./infisical/InfisicalIntegrationList";
 
 type Props = RouteComponentProps & {
   category: string;
@@ -103,7 +104,7 @@ const IntegrationCategories: React.FC<Props> = (props) => {
 
   useEffect(() => {
     getIntegrationsForCategory(props.category);
-    if (props.category === "doppler") {
+    if (props.category === "doppler" || props.category === "infisical") {
       setLoading(false);
     }
   }, [props.category]);
@@ -131,14 +132,14 @@ const IntegrationCategories: React.FC<Props> = (props) => {
         <TitleSection icon={icon} iconWidth="32px">
           {label}
         </TitleSection>
-        {props.category === "doppler" ? null : (
+        {props.category === "doppler" || props.category === "infisical" ? null : (
           <Button
             onClick={() => {
               if (props.category === "gitlab") {
                 pushFiltered(props, `/integrations/gitlab/create/gitlab`, [
                   "project_id",
                 ]);
-              } else if (props.category === "doppler") {
+              } else if (props.category === "doppler" || props.category === "infisical") {
                 // ret2
               } else if (props.category !== "slack") {
                 setCurrentModal("IntegrationsModal", {
@@ -175,6 +176,8 @@ const IntegrationCategories: React.FC<Props> = (props) => {
         <SlackIntegrationList slackData={slackData} />
       ) : props.category === "doppler" ? (
         <DopplerIntegrationList />
+      ) : props.category === "infisical" ? (
+        <InfisicalIntegrationList />
       ) : (
         <IntegrationList
           currentCategory={props.category}

+ 2 - 2
dashboard/src/main/home/integrations/Integrations.tsx

@@ -21,10 +21,10 @@ const Integrations: React.FC<PropsType> = (props) => {
 
   const IntegrationCategoryStrings = useMemo(() => {
     if (!enableGitlab) {
-      return ["slack", "doppler"];
+      return ["slack", "doppler", "infisical"];
     }
 
-    return ["slack", "doppler", "gitlab"];
+    return ["slack", "doppler", "infisical", "gitlab"];
   }, [enableGitlab]);
 
   return (

+ 162 - 0
dashboard/src/main/home/integrations/infisical/AddInfisicalEnvModal.tsx

@@ -0,0 +1,162 @@
+import React, {
+  useContext,
+  useMemo,
+  useState,
+  type Dispatch,
+  type SetStateAction,
+} from "react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { useHistory } from "react-router";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
+import { ControlledInput } from "components/porter/ControlledInput";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { getErrorMessageFromNetworkCall } from "lib/hooks/useCluster";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+const addInfisicalEnvFormData = z.object({
+  vanityName: z.string(),
+  envSlug: z.string(),
+  envPath: z.string().default("/"),
+  serviceToken: z.string(),
+});
+type AddInfisicalEnvFormData = z.infer<typeof addInfisicalEnvFormData>;
+
+type AddInfisicalEnvModalProps = {
+  setShowAddInfisicalEnvModal: Dispatch<SetStateAction<boolean>>;
+};
+
+export const AddInfisicalEnvModal: React.FC<AddInfisicalEnvModalProps> = ({
+  setShowAddInfisicalEnvModal,
+}) => {
+  const { currentCluster, currentProject } = useContext(Context);
+  const [infisicalEnvGroupCreationError, setInfisicalEnvGroupCreationError] =
+    useState<string>("");
+  const history = useHistory();
+
+  const {
+    register,
+    handleSubmit,
+    formState: { isSubmitting },
+  } = useForm<AddInfisicalEnvFormData>({
+    resolver: zodResolver(addInfisicalEnvFormData),
+    defaultValues: {
+      vanityName: "",
+      envSlug: "",
+      envPath: "/",
+      serviceToken: "",
+    },
+  });
+
+  const vanityName = register("vanityName");
+  const envSlug = register("envSlug");
+  const envPath = register("envPath");
+  const serviceToken = register("serviceToken");
+
+  const onSubmit = handleSubmit(async (data) => {
+    try {
+      setInfisicalEnvGroupCreationError("");
+      if (!currentProject || !currentCluster) {
+        return;
+      }
+
+      await api.createEnvironmentGroups(
+        "<token>",
+        {
+          name: data.vanityName,
+          type: "infisical",
+          auth_token: data.serviceToken,
+          infisical_env: {
+            slug: data.envSlug,
+            path: data.envPath,
+          },
+        },
+        {
+          id: currentProject?.id,
+          cluster_id: currentCluster?.id,
+        }
+      );
+
+      history.push("/environment-groups");
+    } catch (err) {
+      setInfisicalEnvGroupCreationError(
+        getErrorMessageFromNetworkCall(err, "Adding Infisical Env Group")
+      );
+    }
+  });
+
+  const submitStatus = useMemo(() => {
+    if (isSubmitting) {
+      return "loading";
+    }
+  }, [isSubmitting]);
+
+  return (
+    <Modal
+      closeModal={() => {
+        setShowAddInfisicalEnvModal(false);
+      }}
+    >
+      <form onSubmit={onSubmit}>
+        <Text size={16}>Add a new Infisical service token</Text>
+        <Spacer y={1} />
+        <Text color="helper">
+          Your Infisical secrets will be made available to Porter apps as an
+          environment group.
+        </Text>
+        <Spacer y={1} />
+        <ControlledInput
+          type="text"
+          placeholder="ex: my-infisical-env"
+          label="Env group name (vanity name for Porter)"
+          width="100%"
+          height="40px"
+          {...register("vanityName")}
+        />
+        <Spacer y={1} />
+        <ControlledInput
+          type="text"
+          placeholder="ex: dev"
+          label="Env slug"
+          width="100%"
+          height="40px"
+          {...register("envSlug")}
+        />
+        <Spacer y={1} />
+        <ControlledInput
+          type="text"
+          placeholder="ex: /"
+          label="Env path"
+          width="100%"
+          height="40px"
+          {...register("envPath")}
+        />
+        <Spacer y={1} />
+        <ControlledInput
+          type="password"
+          placeholder="ex: st.123...abcdef"
+          label="Infisical service token"
+          width="100%"
+          height="40px"
+          {...register("serviceToken")}
+        />
+        <Spacer y={1} />
+        <Button
+          type="submit"
+          disabled={!vanityName || !envSlug || !envPath || !serviceToken}
+          status={submitStatus}
+          errorText={infisicalEnvGroupCreationError}
+          width="180px"
+        >
+          Add Infisical env group
+        </Button>
+      </form>
+    </Modal>
+  );
+};

+ 187 - 0
dashboard/src/main/home/integrations/infisical/InfisicalIntegrationList.tsx

@@ -0,0 +1,187 @@
+import React, { useContext, useEffect, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { z } from "zod";
+
+import Loading from "components/Loading";
+import Placeholder from "components/Placeholder";
+import Banner from "components/porter/Banner";
+import Button from "components/porter/Button";
+import Link from "components/porter/Link";
+import Spacer from "components/porter/Spacer";
+import ToggleRow from "components/porter/ToggleRow";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+import { AddInfisicalEnvModal } from "./AddInfisicalEnvModal";
+
+const InfisicalIntegrationList: React.FC = (_) => {
+  const [infisicalToggled, setInfisicalToggled] = useState<boolean>(false);
+  const [infisicalEnabled, setInfisicalEnabled] = useState<boolean>(false);
+  const [showServiceTokenModal, setShowServiceTokenModal] =
+    useState<boolean>(false);
+
+  const { currentCluster, currentProject } = useContext(Context);
+
+  const {
+    data: externalProviderStatus,
+    isLoading: isExternalProviderStatusLoading,
+  } = useQuery(
+    [
+      "areExternalEnvGroupProvidersEnabled",
+      currentProject?.id,
+      currentCluster?.id,
+    ],
+    async () => {
+      if (!currentProject || !currentCluster) {
+        return;
+      }
+
+      const res = await api.areExternalEnvGroupProvidersEnabled(
+        "<token>",
+        {},
+        { id: currentProject?.id, cluster_id: currentCluster?.id }
+      );
+      const externalEnvGroupProviderStatus = await z
+        .object({
+          operators: z.array(
+            z.object({
+              type: z.enum(["infisical", "external-secrets"]),
+              enabled: z.boolean(),
+              reprovision_required: z.boolean(),
+              k8s_upgrade_required: z.boolean(),
+            })
+          ),
+        })
+        .parseAsync(res.data);
+
+      return (
+        externalEnvGroupProviderStatus.operators.find(
+          (o) => o.type === "infisical"
+        ) || {
+          enabled: false,
+          reprovision_required: true,
+          k8s_upgrade_required: false,
+        }
+      );
+    },
+    {
+      enabled: !!currentProject && !!currentCluster,
+      refetchInterval: 5000,
+      refetchOnWindowFocus: false,
+    }
+  );
+
+  useEffect(() => {
+    if (externalProviderStatus) {
+      setInfisicalToggled(externalProviderStatus.enabled);
+      setInfisicalEnabled(externalProviderStatus.enabled);
+    }
+  }, [externalProviderStatus]);
+
+  const installInfisical = async (): Promise<void> => {
+    if (!currentCluster || !currentProject) {
+      return;
+    }
+
+    try {
+      setInfisicalToggled(true);
+
+      await api.enableExternalEnvGroupProviders(
+        "<token>",
+        {},
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+    } catch (err) {
+      setInfisicalToggled(false);
+    }
+  };
+
+  if (!infisicalEnabled) {
+    return (
+      <>
+        {isExternalProviderStatusLoading ? (
+          <Placeholder>
+            <Loading message={"Checking status of Infisical integration..."} />
+          </Placeholder>
+        ) : externalProviderStatus?.k8s_upgrade_required ? (
+          <Placeholder>
+            Cluster must be upgraded to Kubernetes v1.27 to integrate with
+            Infisical.
+          </Placeholder>
+        ) : externalProviderStatus?.reprovision_required ? (
+          <Placeholder>
+            To enable integration with Infisical, <Spacer inline x={0.5} />
+            <Link
+              to={
+                currentCluster?.id
+                  ? `/infrastructure/${currentCluster.id}`
+                  : "/infrastructure"
+              }
+              hasunderline
+            >
+              re-provision your cluster
+            </Link>
+            .
+          </Placeholder>
+        ) : (
+          <>
+            <Banner icon="none">
+              <ToggleRow
+                isToggled={infisicalToggled}
+                onToggle={installInfisical}
+                disabled={infisicalToggled}
+              >
+                {infisicalToggled
+                  ? "Enabling Infisical integration . . ."
+                  : "Enable Infisical integration"}
+              </ToggleRow>
+            </Banner>
+            <Spacer y={1} />
+            <Placeholder>
+              Enable the Infisical integration to add environment groups from
+              Infisical.
+            </Placeholder>
+          </>
+        )}
+      </>
+    );
+  }
+
+  return (
+    <>
+      <Banner icon="none">
+        <ToggleRow
+          isToggled={infisicalToggled}
+          onToggle={installInfisical}
+          disabled={infisicalToggled}
+        >
+          {infisicalToggled
+            ? infisicalEnabled
+              ? "Infisical integration enabled"
+              : "Enabling Infisical integration . . ."
+            : "Enable Infisical integration"}
+        </ToggleRow>
+      </Banner>
+      <Spacer y={1} />
+      <Button
+        onClick={() => {
+          setShowServiceTokenModal(true);
+        }}
+      >
+        + Add Infisical env group
+      </Button>
+
+      {showServiceTokenModal && (
+        <AddInfisicalEnvModal
+          setShowAddInfisicalEnvModal={setShowServiceTokenModal}
+        />
+      )}
+    </>
+  );
+};
+
+export default InfisicalIntegrationList;

+ 4 - 0
dashboard/src/shared/api.tsx

@@ -2313,6 +2313,10 @@ const createEnvironmentGroups = baseApi<
     type?: string;
     auth_token?: string;
     is_env_override?: boolean;
+    infisical_env?: {
+      slug: string;
+      path: string;
+    }
   },
   {
     id: number;

+ 6 - 0
dashboard/src/shared/common.tsx

@@ -4,6 +4,7 @@ import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
 import azure from "assets/azure.png";
 import doppler from "assets/doppler.png";
+import infisical from "assets/infisical.svg";
 
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
@@ -20,6 +21,11 @@ export const integrationList: any = {
     label: "Doppler",
     buttonText: "Add a service token",
   },
+  infisical: {
+    icon: infisical,
+    label: "Infisical",
+    buttonText: "Add an API key",
+  },
   kubernetes: {
     icon:
       "https://upload.wikimedia.org/wikipedia/labs/thumb/b/ba/Kubernetes-icon-color.svg/2110px-Kubernetes-icon-color.svg.png",