From b8417b156472db84d3048798da8824fd118b98c2 Mon Sep 17 00:00:00 2001
From: Yaroslav Svitlytskyi <53532703+YarikRevich@users.noreply.github.com>
Date: Thu, 5 Dec 2024 12:16:24 +0100
Subject: [PATCH] Feature: add postgres support (#21)
* feature: added postgres support
* fix: fixed bugs
---
README.md | 28 +++-
api-server/pom.xml | 4 +
.../entity/common/ConfigEntity.java | 49 +++++++
.../entity/common/PropertiesEntity.java | 9 ++
...figDatabasePropertiesMissingException.java | 21 +++
.../repository/ConfigRepository.java | 121 ----------------
.../repository/ContentRepository.java | 33 +++--
.../repository/ProviderRepository.java | 52 +++++--
.../repository/SecretRepository.java | 51 ++++---
.../repository/TemporateRepository.java | 121 ++++++++++++----
.../executor/RepositoryExecutor.java | 40 ++++--
.../integration/backup/BackupService.java | 2 -
.../diagnostics/DiagnosticsConfigService.java | 36 +----
.../DatabasePropertiesConfigService.java | 130 +++++++++++++++++
.../SecurityPropertiesConfigService.java | 6 +-
.../service/state/watcher/WatcherService.java | 7 +-
.../io.smallrye.config.ConfigSourceFactory | 3 +-
.../src/main/resources/application.properties | 13 +-
.../resources/liquibase/postgres/config.yaml | 134 ++++++++++++++++++
.../liquibase/{ => postgres}/data/data.csv | 0
.../liquibase/{ => sqlite3}/config.yaml | 31 ----
.../resources/liquibase/sqlite3/data/data.csv | 3 +
config/grafana/dashboards/diagnostics.tmpl | 118 +++++----------
docs/internal-database-design.md | 13 +-
docs/internal-database-design.png | Bin 37487 -> 32543 bytes
docs/internal-storage-design.md | 4 +-
docs/internal-storage-design.png | Bin 8203 -> 8867 bytes
pom.xml | 5 +
samples/config/api-server/api-server.yaml | 14 ++
29 files changed, 672 insertions(+), 376 deletions(-)
create mode 100644 api-server/src/main/java/com/objectstorage/exception/ConfigDatabasePropertiesMissingException.java
delete mode 100644 api-server/src/main/java/com/objectstorage/repository/ConfigRepository.java
create mode 100644 api-server/src/main/java/com/objectstorage/service/integration/properties/database/DatabasePropertiesConfigService.java
create mode 100644 api-server/src/main/resources/liquibase/postgres/config.yaml
rename api-server/src/main/resources/liquibase/{ => postgres}/data/data.csv (100%)
rename api-server/src/main/resources/liquibase/{ => sqlite3}/config.yaml (80%)
create mode 100644 api-server/src/main/resources/liquibase/sqlite3/data/data.csv
diff --git a/README.md b/README.md
index 559538a..2b6c80f 100644
--- a/README.md
+++ b/README.md
@@ -102,11 +102,23 @@ connection:
# Represents password, which will be used to decode operations.
password: "test123"
-# Represents section used for ObjectStorage API Server temporate storage configuration.
-temporate-storage:
- # Represents format used for content to be saved.
- format: "zip"
+# Represents section used for ObjectStorage API Server internal database configuration.
+internal-storage:
+ # Represents provider selected for ObjectStorage internal database. Supported providers are "sqlite3" and "postgres" only.
+ provider: "sqlite3"
+
+ # Represents host for the previously selected ObjectStorage internal database provider, works only for "postgres".
+ # host: "localhost:5432"
+
+ # Represents username for the previously selected ObjectStorage internal database provider.
+ username: "objectstorage_user"
+
+ # Represents password for the previously selected ObjectStorage internal database provider.
+ password: "objectstorage_password"
+# Represents section used for ObjectStorage API Server temporate storage configuration. Same compression will be
+# used to upload files to the configured cloud providers.
+temporate-storage:
# Represents frequency of scheduled operations processing.
frequency: "*/5 * * * * ?"
@@ -118,10 +130,13 @@ backup:
# Represents frequency of backup operation for selected provider.
frequency: "0 */5 * * * ?"
+ # Represents the highest amount of downloaded backup content versions per each workspace.
+ max-versions: 5
+
# Represents section used for ObjectStorage API Server diagnostics configuration.
diagnostics:
# Enables diagnostics functionality.
- enabled: false
+ enabled: true
# Represents section used for ObjectStorage diagnostics metrics configuration.
metrics:
@@ -144,6 +159,9 @@ diagnostics:
port: 8121
```
+In the **~/.objectstorage/internal/database** directory there will be located internal database data, if **sqlite3**
+option is selected as target database.
+
### Diagnostics dashboard
For **ObjectStorage API Server** configuration the following section should be modified:
\ No newline at end of file
diff --git a/api-server/pom.xml b/api-server/pom.xml
index 6a79f68..2f99a8e 100644
--- a/api-server/pom.xml
+++ b/api-server/pom.xml
@@ -100,6 +100,10 @@
io.quarkiverse.jdbc
quarkus-jdbc-sqlite
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
io.quarkus
quarkus-smallrye-jwt
diff --git a/api-server/src/main/java/com/objectstorage/entity/common/ConfigEntity.java b/api-server/src/main/java/com/objectstorage/entity/common/ConfigEntity.java
index 361f673..347d4f3 100644
--- a/api-server/src/main/java/com/objectstorage/entity/common/ConfigEntity.java
+++ b/api-server/src/main/java/com/objectstorage/entity/common/ConfigEntity.java
@@ -51,6 +51,55 @@ public static class Security {
@JsonProperty("connection")
public Connection connection;
+ /**
+ * Represents ObjectStorage internal storage configuration used for internal database setup.
+ */
+ @Getter
+ public static class InternalStorage {
+ /**
+ * Represents all supported providers, which can be used by ObjectStorage internal storage.
+ */
+ @Getter
+ public enum Provider {
+ @JsonProperty("sqlite3")
+ SQLITE3("sqlite3"),
+
+ @JsonProperty("postgres")
+ POSTGRES("postgres");
+
+ private final String value;
+
+ Provider(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+
+ @Valid
+ @NotNull
+ @JsonProperty("provider")
+ public Provider provider;
+
+ @JsonProperty("host")
+ public String host;
+
+ @NotNull
+ @JsonProperty("username")
+ public String username;
+
+ @NotNull
+ @JsonProperty("password")
+ public String password;
+ }
+
+ @Valid
+ @NotNull
+ @JsonProperty("internal-storage")
+ public InternalStorage internalStorage;
+
/**
* Represents ObjectStorage API Server configuration used for temporate storage setup.
*/
diff --git a/api-server/src/main/java/com/objectstorage/entity/common/PropertiesEntity.java b/api-server/src/main/java/com/objectstorage/entity/common/PropertiesEntity.java
index ec3e7a9..cbc7502 100644
--- a/api-server/src/main/java/com/objectstorage/entity/common/PropertiesEntity.java
+++ b/api-server/src/main/java/com/objectstorage/entity/common/PropertiesEntity.java
@@ -20,6 +20,15 @@ public class PropertiesEntity {
@ConfigProperty(name = "quarkus.http.port")
Integer applicationPort;
+ @ConfigProperty(name = "database.name")
+ String databaseName;
+
+ @ConfigProperty(name = "liquibase.sqlite3.config")
+ String liquibaseSqlite3Config;
+
+ @ConfigProperty(name = "liquibase.postgres.config")
+ String liquibasePostgresConfig;
+
@ConfigProperty(name = "content.root.notation")
String contentRootNotation;
diff --git a/api-server/src/main/java/com/objectstorage/exception/ConfigDatabasePropertiesMissingException.java b/api-server/src/main/java/com/objectstorage/exception/ConfigDatabasePropertiesMissingException.java
new file mode 100644
index 0000000..76007d9
--- /dev/null
+++ b/api-server/src/main/java/com/objectstorage/exception/ConfigDatabasePropertiesMissingException.java
@@ -0,0 +1,21 @@
+package com.objectstorage.exception;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Formatter;
+
+/**
+ * Represents exception used when configuration file database properties are missing.
+ */
+public class ConfigDatabasePropertiesMissingException extends IOException {
+ public ConfigDatabasePropertiesMissingException() {
+ this("");
+ }
+
+ public ConfigDatabasePropertiesMissingException(Object... message) {
+ super(
+ new Formatter()
+ .format("Config file database properties are missing: %s", Arrays.stream(message).toArray())
+ .toString());
+ }
+}
diff --git a/api-server/src/main/java/com/objectstorage/repository/ConfigRepository.java b/api-server/src/main/java/com/objectstorage/repository/ConfigRepository.java
deleted file mode 100644
index cce2352..0000000
--- a/api-server/src/main/java/com/objectstorage/repository/ConfigRepository.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.objectstorage.repository;
-
-import com.objectstorage.entity.common.PropertiesEntity;
-import com.objectstorage.entity.repository.ConfigEntity;
-import com.objectstorage.exception.QueryEmptyResultException;
-import com.objectstorage.exception.QueryExecutionFailureException;
-import com.objectstorage.exception.RepositoryOperationFailureException;
-import com.objectstorage.repository.executor.RepositoryExecutor;
-import io.quarkus.runtime.annotations.RegisterForReflection;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents repository implementation to handle config table.
- */
-@ApplicationScoped
-@RegisterForReflection
-public class ConfigRepository {
- @Inject
- PropertiesEntity properties;
-
- @Inject
- RepositoryExecutor repositoryExecutor;
-
- /**
- * Inserts given values into the config table.
- *
- * @param provider given provider.
- * @param secret given secret.
- * @param hash given configuration file hash.
- * @throws RepositoryOperationFailureException if operation execution fails.
- */
- public void insert(Integer provider, Integer secret, String hash) throws RepositoryOperationFailureException {
- try {
- repositoryExecutor.performQuery(
- String.format(
- "INSERT INTO %s (provider, secret, hash) VALUES (%d, %d, '%s')",
- properties.getDatabaseConfigTableName(),
- provider,
- secret,
- hash));
-
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
- }
-
- /**
- * Retrieves all the persisted temporate entities with the given provider and secret.
- *
- * @return retrieved temporate entities.
- * @throws RepositoryOperationFailureException if repository operation fails.
- */
- public List findByProviderAndSecret(Integer provider, Integer secret) throws
- RepositoryOperationFailureException {
- ResultSet resultSet;
-
- try {
- resultSet =
- repositoryExecutor.performQueryWithResult(
- String.format(
- "SELECT t.id, t.hash FROM %s as t WHERE t.provider = %d AND t.secret = %d",
- properties.getDatabaseConfigTableName(),
- provider,
- secret));
-
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
-
- List result = new ArrayList<>();
-
- Integer id;
- String hash;
-
- try {
- while (resultSet.next()) {
- id = resultSet.getInt("id");
- hash = resultSet.getString("hash");
-
- result.add(ConfigEntity.of(id, provider, secret, hash));
- }
- } catch (SQLException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
-
- try {
- resultSet.close();
- } catch (SQLException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
-
- return result;
- }
-
- /**
- * Deletes all entities with the given provider and secret from config table.
- *
- * @param provider given provider.
- * @param secret given secret.
- * @throws RepositoryOperationFailureException if operation execution fails.
- */
- public void deleteByProviderAndSecret(Integer provider, Integer secret) throws RepositoryOperationFailureException {
- try {
- repositoryExecutor.performQuery(
- String.format(
- "DELETE FROM %s as t WHERE t.provider = %d AND t.secret = %d",
- properties.getDatabaseConfigTableName(),
- provider,
- secret));
-
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
- }
-}
diff --git a/api-server/src/main/java/com/objectstorage/repository/ContentRepository.java b/api-server/src/main/java/com/objectstorage/repository/ContentRepository.java
index 32412d8..dab78ca 100644
--- a/api-server/src/main/java/com/objectstorage/repository/ContentRepository.java
+++ b/api-server/src/main/java/com/objectstorage/repository/ContentRepository.java
@@ -49,7 +49,7 @@ public void insert(Integer provider, Integer secret, String root) throws Reposit
try {
repositoryExecutor.performQuery(query);
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
@@ -77,12 +77,29 @@ public ContentEntity findByProviderAndSecret(Integer provider, Integer secret) t
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer id;
- String root;
-
try {
- id = resultSet.getInt("id");
- root = resultSet.getString("root");
+ if (resultSet.next()) {
+ try {
+ Integer id = resultSet.getInt("id");
+ String root = resultSet.getString("root");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return ContentEntity.of(id, provider, secret, root);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -99,7 +116,7 @@ public ContentEntity findByProviderAndSecret(Integer provider, Integer secret) t
throw new RepositoryOperationFailureException(e.getMessage());
}
- return ContentEntity.of(id, provider, secret, root);
+ return null;
}
/**
@@ -167,7 +184,7 @@ public void deleteByProviderAndSecret(Integer provider, Integer secret) throws R
provider,
secret));
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
diff --git a/api-server/src/main/java/com/objectstorage/repository/ProviderRepository.java b/api-server/src/main/java/com/objectstorage/repository/ProviderRepository.java
index 6e8ad7c..566bea4 100644
--- a/api-server/src/main/java/com/objectstorage/repository/ProviderRepository.java
+++ b/api-server/src/main/java/com/objectstorage/repository/ProviderRepository.java
@@ -49,10 +49,28 @@ public ProviderEntity findByName(String name) throws RepositoryOperationFailureE
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer id;
-
try {
- id = resultSet.getInt("id");
+ if (resultSet.next()) {
+ try {
+ Integer id = resultSet.getInt("id");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return ProviderEntity.of(id, name);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -69,7 +87,7 @@ public ProviderEntity findByName(String name) throws RepositoryOperationFailureE
throw new RepositoryOperationFailureException(e.getMessage());
}
- return ProviderEntity.of(id, name);
+ return null;
}
/**
@@ -94,10 +112,28 @@ public ProviderEntity findById(Integer id) throws RepositoryOperationFailureExce
throw new RepositoryOperationFailureException(e.getMessage());
}
- String name;
-
try {
- name = resultSet.getString("name");
+ if (resultSet.next()) {
+ try {
+ String name = resultSet.getString("name");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return ProviderEntity.of(id, name);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -114,6 +150,6 @@ public ProviderEntity findById(Integer id) throws RepositoryOperationFailureExce
throw new RepositoryOperationFailureException(e.getMessage());
}
- return ProviderEntity.of(id, name);
+ return null;
}
}
diff --git a/api-server/src/main/java/com/objectstorage/repository/SecretRepository.java b/api-server/src/main/java/com/objectstorage/repository/SecretRepository.java
index 0e0508b..af076da 100644
--- a/api-server/src/main/java/com/objectstorage/repository/SecretRepository.java
+++ b/api-server/src/main/java/com/objectstorage/repository/SecretRepository.java
@@ -45,7 +45,7 @@ public void insert(Integer session, String credentials) throws RepositoryOperati
try {
repositoryExecutor.performQuery(query);
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
@@ -107,10 +107,28 @@ public SecretEntity findBySessionAndCredentials(Integer session, String credenti
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer id;
-
try {
- id = resultSet.getInt("id");
+ if (resultSet.next()) {
+ try {
+ Integer id = resultSet.getInt("id");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return SecretEntity.of(id, session, credentials);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -127,7 +145,7 @@ public SecretEntity findBySessionAndCredentials(Integer session, String credenti
throw new RepositoryOperationFailureException(e.getMessage());
}
- return SecretEntity.of(id, session, credentials);
+ return null;
}
/**
@@ -150,18 +168,19 @@ public SecretEntity findById(Integer id) throws RepositoryOperationFailureExcept
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer session;
-
try {
- session = resultSet.getInt("session");
- } catch (SQLException e) {
- throw new RepositoryOperationFailureException(e.getMessage());
- }
+ if (resultSet.next()) {
+ Integer session = resultSet.getInt("session");
+ String credentials = resultSet.getString("credentials");
- String credentials;
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
- try {
- credentials = resultSet.getString("credentials");
+ return SecretEntity.of(id, session, credentials);
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -178,7 +197,7 @@ public SecretEntity findById(Integer id) throws RepositoryOperationFailureExcept
throw new RepositoryOperationFailureException(e.getMessage());
}
- return SecretEntity.of(id, session, credentials);
+ return null;
}
/**
@@ -195,7 +214,7 @@ public void deleteById(Integer id) throws RepositoryOperationFailureException {
properties.getDatabaseSecretTableName(),
id));
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
diff --git a/api-server/src/main/java/com/objectstorage/repository/TemporateRepository.java b/api-server/src/main/java/com/objectstorage/repository/TemporateRepository.java
index 5069417..1a7a5a3 100644
--- a/api-server/src/main/java/com/objectstorage/repository/TemporateRepository.java
+++ b/api-server/src/main/java/com/objectstorage/repository/TemporateRepository.java
@@ -7,6 +7,7 @@
import com.objectstorage.exception.QueryExecutionFailureException;
import com.objectstorage.exception.RepositoryOperationFailureException;
import com.objectstorage.repository.executor.RepositoryExecutor;
+import com.objectstorage.service.config.ConfigService;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -27,6 +28,9 @@ public class TemporateRepository {
@Inject
PropertiesEntity properties;
+ @Inject
+ ConfigService configService;
+
@Inject
RepositoryExecutor repositoryExecutor;
@@ -54,7 +58,7 @@ public void insert(Integer provider, Integer secret, String location, String has
try {
repositoryExecutor.performQuery(query);
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
@@ -71,16 +75,45 @@ public Integer count() throws RepositoryOperationFailureException {
try {
resultSet =
repositoryExecutor.performQueryWithResult(
- String.format("SELECT COUNT(1) as 'result' FROM %s", properties.getDatabaseTemporateTableName()));
+ String.format("SELECT COUNT(1) as result FROM %s", properties.getDatabaseTemporateTableName()));
} catch (QueryExecutionFailureException | QueryEmptyResultException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer count;
+ Integer count = 0;
try {
- count = resultSet.getInt("result");
+ if (resultSet.next()) {
+ switch (configService.getConfig().getInternalStorage().getProvider()) {
+ case SQLITE3 -> {
+ try {
+ count = resultSet.getInt("result");
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
+ case POSTGRES -> {
+ try {
+ count = (int) resultSet.getLong("result");
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -120,20 +153,33 @@ public TemporateEntity findEarliest() throws RepositoryOperationFailureException
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer id;
- Integer provider;
- Integer secret;
- String location;
- String hash;
- Long createdAt;
-
try {
- id = resultSet.getInt("id");
- provider = resultSet.getInt("provider");
- secret = resultSet.getInt("secret");
- location = resultSet.getString("location");
- hash = resultSet.getString("hash");
- createdAt = resultSet.getLong("created_at");
+ if (resultSet.next()) {
+ try {
+ Integer id = resultSet.getInt("id");
+ Integer provider = resultSet.getInt("provider");
+ Integer secret = resultSet.getInt("secret");
+ String location = resultSet.getString("location");
+ String hash = resultSet.getString("hash");
+ Long createdAt = resultSet.getLong("created_at");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return TemporateEntity.of(id, provider, secret, location, hash, createdAt);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -150,7 +196,7 @@ public TemporateEntity findEarliest() throws RepositoryOperationFailureException
throw new RepositoryOperationFailureException(e.getMessage());
}
- return TemporateEntity.of(id, provider, secret, location, hash, createdAt);
+ return null;
}
/**
@@ -240,15 +286,30 @@ public TemporateEntity findEarliestByLocationProviderAndSecret(
throw new RepositoryOperationFailureException(e.getMessage());
}
- Integer id;
- String hash;
- Long createdAt;
-
try {
- id = resultSet.getInt("id");
- hash = resultSet.getString("hash");
- createdAt = resultSet.getLong("created_at");
-
+ if (resultSet.next()) {
+ try {
+ Integer id = resultSet.getInt("id");
+ String hash = resultSet.getString("hash");
+ Long createdAt = resultSet.getLong("created_at");
+
+ try {
+ resultSet.close();
+ } catch (SQLException e) {
+ throw new RepositoryOperationFailureException(e.getMessage());
+ }
+
+ return TemporateEntity.of(id, provider, secret, location, hash, createdAt);
+ } catch (SQLException e1) {
+ try {
+ resultSet.close();
+ } catch (SQLException e2) {
+ throw new RepositoryOperationFailureException(e2.getMessage());
+ }
+
+ throw new RepositoryOperationFailureException(e1.getMessage());
+ }
+ }
} catch (SQLException e1) {
try {
resultSet.close();
@@ -265,7 +326,7 @@ public TemporateEntity findEarliestByLocationProviderAndSecret(
throw new RepositoryOperationFailureException(e.getMessage());
}
- return TemporateEntity.of(id, provider, secret, location, hash, createdAt);
+ return null;
}
/**
@@ -347,7 +408,7 @@ public void deleteByLocationProviderAndSecret(String location, Integer provider,
provider,
secret));
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
@@ -366,7 +427,7 @@ public void deleteByHash(String hash) throws RepositoryOperationFailureException
properties.getDatabaseTemporateTableName(),
hash));
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
@@ -387,7 +448,7 @@ public void deleteByProviderAndSecret(Integer provider, Integer secret) throws R
provider,
secret));
- } catch (QueryExecutionFailureException | QueryEmptyResultException e) {
+ } catch (QueryExecutionFailureException e) {
throw new RepositoryOperationFailureException(e.getMessage());
}
}
diff --git a/api-server/src/main/java/com/objectstorage/repository/executor/RepositoryExecutor.java b/api-server/src/main/java/com/objectstorage/repository/executor/RepositoryExecutor.java
index 18ab7ee..7848137 100644
--- a/api-server/src/main/java/com/objectstorage/repository/executor/RepositoryExecutor.java
+++ b/api-server/src/main/java/com/objectstorage/repository/executor/RepositoryExecutor.java
@@ -1,8 +1,10 @@
package com.objectstorage.repository.executor;
+import com.objectstorage.entity.common.ConfigEntity;
import com.objectstorage.entity.common.PropertiesEntity;
import com.objectstorage.exception.*;
import com.objectstorage.repository.common.RepositoryConfigurationHelper;
+import com.objectstorage.service.config.ConfigService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
@@ -29,16 +31,16 @@ public class RepositoryExecutor {
PropertiesEntity properties;
@Inject
- DataSource dataSource;
+ ConfigService configService;
@Inject
- RepositoryConfigurationHelper repositoryConfigurationHelper;
+ DataSource dataSource;
private Connection connection;
- private final List statements = new ArrayList<>();
+ private final static List statements = new ArrayList<>();
- private final ScheduledExecutorService scheduledExecutorService =
+ private final static ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();
/**
@@ -56,36 +58,48 @@ private void configure() throws QueryExecutionFailureException {
}
/**
- * Performs given SQL query without result.
+ * Performs given SQL query via given connection without result.
*
+ * @param connection given SQL connection.
* @param query given SQL query to be executed.
+ * @param databaseStatementCloseDelay given database statement close delay.
* @throws QueryExecutionFailureException if query execution is interrupted by failure.
- * @throws QueryEmptyResultException if result is empty.
*/
- public void performQuery(String query) throws QueryExecutionFailureException, QueryEmptyResultException {
+ public static void performQuery(Connection connection, String query, Integer databaseStatementCloseDelay)
+ throws QueryExecutionFailureException {
Statement statement;
try {
- statement = this.connection.createStatement();
+ statement = connection.createStatement();
} catch (SQLException e) {
throw new QueryExecutionFailureException(e.getMessage());
}
+ statements.add(statement);
+
try {
statement.executeUpdate(query);
} catch (SQLException e) {
throw new QueryExecutionFailureException(e.getMessage());
}
- statements.add(statement);
-
scheduledExecutorService.schedule(() -> {
try {
statement.close();
} catch (SQLException e) {
logger.fatal(new QueryExecutionFailureException(e.getMessage()).getMessage());
}
- }, properties.getDatabaseStatementCloseDelay(), TimeUnit.MILLISECONDS);
+ }, databaseStatementCloseDelay, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Performs given SQL query without result.
+ *
+ * @param query given SQL query to be executed.
+ * @throws QueryExecutionFailureException if query execution is interrupted by failure.
+ */
+ public void performQuery(String query) throws QueryExecutionFailureException {
+ performQuery(this.connection, query, properties.getDatabaseStatementCloseDelay());
}
/**
@@ -105,6 +119,8 @@ public ResultSet performQueryWithResult(String query) throws QueryExecutionFailu
throw new QueryExecutionFailureException(e.getMessage());
}
+ statements.add(statement);
+
ResultSet resultSet;
try {
@@ -113,8 +129,6 @@ public ResultSet performQueryWithResult(String query) throws QueryExecutionFailu
throw new QueryExecutionFailureException(e.getMessage());
}
- statements.add(statement);
-
scheduledExecutorService.schedule(() -> {
try {
statement.close();
diff --git a/api-server/src/main/java/com/objectstorage/service/integration/backup/BackupService.java b/api-server/src/main/java/com/objectstorage/service/integration/backup/BackupService.java
index 453fc2a..1eb6e40 100644
--- a/api-server/src/main/java/com/objectstorage/service/integration/backup/BackupService.java
+++ b/api-server/src/main/java/com/objectstorage/service/integration/backup/BackupService.java
@@ -85,8 +85,6 @@ public void process() throws BackupPeriodRetrievalFailureException {
} catch (ContentApplicationRetrievalFailureException e) {
StateService.getBackupProcessorGuard().unlock();
- logger.error(e.getMessage());
-
return;
}
diff --git a/api-server/src/main/java/com/objectstorage/service/integration/diagnostics/DiagnosticsConfigService.java b/api-server/src/main/java/com/objectstorage/service/integration/diagnostics/DiagnosticsConfigService.java
index 662f996..61c5f33 100644
--- a/api-server/src/main/java/com/objectstorage/service/integration/diagnostics/DiagnosticsConfigService.java
+++ b/api-server/src/main/java/com/objectstorage/service/integration/diagnostics/DiagnosticsConfigService.java
@@ -159,22 +159,12 @@ private void process() throws
configService.getConfig().getDiagnostics().getNodeExporter().getPort(),
properties.getDiagnosticsCommonDockerNetworkName());
- CommandExecutorOutputDto nodeExporterDeployCommandOutput;
-
try {
- nodeExporterDeployCommandOutput =
- commandExecutorService.executeCommand(nodeExporterDeployCommandService);
+ commandExecutorService.executeCommand(nodeExporterDeployCommandService);
} catch (CommandExecutorException e) {
throw new NodeExporterDeploymentFailureException(e.getMessage());
}
- String nodeExporterDeployCommandErrorOutput = nodeExporterDeployCommandOutput.getErrorOutput();
-
- if (Objects.nonNull(nodeExporterDeployCommandErrorOutput) &&
- !nodeExporterDeployCommandErrorOutput.isEmpty()) {
- throw new NodeExporterDeploymentFailureException(nodeExporterDeployCommandErrorOutput);
- }
-
PrometheusDeployCommandService prometheusDeployCommandService =
new PrometheusDeployCommandService(
properties.getDiagnosticsPrometheusDockerName(),
@@ -184,22 +174,12 @@ private void process() throws
properties.getDiagnosticsPrometheusConfigLocation(),
properties.getDiagnosticsPrometheusInternalLocation());
- CommandExecutorOutputDto prometheusDeployCommandOutput;
-
try {
- prometheusDeployCommandOutput =
- commandExecutorService.executeCommand(prometheusDeployCommandService);
+ commandExecutorService.executeCommand(prometheusDeployCommandService);
} catch (CommandExecutorException e) {
throw new PrometheusDeploymentFailureException(e.getMessage());
}
- String prometheusDeployCommandErrorOutput = prometheusDeployCommandOutput.getErrorOutput();
-
- if (Objects.nonNull(prometheusDeployCommandErrorOutput) &&
- !prometheusDeployCommandErrorOutput.isEmpty()) {
- throw new PrometheusDeploymentFailureException(prometheusDeployCommandErrorOutput);
- }
-
GrafanaDeployCommandService grafanaDeployCommandService =
new GrafanaDeployCommandService(
properties.getDiagnosticsGrafanaDockerName(),
@@ -209,21 +189,11 @@ private void process() throws
properties.getDiagnosticsGrafanaConfigLocation(),
properties.getDiagnosticsGrafanaInternalLocation());
- CommandExecutorOutputDto grafanaDeployCommandOutput;
-
try {
- grafanaDeployCommandOutput =
- commandExecutorService.executeCommand(grafanaDeployCommandService);
+ commandExecutorService.executeCommand(grafanaDeployCommandService);
} catch (CommandExecutorException e) {
throw new GrafanaDeploymentFailureException(e.getMessage());
}
-
- String grafanaDeployCommandErrorOutput = grafanaDeployCommandOutput.getErrorOutput();
-
- if (Objects.nonNull(grafanaDeployCommandErrorOutput) &&
- !grafanaDeployCommandErrorOutput.isEmpty()) {
- throw new GrafanaDeploymentFailureException(grafanaDeployCommandErrorOutput);
- }
}
}
diff --git a/api-server/src/main/java/com/objectstorage/service/integration/properties/database/DatabasePropertiesConfigService.java b/api-server/src/main/java/com/objectstorage/service/integration/properties/database/DatabasePropertiesConfigService.java
new file mode 100644
index 0000000..2154704
--- /dev/null
+++ b/api-server/src/main/java/com/objectstorage/service/integration/properties/database/DatabasePropertiesConfigService.java
@@ -0,0 +1,130 @@
+package com.objectstorage.service.integration.properties.database;
+
+import com.objectstorage.exception.ConfigDatabasePropertiesMissingException;
+import com.objectstorage.exception.QueryExecutionFailureException;
+import com.objectstorage.repository.executor.RepositoryExecutor;
+import com.objectstorage.service.config.common.ConfigConfigurationHelper;
+import io.quarkus.runtime.annotations.StaticInitSafe;
+import io.smallrye.config.ConfigSourceContext;
+import io.smallrye.config.ConfigSourceFactory;
+import io.smallrye.config.ConfigValue;
+import io.smallrye.config.PropertiesConfigSource;
+import lombok.SneakyThrows;
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import com.objectstorage.entity.common.ConfigEntity;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.OptionalInt;
+import java.util.Properties;
+
+/**
+ * Service used to perform security properties configuration operations.
+ */
+@StaticInitSafe
+public class DatabasePropertiesConfigService implements ConfigSourceFactory {
+ @Override
+ @SneakyThrows
+ public Iterable getConfigSources(final ConfigSourceContext context) {
+ final ConfigValue configLocation = context.getValue("config.location");
+
+ if (Objects.isNull(configLocation) || Objects.isNull(configLocation.getValue())) {
+ return Collections.emptyList();
+ }
+
+ final ConfigValue databaseName = context.getValue("database.name");
+ if (Objects.isNull(databaseName) || Objects.isNull(databaseName.getValue())) {
+ return Collections.emptyList();
+ }
+
+ final ConfigValue liquibaseSqlite3Config = context.getValue("liquibase.sqlite3.config");
+ if (Objects.isNull(liquibaseSqlite3Config) || Objects.isNull(liquibaseSqlite3Config.getValue())) {
+ return Collections.emptyList();
+ }
+
+ final ConfigValue liquibasePostgresConfig = context.getValue("liquibase.postgres.config");
+ if (Objects.isNull(liquibasePostgresConfig) || Objects.isNull(liquibasePostgresConfig.getValue())) {
+ return Collections.emptyList();
+ }
+
+ final ConfigValue databaseStatementCloseDelay = context.getValue("database.statement.close-delay");
+ if (Objects.isNull(databaseStatementCloseDelay) || Objects.isNull(databaseStatementCloseDelay.getValue())) {
+ return Collections.emptyList();
+ }
+
+ Properties properties = new Properties();
+
+ ConfigEntity config = ConfigConfigurationHelper.readConfig(configLocation.getValue(), false);
+ if (Objects.isNull(config)) {
+ return Collections.emptyList();
+ }
+
+ if (Objects.isNull(config.getInternalStorage()) ||
+ Objects.isNull(config.getInternalStorage().getProvider()) ||
+ Objects.isNull(config.getInternalStorage().getUsername()) ||
+ Objects.isNull(config.getInternalStorage().getPassword())) {
+ throw new ConfigDatabasePropertiesMissingException();
+ }
+
+ if (config.getInternalStorage().getProvider() == ConfigEntity.InternalStorage.Provider.POSTGRES &&
+ Objects.isNull(config.getInternalStorage().getHost())) {
+ throw new ConfigDatabasePropertiesMissingException();
+ }
+
+ switch (config.getInternalStorage().getProvider()) {
+ case SQLITE3 -> {
+ properties.put("quarkus.liquibase.change-log", liquibaseSqlite3Config.getValue());
+
+ properties.put("quarkus.datasource.jdbc.driver", "org.sqlite.JDBC");
+ properties.put("quarkus.datasource.db-kind", "other");
+ properties.put(
+ "quarkus.datasource.jdbc.url",
+ String.format(
+ "jdbc:sqlite:%s/.%s/internal/database/data.db",
+ System.getProperty("user.home"),
+ databaseName.getValue()));
+ }
+ case POSTGRES -> {
+ properties.put("quarkus.liquibase.change-log", liquibasePostgresConfig.getValue());
+
+ properties.put("quarkus.datasource.db-kind", "postgresql");
+ properties.put(
+ "quarkus.datasource.jdbc.url",
+ String.format("jdbc:postgresql://%s/%s",
+ config.getInternalStorage().getHost(),
+ databaseName.getValue()));
+
+ Connection connection = DriverManager.getConnection(
+ String.format("jdbc:postgresql://%s/postgres", config.getInternalStorage().getHost()),
+ config.getInternalStorage().getUsername(),
+ config.getInternalStorage().getPassword());
+
+ try {
+ RepositoryExecutor.performQuery(
+ connection,
+ String.format("CREATE DATABASE %s", databaseName.getValue()),
+ Integer.valueOf(databaseStatementCloseDelay.getValue()));
+ } catch (QueryExecutionFailureException ignore) {
+ }
+
+ connection.close();
+ }
+ }
+
+ properties.put("quarkus.datasource.username", config.getInternalStorage().getUsername());
+ properties.put("quarkus.datasource.password", config.getInternalStorage().getPassword());
+
+ return Collections.singletonList(
+ new PropertiesConfigSource(
+ properties,
+ com.objectstorage.service.integration.properties.security.SecurityPropertiesConfigService.class.getSimpleName(),
+ 290));
+ }
+
+ @Override
+ public OptionalInt getPriority() {
+ return OptionalInt.of(290);
+ }
+}
\ No newline at end of file
diff --git a/api-server/src/main/java/com/objectstorage/service/integration/properties/security/SecurityPropertiesConfigService.java b/api-server/src/main/java/com/objectstorage/service/integration/properties/security/SecurityPropertiesConfigService.java
index ad66940..b7084c2 100644
--- a/api-server/src/main/java/com/objectstorage/service/integration/properties/security/SecurityPropertiesConfigService.java
+++ b/api-server/src/main/java/com/objectstorage/service/integration/properties/security/SecurityPropertiesConfigService.java
@@ -24,14 +24,14 @@ public class SecurityPropertiesConfigService implements ConfigSourceFactory {
@Override
@SneakyThrows
public Iterable getConfigSources(final ConfigSourceContext context) {
- final ConfigValue value = context.getValue("config.location");
- if (value == null || value.getValue() == null) {
+ final ConfigValue configLocation = context.getValue("config.location");
+ if (Objects.isNull(configLocation) || Objects.isNull(configLocation.getValue())) {
return Collections.emptyList();
}
Properties properties = new Properties();
- ConfigEntity config = ConfigConfigurationHelper.readConfig(value.getValue(), false);
+ ConfigEntity config = ConfigConfigurationHelper.readConfig(configLocation.getValue(), false);
if (Objects.isNull(config)) {
return Collections.emptyList();
}
diff --git a/api-server/src/main/java/com/objectstorage/service/state/watcher/WatcherService.java b/api-server/src/main/java/com/objectstorage/service/state/watcher/WatcherService.java
index bf3a21e..0d215c3 100644
--- a/api-server/src/main/java/com/objectstorage/service/state/watcher/WatcherService.java
+++ b/api-server/src/main/java/com/objectstorage/service/state/watcher/WatcherService.java
@@ -1,7 +1,5 @@
package com.objectstorage.service.state.watcher;
-import org.apache.commons.io.FileUtils;
-
/**
* Service used to track state metrics for the current session in the application.
*/
@@ -39,7 +37,10 @@ public void increaseUploadedFilesSize(Integer value) {
*/
public Double getAverageFileSize() {
if (filesUploadCounter > 0) {
- return Double.valueOf(uploadedFilesSize) / Double.valueOf(filesUploadCounter) / 1024 / 1024;
+ return (Double.valueOf(uploadedFilesSize) /
+ Double.valueOf(filesUploadCounter)) /
+ (double) 1024 /
+ (double) 1024;
}
return (double) 0;
diff --git a/api-server/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory b/api-server/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
index 282b490..f12dc8a 100644
--- a/api-server/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
+++ b/api-server/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceFactory
@@ -1 +1,2 @@
-com.objectstorage.service.integration.properties.security.SecurityPropertiesConfigService
\ No newline at end of file
+com.objectstorage.service.integration.properties.database.DatabasePropertiesConfigService
+com.objectstorage.service.integration.properties.security.SecurityPropertiesConfigService
diff --git a/api-server/src/main/resources/application.properties b/api-server/src/main/resources/application.properties
index d9213f4..0eb7d0c 100644
--- a/api-server/src/main/resources/application.properties
+++ b/api-server/src/main/resources/application.properties
@@ -6,18 +6,15 @@ quarkus.swagger-ui.always-include=true
quarkus.native.builder-image=graalvm
quarkus.banner.path=banner.txt
+# Describes database Quarkus configuration.
+database.name=objectstorage
+
# Describes security Quarkus configuration.
quarkus.rest-client.alpn=false
-# Describes database Quarkus configuration.
-quarkus.datasource.jdbc.driver=org.sqlite.JDBC
-quarkus.datasource.db-kind=other
-quarkus.datasource.jdbc.url=jdbc:sqlite:${user.home}/.objectstorage/internal/database/data.db
-quarkus.datasource.username=objectstorage_user
-quarkus.datasource.password=objectstorage_password
-
# Describes LiquiBase Quarkus configuration.
-quarkus.liquibase.change-log=liquibase/config.yaml
+liquibase.sqlite3.config=liquibase/sqlite3/config.yaml
+liquibase.postgres.config=liquibase/postgres/config.yaml
quarkus.liquibase.migrate-at-start=true
# Describes internal healthcheck client configuration.
diff --git a/api-server/src/main/resources/liquibase/postgres/config.yaml b/api-server/src/main/resources/liquibase/postgres/config.yaml
new file mode 100644
index 0000000..affff68
--- /dev/null
+++ b/api-server/src/main/resources/liquibase/postgres/config.yaml
@@ -0,0 +1,134 @@
+databaseChangeLog:
+ - changeSet:
+ id: 1
+ author: YarikRevich
+ changes:
+ - createTable:
+ tableName: secret
+ columns:
+ - column:
+ name: id
+ type: INT
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: session
+ type: INT
+ constraints:
+ nullable: false
+ - column:
+ name: credentials
+ type: TEXT
+ constraints:
+ nullable: true
+ - createTable:
+ tableName: provider
+ columns:
+ - column:
+ name: id
+ type: INT
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: name
+ type: TEXT
+ constraints:
+ unique: true
+ nullable: false
+ - createTable:
+ tableName: content
+ columns:
+ - column:
+ name: id
+ type: INT
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: provider
+ type: INT
+ constraints:
+ foreignKeyName: provider_fk
+ references: provider(id)
+ nullable: false
+ deleteCascade: true
+ - column:
+ name: secret
+ type: INT
+ constraints:
+ foreignKeyName: secret_fk
+ references: secret(id)
+ nullable: false
+ unique: true
+ deleteCascade: true
+ - column:
+ name: root
+ type: TEXT
+ constraints:
+ nullable: false
+ - createTable:
+ tableName: temporate
+ columns:
+ - column:
+ name: id
+ type: INT
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: provider
+ type: INT
+ constraints:
+ foreignKeyName: provider_fk
+ references: provider(id)
+ nullable: false
+ deleteCascade: true
+ - column:
+ name: secret
+ type: INT
+ constraints:
+ foreignKeyName: secret_fk
+ references: secret(id)
+ nullable: false
+ deleteCascade: true
+ - column:
+ name: location
+ type: TEXT
+ constraints:
+ nullable: false
+ - column:
+ name: hash
+ type: TEXT
+ constraints:
+ nullable: false
+ unique: true
+ - column:
+ name: created_at
+ type: BIGINT
+ constraints:
+ nullable: false
+ - createIndex:
+ columns:
+ - column:
+ name: name
+ indexName: idx_provider_name
+ tableName: provider
+ - loadData:
+ tableName: provider
+ usePreparedStatements: false
+ separator: ;
+ relativeToChangelogFile: true
+ file: data/data.csv
+ encoding: UTF-8
+ quotchar: ''''
+ columns:
+ - column:
+ header: Name
+ name: name
+ type: STRING
\ No newline at end of file
diff --git a/api-server/src/main/resources/liquibase/data/data.csv b/api-server/src/main/resources/liquibase/postgres/data/data.csv
similarity index 100%
rename from api-server/src/main/resources/liquibase/data/data.csv
rename to api-server/src/main/resources/liquibase/postgres/data/data.csv
diff --git a/api-server/src/main/resources/liquibase/config.yaml b/api-server/src/main/resources/liquibase/sqlite3/config.yaml
similarity index 80%
rename from api-server/src/main/resources/liquibase/config.yaml
rename to api-server/src/main/resources/liquibase/sqlite3/config.yaml
index 75f23cd..2443eb8 100644
--- a/api-server/src/main/resources/liquibase/config.yaml
+++ b/api-server/src/main/resources/liquibase/sqlite3/config.yaml
@@ -3,37 +3,6 @@ databaseChangeLog:
id: 1
author: YarikRevich
changes:
- - createTable:
- tableName: config
- columns:
- - column:
- name: id
- type: INT
- autoIncrement: true
- constraints:
- primaryKey: true
- nullable: false
- - column:
- name: provider
- type: INT
- constraints:
- foreignKeyName: provider_fk
- references: provider(id)
- nullable: false
- deleteCascade: true
- - column:
- name: secret
- type: INT
- constraints:
- foreignKeyName: secret_fk
- references: secret(id)
- nullable: false
- deleteCascade: true
- - column:
- name: hash
- type: VARCHAR
- constraints:
- nullable: false
- createTable:
tableName: secret
columns:
diff --git a/api-server/src/main/resources/liquibase/sqlite3/data/data.csv b/api-server/src/main/resources/liquibase/sqlite3/data/data.csv
new file mode 100644
index 0000000..49f7e28
--- /dev/null
+++ b/api-server/src/main/resources/liquibase/sqlite3/data/data.csv
@@ -0,0 +1,3 @@
+Name
+s3
+gcs
\ No newline at end of file
diff --git a/config/grafana/dashboards/diagnostics.tmpl b/config/grafana/dashboards/diagnostics.tmpl
index a9e7da5..c7ebb33 100644
--- a/config/grafana/dashboards/diagnostics.tmpl
+++ b/config/grafana/dashboards/diagnostics.tmpl
@@ -18,8 +18,8 @@
"description": "ObjectStorage API Server: ${(info.version)}",
"editable": true,
"fiscalYearStartMonth": 0,
- "gnetId": 179,
"graphTooltip": 1,
+ "id": 1,
"links": [],
"panels": [
{
@@ -52,6 +52,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
+ "barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -115,6 +116,7 @@
"sort": "none"
}
},
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -153,6 +155,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
+ "barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -216,6 +219,7 @@
"sort": "none"
}
},
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -254,6 +258,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
+ "barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -317,6 +322,7 @@
"sort": "none"
}
},
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -355,6 +361,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
+ "barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -419,6 +426,7 @@
"sort": "none"
}
},
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -457,6 +465,7 @@
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
+ "barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
@@ -497,7 +506,7 @@
}
]
},
- "unit": "decmbytes"
+ "unit": "none"
},
"overrides": []
},
@@ -521,6 +530,7 @@
"sort": "none"
}
},
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -578,6 +588,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
+ "percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -589,7 +600,7 @@
"textMode": "auto",
"wideLayout": true
},
- "pluginVersion": "10.4.2",
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -649,6 +660,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
+ "percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -660,7 +672,7 @@
"textMode": "auto",
"wideLayout": true
},
- "pluginVersion": "10.4.2",
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -747,6 +759,7 @@
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
+ "percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
@@ -758,7 +771,7 @@
"textMode": "auto",
"wideLayout": true
},
- "pluginVersion": "10.4.2",
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -842,7 +855,7 @@
"showThresholdMarkers": true,
"sizing": "auto"
},
- "pluginVersion": "10.4.2",
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -928,7 +941,7 @@
"showThresholdMarkers": true,
"sizing": "auto"
},
- "pluginVersion": "10.4.2",
+ "pluginVersion": "11.3.1",
"targets": [
{
"datasource": {
@@ -947,8 +960,9 @@
"type": "gauge"
}
],
- "refresh": "",
- "schemaVersion": 39,
+ "preload": false,
+ "refresh": "auto",
+ "schemaVersion": 40,
"tags": [
"docker",
"prometheus, ",
@@ -962,11 +976,9 @@
"auto_count": 30,
"auto_min": "10s",
"current": {
- "selected": false,
"text": "1h",
"value": "1h"
},
- "hide": 0,
"label": "interval",
"name": "interval",
"options": [
@@ -1022,27 +1034,19 @@
}
],
"query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
- "queryValue": "",
"refresh": 2,
- "skipUrlSync": false,
"type": "interval"
},
{
"current": {
- "selected": true,
- "text": [
- "All"
- ],
- "value": [
- "$__all"
- ]
+ "text": "All",
+ "value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "P21B111CBFE6E8FCA"
},
"definition": "label_values(node_exporter_build_info{name=~'$name'},instance)",
- "hide": 0,
"includeAll": true,
"label": "IP",
"multi": true,
@@ -1051,29 +1055,19 @@
"query": "label_values(node_exporter_build_info{name=~'$name'},instance)",
"refresh": 2,
"regex": "",
- "skipUrlSync": false,
"sort": 1,
- "tagValuesQuery": "",
- "tagsQuery": "",
- "type": "query",
- "useTags": false
+ "type": "query"
},
{
"current": {
- "selected": true,
- "text": [
- "All"
- ],
- "value": [
- "$__all"
- ]
+ "text": "All",
+ "value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "P21B111CBFE6E8FCA"
},
"definition": "label_values(node_exporter_build_info,env)",
- "hide": 0,
"includeAll": true,
"label": "Env",
"multi": true,
@@ -1082,29 +1076,18 @@
"query": "label_values(node_exporter_build_info,env)",
"refresh": 2,
"regex": "",
- "skipUrlSync": false,
- "sort": 0,
- "tagValuesQuery": "",
- "tagsQuery": "",
- "type": "query",
- "useTags": false
+ "type": "query"
},
{
"current": {
- "selected": true,
- "text": [
- "All"
- ],
- "value": [
- "$__all"
- ]
+ "text": "All",
+ "value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "P21B111CBFE6E8FCA"
},
"definition": "label_values(node_exporter_build_info{env=~'$env'},name)",
- "hide": 0,
"includeAll": true,
"label": "CPU Name",
"multi": true,
@@ -1113,47 +1096,18 @@
"query": "label_values(node_exporter_build_info{env=~'$env'},name)",
"refresh": 2,
"regex": "",
- "skipUrlSync": false,
- "sort": 0,
- "tagValuesQuery": "",
- "tagsQuery": "",
- "type": "query",
- "useTags": false
+ "type": "query"
}
]
},
"time": {
- "from": "2024-11-29T14:23:10.911Z",
- "to": "2024-11-29T14:25:03.115Z"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
+ "from": "now-5m",
+ "to": "now"
},
+ "timepicker": {},
"timezone": "browser",
"title": "ObjectStorage Diagnostics",
"uid": "64nrElFmk",
- "version": 10,
+ "version": 2,
"weekStart": ""
}
\ No newline at end of file
diff --git a/docs/internal-database-design.md b/docs/internal-database-design.md
index 09c31c2..03568f5 100644
--- a/docs/internal-database-design.md
+++ b/docs/internal-database-design.md
@@ -7,14 +7,6 @@ Internal database design of "ObjectStorage"
end title
-entity "config" {
- *id : number <>
- *provider : number <> # provider(id)
- *secret: number <> # secret(id)
- --
- hash : varchar
-}
-
entity "secret" {
*id : number <>
--
@@ -44,9 +36,8 @@ entity "temporate" {
hash : varchar
}
-config ||...|| secret #magenta : attached to
content ||...|| secret #magenta : attached to
content }|...|| provider #magenta : configures
-temporate ||...|| secret #magenta : created with
-temporate }|...|| provider #magenta : created with
+temporate }|...|| secret #magenta : created with
+temporate }|...|| provider #magenta : created for
```
\ No newline at end of file
diff --git a/docs/internal-database-design.png b/docs/internal-database-design.png
index 79e56b19230479f9e12931aab67f9d07e0b47f71..c733a54e9007e1a6ab81501be449e3b8f24d75d6 100644
GIT binary patch
literal 32543
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
literal 37487
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 |