mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add k8s stuff
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateKubernetesAccountSchema,
|
||||
SanitizedKubernetesAccountWithResourceSchema,
|
||||
UpdateKubernetesAccountSchema
|
||||
} from "@app/ee/services/pam-resource/kubernetes/kubernetes-resource-schemas";
|
||||
import {
|
||||
CreateMySQLAccountSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema,
|
||||
@@ -44,5 +49,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
||||
createAccountSchema: CreateSSHAccountSchema,
|
||||
updateAccountSchema: UpdateSSHAccountSchema
|
||||
});
|
||||
},
|
||||
[PamResource.Kubernetes]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.Kubernetes,
|
||||
accountResponseSchema: SanitizedKubernetesAccountWithResourceSchema,
|
||||
createAccountSchema: CreateKubernetesAccountSchema,
|
||||
updateAccountSchema: UpdateKubernetesAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateKubernetesResourceSchema,
|
||||
SanitizedKubernetesResourceSchema,
|
||||
UpdateKubernetesResourceSchema
|
||||
} from "@app/ee/services/pam-resource/kubernetes/kubernetes-resource-schemas";
|
||||
import {
|
||||
CreateMySQLResourceSchema,
|
||||
MySQLResourceSchema,
|
||||
@@ -44,5 +49,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
||||
createResourceSchema: CreateSSHResourceSchema,
|
||||
updateResourceSchema: UpdateSSHResourceSchema
|
||||
});
|
||||
},
|
||||
[PamResource.Kubernetes]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.Kubernetes,
|
||||
resourceResponseSchema: SanitizedKubernetesResourceSchema,
|
||||
createResourceSchema: CreateKubernetesResourceSchema,
|
||||
updateResourceSchema: UpdateKubernetesResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
KubernetesResourceListItemSchema,
|
||||
SanitizedKubernetesResourceSchema
|
||||
} from "@app/ee/services/pam-resource/kubernetes/kubernetes-resource-schemas";
|
||||
import {
|
||||
MySQLResourceListItemSchema,
|
||||
SanitizedMySQLResourceSchema
|
||||
@@ -22,13 +26,15 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SanitizedResourceSchema = z.union([
|
||||
SanitizedPostgresResourceSchema,
|
||||
SanitizedMySQLResourceSchema,
|
||||
SanitizedSSHResourceSchema
|
||||
SanitizedSSHResourceSchema,
|
||||
SanitizedKubernetesResourceSchema
|
||||
]);
|
||||
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||
PostgresResourceListItemSchema,
|
||||
MySQLResourceListItemSchema,
|
||||
SSHResourceListItemSchema
|
||||
SSHResourceListItemSchema,
|
||||
KubernetesResourceListItemSchema
|
||||
]);
|
||||
|
||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum KubernetesAuthMethod {
|
||||
ServiceAccountToken = "service-account-token"
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { GatewayProxyProtocol, withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
|
||||
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
TPamResourceFactory,
|
||||
TPamResourceFactoryRotateAccountCredentials,
|
||||
TPamResourceFactoryValidateAccountCredentials
|
||||
} from "../pam-resource-types";
|
||||
import { KubernetesAuthMethod } from "./kubernetes-resource-enums";
|
||||
import { TKubernetesAccountCredentials, TKubernetesResourceConnectionDetails } from "./kubernetes-resource-types";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
export const executeWithGateway = async <T>(
|
||||
config: {
|
||||
connectionDetails: TKubernetesResourceConnectionDetails;
|
||||
resourceType: PamResource;
|
||||
gatewayId: string;
|
||||
},
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
operation: (baseUrl: string, httpsAgent?: https.Agent) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const { connectionDetails, gatewayId } = config;
|
||||
const url = new URL(connectionDetails.url);
|
||||
const [targetHost] = await verifyHostInputValidity(url.hostname, true);
|
||||
const targetPort = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
|
||||
|
||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId,
|
||||
targetHost,
|
||||
targetPort
|
||||
});
|
||||
|
||||
if (!platformConnectionDetails) {
|
||||
throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" });
|
||||
}
|
||||
|
||||
let httpsAgent: https.Agent | undefined;
|
||||
if (connectionDetails.caCertificate) {
|
||||
httpsAgent = new https.Agent({
|
||||
ca: connectionDetails.caCertificate,
|
||||
rejectUnauthorized: !connectionDetails.skipTLSVerify
|
||||
});
|
||||
} else if (!connectionDetails.skipTLSVerify) {
|
||||
httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: true
|
||||
});
|
||||
}
|
||||
|
||||
return withGatewayV2Proxy(
|
||||
async (proxyPort) => {
|
||||
const protocol = url.protocol === "https:" ? "https" : "http";
|
||||
const baseUrl = `${protocol}://localhost:${proxyPort}`;
|
||||
return operation(baseUrl, httpsAgent);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
relayHost: platformConnectionDetails.relayHost,
|
||||
gateway: platformConnectionDetails.gateway,
|
||||
relay: platformConnectionDetails.relay,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const kubernetesResourceFactory: TPamResourceFactory<
|
||||
TKubernetesResourceConnectionDetails,
|
||||
TKubernetesAccountCredentials
|
||||
> = (resourceType, connectionDetails, gatewayId, gatewayV2Service) => {
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
await executeWithGateway(
|
||||
{ connectionDetails, gatewayId, resourceType },
|
||||
gatewayV2Service,
|
||||
async (baseUrl, httpsAgent) => {
|
||||
// Validate connection by checking API server version
|
||||
try {
|
||||
await axios.get(`${baseUrl}/version`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
...(httpsAgent ? { httpsAgent } : {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
// If we get a 401/403, it means we reached the API server but need auth - that's fine for connection validation
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
logger.info(
|
||||
{ status: error.response.status },
|
||||
"[Kubernetes Resource Factory] Kubernetes connection validation succeeded (auth required)"
|
||||
);
|
||||
return connectionDetails;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Unable to connect to Kubernetes API server: ${error.response?.statusText || error.message}`
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info("[Kubernetes Resource Factory] Kubernetes connection validation succeeded");
|
||||
return connectionDetails;
|
||||
}
|
||||
);
|
||||
return connectionDetails;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<
|
||||
TKubernetesAccountCredentials
|
||||
> = async (credentials) => {
|
||||
try {
|
||||
await executeWithGateway(
|
||||
{ connectionDetails, gatewayId, resourceType },
|
||||
gatewayV2Service,
|
||||
async (baseUrl, httpsAgent) => {
|
||||
if (credentials.authMethod === KubernetesAuthMethod.ServiceAccountToken) {
|
||||
// Validate service account token by making an authenticated API call
|
||||
try {
|
||||
await axios.get(`${baseUrl}/api/v1/namespaces/${connectionDetails.namespace}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${credentials.serviceAccountToken}`
|
||||
},
|
||||
...(httpsAgent ? { httpsAgent } : {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{ serviceAccountName: credentials.serviceAccountName, namespace: connectionDetails.namespace },
|
||||
"[Kubernetes Resource Factory] Kubernetes service account token authentication successful"
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Account credentials invalid. Service account token is not valid or does not have required permissions."
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate account credentials: ${error.response?.statusText || error.message}`
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported Kubernetes auth method: ${(credentials as TKubernetesAccountCredentials).authMethod}`
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<TKubernetesAccountCredentials> = async (
|
||||
rotationAccountCredentials
|
||||
) => {
|
||||
// For Kubernetes, rotation would typically involve creating a new service account token
|
||||
// This is a placeholder - actual rotation logic would need to be implemented based on requirements
|
||||
return rotationAccountCredentials;
|
||||
};
|
||||
|
||||
const handleOverwritePreventionForCensoredValues = async (
|
||||
updatedAccountCredentials: TKubernetesAccountCredentials,
|
||||
currentCredentials: TKubernetesAccountCredentials
|
||||
) => {
|
||||
if (updatedAccountCredentials.authMethod !== currentCredentials.authMethod) {
|
||||
return updatedAccountCredentials;
|
||||
}
|
||||
|
||||
if (
|
||||
updatedAccountCredentials.authMethod === KubernetesAuthMethod.ServiceAccountToken &&
|
||||
currentCredentials.authMethod === KubernetesAuthMethod.ServiceAccountToken
|
||||
) {
|
||||
if (updatedAccountCredentials.serviceAccountToken === "__INFISICAL_UNCHANGED__") {
|
||||
return {
|
||||
...updatedAccountCredentials,
|
||||
serviceAccountToken: currentCredentials.serviceAccountToken
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return updatedAccountCredentials;
|
||||
};
|
||||
|
||||
return {
|
||||
validateConnection,
|
||||
validateAccountCredentials,
|
||||
rotateAccountCredentials,
|
||||
handleOverwritePreventionForCensoredValues
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { KubernetesResourceListItemSchema } from "./kubernetes-resource-schemas";
|
||||
|
||||
export const getKubernetesResourceListItem = () => {
|
||||
return {
|
||||
name: KubernetesResourceListItemSchema.shape.name.value,
|
||||
resource: KubernetesResourceListItemSchema.shape.resource.value
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import { KubernetesAuthMethod } from "./kubernetes-resource-enums";
|
||||
|
||||
export const BaseKubernetesResourceSchema = BasePamResourceSchema.extend({
|
||||
resourceType: z.literal(PamResource.Kubernetes)
|
||||
});
|
||||
|
||||
export const KubernetesResourceListItemSchema = z.object({
|
||||
name: z.literal("Kubernetes"),
|
||||
resource: z.literal(PamResource.Kubernetes)
|
||||
});
|
||||
|
||||
export const KubernetesResourceConnectionDetailsSchema = z.object({
|
||||
url: z.string().url().trim().max(500),
|
||||
namespace: z.string().trim().max(255),
|
||||
skipTLSVerify: z.boolean().optional().default(false),
|
||||
caCertificate: z.string().trim().max(10000).optional()
|
||||
});
|
||||
|
||||
export const KubernetesServiceAccountTokenCredentialsSchema = z.object({
|
||||
authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken),
|
||||
serviceAccountName: z.string().trim().max(255),
|
||||
serviceAccountToken: z.string().trim().max(10000)
|
||||
});
|
||||
|
||||
export const KubernetesAccountCredentialsSchema = z.discriminatedUnion("authMethod", [
|
||||
KubernetesServiceAccountTokenCredentialsSchema
|
||||
]);
|
||||
|
||||
export const KubernetesResourceSchema = BaseKubernetesResourceSchema.extend({
|
||||
connectionDetails: KubernetesResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: KubernetesAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const SanitizedKubernetesResourceSchema = BaseKubernetesResourceSchema.extend({
|
||||
connectionDetails: KubernetesResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: z
|
||||
.discriminatedUnion("authMethod", [
|
||||
z.object({
|
||||
authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken),
|
||||
serviceAccountName: z.string()
|
||||
})
|
||||
])
|
||||
.nullable()
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const CreateKubernetesResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
connectionDetails: KubernetesResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: KubernetesAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateKubernetesResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
connectionDetails: KubernetesResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: KubernetesAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const KubernetesAccountSchema = BasePamAccountSchema.extend({
|
||||
credentials: KubernetesAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreateKubernetesAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||
credentials: KubernetesAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateKubernetesAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||
credentials: KubernetesAccountCredentialsSchema.optional()
|
||||
});
|
||||
|
||||
export const SanitizedKubernetesAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||
credentials: z.discriminatedUnion("authMethod", [
|
||||
z.object({
|
||||
authMethod: z.literal(KubernetesAuthMethod.ServiceAccountToken),
|
||||
serviceAccountName: z.string()
|
||||
})
|
||||
])
|
||||
});
|
||||
|
||||
// Sessions
|
||||
export const KubernetesSessionCredentialsSchema = KubernetesResourceConnectionDetailsSchema.and(
|
||||
KubernetesAccountCredentialsSchema
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
KubernetesAccountCredentialsSchema,
|
||||
KubernetesAccountSchema,
|
||||
KubernetesResourceConnectionDetailsSchema,
|
||||
KubernetesResourceSchema
|
||||
} from "./kubernetes-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TKubernetesResource = z.infer<typeof KubernetesResourceSchema>;
|
||||
export type TKubernetesResourceConnectionDetails = z.infer<typeof KubernetesResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TKubernetesAccount = z.infer<typeof KubernetesAccountSchema>;
|
||||
export type TKubernetesAccountCredentials = z.infer<typeof KubernetesAccountCredentialsSchema>;
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql",
|
||||
SSH = "ssh"
|
||||
SSH = "ssh",
|
||||
Kubernetes = "kubernetes"
|
||||
}
|
||||
|
||||
export enum PamResourceOrderBy {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
|
||||
import { kubernetesResourceFactory } from "./kubernetes/kubernetes-resource-factory";
|
||||
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||
import { sshResourceFactory } from "./ssh/ssh-resource-factory";
|
||||
|
||||
@@ -8,5 +9,6 @@ type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnect
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation
|
||||
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.Kubernetes]: kubernetesResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||
import { getKubernetesResourceListItem } from "./kubernetes/kubernetes-resource-fns";
|
||||
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem(), getKubernetesResourceListItem()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
};
|
||||
|
||||
// Resource
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import {
|
||||
TKubernetesAccount,
|
||||
TKubernetesAccountCredentials,
|
||||
TKubernetesResource,
|
||||
TKubernetesResourceConnectionDetails
|
||||
} from "./kubernetes/kubernetes-resource-types";
|
||||
import {
|
||||
TMySQLAccount,
|
||||
TMySQLAccountCredentials,
|
||||
@@ -22,16 +28,21 @@ import {
|
||||
} from "./ssh/ssh-resource-types";
|
||||
|
||||
// Resource types
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource;
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource;
|
||||
export type TPamResourceConnectionDetails =
|
||||
| TPostgresResourceConnectionDetails
|
||||
| TMySQLResourceConnectionDetails
|
||||
| TSSHResourceConnectionDetails;
|
||||
| TSSHResourceConnectionDetails
|
||||
| TKubernetesResourceConnectionDetails;
|
||||
|
||||
// Account types
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount;
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials | TSSHAccountCredentials;
|
||||
export type TPamAccountCredentials =
|
||||
| TPostgresAccountCredentials
|
||||
| TMySQLAccountCredentials
|
||||
| TSSHAccountCredentials
|
||||
| TKubernetesAccountCredentials;
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreateResourceDTO = Pick<
|
||||
|
||||
Reference in New Issue
Block a user