mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feat(e2e-tests): secret rotations
This commit is contained in:
3
.github/workflows/run-backend-tests.yml
vendored
3
.github/workflows/run-backend-tests.yml
vendored
@@ -34,6 +34,9 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: Start postgres and redis
|
- name: Start postgres and redis
|
||||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||||
|
- name: Login to Oracle Container Registry
|
||||||
|
run: echo "${{ secrets.ORACLE_DOCKER_REGISTRY_PASSWORD }}" | docker login container-registry.oracle.com -u "${{ secrets.ORACLE_DOCKER_REGISTRY_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: Start Secret Rotation testing databases
|
- name: Start Secret Rotation testing databases
|
||||||
run: docker compose -f docker-compose.e2e-dbs.yml up -d
|
run: docker compose -f docker-compose.e2e-dbs.yml up -d
|
||||||
- name: Run unit test
|
- name: Run unit test
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { seedData1 } from "@app/db/seed-data";
|
|||||||
|
|
||||||
enum SecretRotationType {
|
enum SecretRotationType {
|
||||||
OracleDb = "oracledb",
|
OracleDb = "oracledb",
|
||||||
MySQL = "mysql"
|
MySQL = "mysql",
|
||||||
|
Postgres = "postgres"
|
||||||
}
|
}
|
||||||
|
|
||||||
type TGenericSqlCredentials = {
|
type TGenericSqlCredentials = {
|
||||||
@@ -147,6 +148,40 @@ const createMySQLAppConnection = async (credentials: TGenericSqlCredentials) =>
|
|||||||
return json.appConnection.id as string;
|
return json.appConnection.id as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPostgresAppConnection = async (credentials: TGenericSqlCredentials) => {
|
||||||
|
const createPostgresAppConnectionReqBody = {
|
||||||
|
credentials: {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
database: credentials.database,
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
sslEnabled: false,
|
||||||
|
sslRejectUnauthorized: true
|
||||||
|
},
|
||||||
|
name: `postgres-test-${uuidv4()}`,
|
||||||
|
description: "test-postgres",
|
||||||
|
gatewayId: null,
|
||||||
|
method: "username-and-password"
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await testServer.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/app-connections/postgres`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
|
},
|
||||||
|
body: createPostgresAppConnectionReqBody
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = JSON.parse(res.payload);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(json.appConnection).toBeDefined();
|
||||||
|
|
||||||
|
return json.appConnection.id as string;
|
||||||
|
};
|
||||||
|
|
||||||
const createOracleInfisicalUsers = async (
|
const createOracleInfisicalUsers = async (
|
||||||
credentials: TGenericSqlCredentials,
|
credentials: TGenericSqlCredentials,
|
||||||
userCredentials: TDatabaseUserCredentials[]
|
userCredentials: TDatabaseUserCredentials[]
|
||||||
@@ -169,7 +204,7 @@ const createOracleInfisicalUsers = async (
|
|||||||
|
|
||||||
for await (const { username } of userCredentials) {
|
for await (const { username } of userCredentials) {
|
||||||
// check if user exists, and if it does, don't create it
|
// check if user exists, and if it does, don't create it
|
||||||
const existingUser = await client.raw(`SELECT * FROM all_users WHERE username = '${username.toUpperCase()}'`);
|
const existingUser = await client.raw(`SELECT * FROM all_users WHERE username = '${username}'`);
|
||||||
|
|
||||||
if (!existingUser.length) {
|
if (!existingUser.length) {
|
||||||
await client.raw(`CREATE USER ${username} IDENTIFIED BY "temporary_password"`);
|
await client.raw(`CREATE USER ${username} IDENTIFIED BY "temporary_password"`);
|
||||||
@@ -220,6 +255,36 @@ const createMySQLInfisicalUsers = async (
|
|||||||
await client.destroy();
|
await client.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPostgresInfisicalUsers = async (
|
||||||
|
credentials: TGenericSqlCredentials,
|
||||||
|
userCredentials: TDatabaseUserCredentials[]
|
||||||
|
) => {
|
||||||
|
const client = knex({
|
||||||
|
client: "pg",
|
||||||
|
connection: {
|
||||||
|
database: credentials.database,
|
||||||
|
port: credentials.port,
|
||||||
|
host: credentials.host,
|
||||||
|
user: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
connectionTimeoutMillis: 10000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const { username } of userCredentials) {
|
||||||
|
// check if user exists, and if it does, don't create it
|
||||||
|
const existingUser = await client.raw("SELECT * FROM pg_catalog.pg_user WHERE usename = ?", [username]);
|
||||||
|
|
||||||
|
if (!existingUser.rows.length) {
|
||||||
|
await client.raw(`CREATE USER "${username}" WITH PASSWORD 'temporary_password'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.raw("GRANT ALL PRIVILEGES ON DATABASE ?? TO ??", [credentials.database, username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
const createOracleDBSecretRotation = async (
|
const createOracleDBSecretRotation = async (
|
||||||
appConnectionId: string,
|
appConnectionId: string,
|
||||||
credentials: TGenericSqlCredentials,
|
credentials: TGenericSqlCredentials,
|
||||||
@@ -324,6 +389,58 @@ const createMySQLSecretRotation = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPostgresSecretRotation = async (
|
||||||
|
appConnectionId: string,
|
||||||
|
credentials: TGenericSqlCredentials,
|
||||||
|
userCredentials: TDatabaseUserCredentials[],
|
||||||
|
secretMapping: TSecretMapping
|
||||||
|
) => {
|
||||||
|
const now = new Date();
|
||||||
|
const rotationTime = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
|
||||||
|
|
||||||
|
await createPostgresInfisicalUsers(credentials, userCredentials);
|
||||||
|
|
||||||
|
const createPostgresSecretRotationReqBody = {
|
||||||
|
parameters: userCredentials.reduce(
|
||||||
|
(acc, user, index) => {
|
||||||
|
acc[`username${index + 1}`] = user.username;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
),
|
||||||
|
secretsMapping: {
|
||||||
|
username: secretMapping.username,
|
||||||
|
password: secretMapping.password
|
||||||
|
},
|
||||||
|
name: `test-postgres-rotation-${uuidv4()}`,
|
||||||
|
description: "Test Postgres Secret Rotation",
|
||||||
|
secretPath: "/",
|
||||||
|
isAutoRotationEnabled: true,
|
||||||
|
rotationInterval: 5,
|
||||||
|
rotateAtUtc: {
|
||||||
|
hours: rotationTime.getUTCHours(),
|
||||||
|
minutes: rotationTime.getUTCMinutes()
|
||||||
|
},
|
||||||
|
connectionId: appConnectionId,
|
||||||
|
environment: seedData1.environment.slug,
|
||||||
|
projectId: seedData1.projectV3.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await testServer.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v2/secret-rotations/postgres-credentials`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
|
},
|
||||||
|
body: createPostgresSecretRotationReqBody
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.json().secretRotation).toBeDefined();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
describe("Secret Rotations", async () => {
|
describe("Secret Rotations", async () => {
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
@@ -349,6 +466,52 @@ describe("Secret Rotations", async () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.MySQL,
|
||||||
|
name: "MySQL (8.0.29) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "mysql-test",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "root",
|
||||||
|
password: "mysql-test",
|
||||||
|
port: 3307
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("MYSQL_USERNAME"),
|
||||||
|
password: formatSqlUsername("MYSQL_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("MYSQL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("MYSQL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.MySQL,
|
||||||
|
name: "MySQL (5.7.31) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "mysql-test",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "root",
|
||||||
|
password: "mysql-test",
|
||||||
|
port: 3308
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("MYSQL_USERNAME"),
|
||||||
|
password: formatSqlUsername("MYSQL_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("MYSQL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("MYSQL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: SecretRotationType.OracleDb,
|
type: SecretRotationType.OracleDb,
|
||||||
name: "OracleDB (23.8) Secret Rotation",
|
name: "OracleDB (23.8) Secret Rotation",
|
||||||
@@ -371,6 +534,98 @@ describe("Secret Rotations", async () => {
|
|||||||
username: formatSqlUsername("INFISICAL_USER_2")
|
username: formatSqlUsername("INFISICAL_USER_2")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.OracleDb,
|
||||||
|
name: "OracleDB (19.3) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "ORCLPDB1",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "system",
|
||||||
|
password: "OrCAKF112aaSfAdfdA2Ac3@@!",
|
||||||
|
port: 1522
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("ORACLEDB_USERNAME"),
|
||||||
|
password: formatSqlUsername("ORACLEDB_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.Postgres,
|
||||||
|
name: "Postgres (17) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "postgres-test",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "postgres-test",
|
||||||
|
password: "postgres-test",
|
||||||
|
port: 5433
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||||
|
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.Postgres,
|
||||||
|
name: "Postgres (16) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "postgres-test",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "postgres-test",
|
||||||
|
password: "postgres-test",
|
||||||
|
port: 5434
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||||
|
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SecretRotationType.Postgres,
|
||||||
|
name: "Postgres (10.12) Secret Rotation",
|
||||||
|
dbCredentials: {
|
||||||
|
database: "postgres-test",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
username: "postgres-test",
|
||||||
|
password: "postgres-test",
|
||||||
|
port: 5435
|
||||||
|
},
|
||||||
|
secretMapping: {
|
||||||
|
username: formatSqlUsername("POSTGRES_USERNAME"),
|
||||||
|
password: formatSqlUsername("POSTGRES_PASSWORD")
|
||||||
|
},
|
||||||
|
userCredentials: [
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_1")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: formatSqlUsername("INFISICAL_USER_2")
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
] as {
|
] as {
|
||||||
type: SecretRotationType;
|
type: SecretRotationType;
|
||||||
@@ -382,12 +637,14 @@ describe("Secret Rotations", async () => {
|
|||||||
|
|
||||||
const createAppConnectionMap = {
|
const createAppConnectionMap = {
|
||||||
[SecretRotationType.OracleDb]: createOracleDBAppConnection,
|
[SecretRotationType.OracleDb]: createOracleDBAppConnection,
|
||||||
[SecretRotationType.MySQL]: createMySQLAppConnection
|
[SecretRotationType.MySQL]: createMySQLAppConnection,
|
||||||
|
[SecretRotationType.Postgres]: createPostgresAppConnection
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRotationMap = {
|
const createRotationMap = {
|
||||||
[SecretRotationType.OracleDb]: createOracleDBSecretRotation,
|
[SecretRotationType.OracleDb]: createOracleDBSecretRotation,
|
||||||
[SecretRotationType.MySQL]: createMySQLSecretRotation
|
[SecretRotationType.MySQL]: createMySQLSecretRotation,
|
||||||
|
[SecretRotationType.Postgres]: createPostgresSecretRotation
|
||||||
};
|
};
|
||||||
|
|
||||||
const appConnectionIds: { id: string; type: SecretRotationType }[] = [];
|
const appConnectionIds: { id: string; type: SecretRotationType }[] = [];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# Oracle Databases
|
||||||
oracle-db-23.8:
|
oracle-db-23.8:
|
||||||
image: container-registry.oracle.com/database/free:23.8.0.0
|
image: container-registry.oracle.com/database/free:23.8.0.0
|
||||||
container_name: oracle-db-23.8
|
container_name: oracle-db-23.8
|
||||||
@@ -18,6 +19,26 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
oracle-db-19.19:
|
||||||
|
# Official Oracle 19.19.0.0 - requires docker login container-registry.oracle.com
|
||||||
|
image: container-registry.oracle.com/database/enterprise:19.19.0.0
|
||||||
|
container_name: oracle-db-19.19
|
||||||
|
ports:
|
||||||
|
- "1522:1521"
|
||||||
|
environment:
|
||||||
|
- ORACLE_SID=ORCLCDB
|
||||||
|
- ORACLE_PDB=ORCLPDB1
|
||||||
|
- ORACLE_PWD=OrCAKF112aaSfAdfdA2Ac3@@!
|
||||||
|
- ORACLE_EDITION=enterprise
|
||||||
|
- ORACLE_CHARACTERSET=AL32UTF8
|
||||||
|
volumes:
|
||||||
|
- oracle-data-19.19:/opt/oracle/oradata
|
||||||
|
shm_size: 2gb
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
disable: true
|
||||||
|
|
||||||
|
# MySQL Databases
|
||||||
mysql-8.4.6:
|
mysql-8.4.6:
|
||||||
image: mysql:8.4.6
|
image: mysql:8.4.6
|
||||||
container_name: mysql-8.4.6
|
container_name: mysql-8.4.6
|
||||||
@@ -38,6 +59,113 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
mysql-8.0.29:
|
||||||
|
image: mysql:8.0.29
|
||||||
|
container_name: mysql-8.0.28
|
||||||
|
ports:
|
||||||
|
- "3307:3306"
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=mysql-test
|
||||||
|
- MYSQL_DATABASE=mysql-test
|
||||||
|
- MYSQL_ROOT_HOST=%
|
||||||
|
- MYSQL_USER=mysql-test
|
||||||
|
- MYSQL_PASSWORD=mysql-test
|
||||||
|
volumes:
|
||||||
|
- mysql-data-8.0.29:/var/lib/mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "mysql-test", "-pmysql-test"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
mysql-5.7.31:
|
||||||
|
image: mysql:5.7.31
|
||||||
|
container_name: mysql-5.7.31
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "3308:3306"
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=mysql-test
|
||||||
|
- MYSQL_DATABASE=mysql-test
|
||||||
|
- MYSQL_ROOT_HOST=%
|
||||||
|
- MYSQL_USER=mysql-test
|
||||||
|
- MYSQL_PASSWORD=mysql-test
|
||||||
|
volumes:
|
||||||
|
- mysql-data-5.7.31:/var/lib/mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "mysql-test", "-pmysql-test"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# PostgreSQL Databases
|
||||||
|
postgres-17:
|
||||||
|
image: postgres:17
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: postgres-17
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=postgres-test
|
||||||
|
- POSTGRES_USER=postgres-test
|
||||||
|
- POSTGRES_PASSWORD=postgres-test
|
||||||
|
volumes:
|
||||||
|
- postgres-data-17:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
postgres-16:
|
||||||
|
image: postgres:16
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: postgres-16
|
||||||
|
ports:
|
||||||
|
- "5434:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=postgres-test
|
||||||
|
- POSTGRES_USER=postgres-test
|
||||||
|
- POSTGRES_PASSWORD=postgres-test
|
||||||
|
volumes:
|
||||||
|
- postgres-data-16:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
postgres-10.12:
|
||||||
|
image: postgres:10.12
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: postgres-10.12
|
||||||
|
ports:
|
||||||
|
- "5435:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=postgres-test
|
||||||
|
- POSTGRES_USER=postgres-test
|
||||||
|
- POSTGRES_PASSWORD=postgres-test
|
||||||
|
volumes:
|
||||||
|
- postgres-data-10.12:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres-test -d postgres-test"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
oracle-data-23.8:
|
oracle-data-23.8:
|
||||||
mysql-data-8.4.6:
|
oracle-data-19.19:
|
||||||
|
mysql-data-8.4.6:
|
||||||
|
mysql-data-8.0.29:
|
||||||
|
mysql-data-5.7.31:
|
||||||
|
postgres-data-17:
|
||||||
|
postgres-data-16:
|
||||||
|
postgres-data-10.12:
|
||||||
Reference in New Issue
Block a user