Compare commits

...

9 Commits

Author SHA1 Message Date
Rostislav Dugin
6eb53bb07b Merge pull request #341 from databasus/develop
Develop
2026-02-06 00:25:30 +03:00
Rostislav Dugin
6ac04270b9 FEATURE (healthcheck): Add checking whether backup nodes available for primary node 2026-02-06 00:24:34 +03:00
Rostislav Dugin
b0510d7c21 FIX (logging): Add login to VictoriaLogs logger 2026-02-06 00:18:09 +03:00
Rostislav Dugin
dc5f271882 Merge pull request #339 from databasus/develop
FIX (storages): Do not remove system storage on any workspace deletion
2026-02-05 01:32:46 +03:00
Rostislav Dugin
8f718771c9 FIX (storages): Do not remove system storage on any workspace deletion 2026-02-05 01:32:21 +03:00
Rostislav Dugin
d8eea05dca Merge pull request #332 from databasus/develop
FIX (script): Fix script creation in playground head x2
2026-02-02 20:46:35 +03:00
Rostislav Dugin
b2a94274d7 FIX (script): Fix script creation in playground head x2 2026-02-02 20:44:52 +03:00
Rostislav Dugin
77c2712ebb Merge pull request #331 from databasus/develop
FIX (script): Fix script creation in playground head
2026-02-02 19:47:44 +03:00
Rostislav Dugin
a9dc29f82c FIX (script): Fix script creation in playground head 2026-02-02 19:47:15 +03:00
7 changed files with 196 additions and 22 deletions

View File

@@ -272,10 +272,13 @@ window.__RUNTIME_CONFIG__ = {
};
JSEOF
# Inject analytics script if provided
# Inject analytics script if provided (only if not already injected)
if [ -n "\${ANALYTICS_SCRIPT:-}" ]; then
echo "Injecting analytics script..."
sed -i "s#</head># \${ANALYTICS_SCRIPT}\n </head>#" /app/ui/build/index.html
if ! grep -q "rybbit.databasus.com" /app/ui/build/index.html 2>/dev/null; then
echo "Injecting analytics script..."
sed -i "s#</head># \${ANALYTICS_SCRIPT}\\
</head>#" /app/ui/build/index.html
fi
fi
# Ensure proper ownership of data directory

View File

@@ -103,6 +103,16 @@ func (s *BackupsScheduler) IsSchedulerRunning() bool {
return s.lastBackupTime.After(time.Now().UTC().Add(-schedulerHealthcheckThreshold))
}
func (s *BackupsScheduler) IsBackupNodesAvailable() bool {
nodes, err := s.backupNodesRegistry.GetAvailableNodes()
if err != nil {
s.logger.Error("Failed to get available nodes for health check", "error", err)
return false
}
return len(nodes) > 0
}
func (s *BackupsScheduler) StartBackup(databaseID uuid.UUID, isCallNotifier bool) {
backupConfig, err := s.backupConfigService.GetBackupConfigByDbId(databaseID)
if err != nil {

View File

@@ -21,6 +21,7 @@ import (
users_services "databasus-backend/internal/features/users/services"
users_testing "databasus-backend/internal/features/users/testing"
workspaces_controllers "databasus-backend/internal/features/workspaces/controllers"
workspaces_repositories "databasus-backend/internal/features/workspaces/repositories"
workspaces_testing "databasus-backend/internal/features/workspaces/testing"
"databasus-backend/internal/util/encryption"
test_utils "databasus-backend/internal/util/testing"
@@ -1969,6 +1970,143 @@ func Test_TransferSystemStorage_TransferBlocked(t *testing.T) {
workspaces_testing.RemoveTestWorkspace(workspaceB, router)
}
func Test_DeleteWorkspace_SystemStoragesFromAnotherWorkspaceNotRemovedAndWorkspaceDeletedSuccessfully(
t *testing.T,
) {
router := createRouter()
GetStorageService().SetStorageDatabaseCounter(&mockStorageDatabaseCounter{})
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
workspaceA := workspaces_testing.CreateTestWorkspace("Workspace A", admin, router)
workspaceD := workspaces_testing.CreateTestWorkspace("Workspace D", admin, router)
// Create a system storage in workspace A
systemStorage := &Storage{
WorkspaceID: workspaceA.ID,
Type: StorageTypeLocal,
Name: "Test System Storage " + uuid.New().String(),
IsSystem: true,
LocalStorage: &local_storage.LocalStorage{},
}
var savedSystemStorage Storage
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/storages",
"Bearer "+admin.Token,
*systemStorage,
http.StatusOK,
&savedSystemStorage,
)
assert.True(t, savedSystemStorage.IsSystem)
assert.Equal(t, workspaceA.ID, savedSystemStorage.WorkspaceID)
// Create a regular storage in workspace D
regularStorage := createNewStorage(workspaceD.ID)
var savedRegularStorage Storage
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/storages",
"Bearer "+admin.Token,
*regularStorage,
http.StatusOK,
&savedRegularStorage,
)
assert.False(t, savedRegularStorage.IsSystem)
assert.Equal(t, workspaceD.ID, savedRegularStorage.WorkspaceID)
// Delete workspace D
workspaces_testing.DeleteWorkspace(workspaceD, admin.Token, router)
// Verify system storage from workspace A still exists
repository := &StorageRepository{}
systemStorageAfterDeletion, err := repository.FindByID(savedSystemStorage.ID)
assert.NoError(t, err, "System storage should still exist after workspace D deletion")
assert.NotNil(t, systemStorageAfterDeletion)
assert.Equal(t, savedSystemStorage.ID, systemStorageAfterDeletion.ID)
assert.True(t, systemStorageAfterDeletion.IsSystem)
assert.Equal(t, workspaceA.ID, systemStorageAfterDeletion.WorkspaceID)
// Verify regular storage from workspace D was deleted
regularStorageAfterDeletion, err := repository.FindByID(savedRegularStorage.ID)
assert.Error(t, err, "Regular storage should be deleted with workspace D")
assert.Nil(t, regularStorageAfterDeletion)
// Cleanup
deleteStorage(t, router, savedSystemStorage.ID, admin.Token)
workspaces_testing.RemoveTestWorkspace(workspaceA, router)
}
func Test_DeleteWorkspace_WithOwnSystemStorage_ReturnsForbidden(t *testing.T) {
router := createRouter()
GetStorageService().SetStorageDatabaseCounter(&mockStorageDatabaseCounter{})
admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin)
workspaceA := workspaces_testing.CreateTestWorkspace("Workspace A", admin, router)
// Create a system storage assigned to workspace A
systemStorage := &Storage{
WorkspaceID: workspaceA.ID,
Type: StorageTypeLocal,
Name: "System Storage in A " + uuid.New().String(),
IsSystem: true,
LocalStorage: &local_storage.LocalStorage{},
}
var savedSystemStorage Storage
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/storages",
"Bearer "+admin.Token,
*systemStorage,
http.StatusOK,
&savedSystemStorage,
)
assert.True(t, savedSystemStorage.IsSystem)
assert.Equal(t, workspaceA.ID, savedSystemStorage.WorkspaceID)
// Attempt to delete workspace A - should fail because it has a system storage
resp := workspaces_testing.MakeAPIRequest(
router,
"DELETE",
"/api/v1/workspaces/"+workspaceA.ID.String(),
"Bearer "+admin.Token,
nil,
)
assert.Equal(t, http.StatusBadRequest, resp.Code, "Workspace deletion should fail")
assert.Contains(
t,
resp.Body.String(),
"system storage cannot be deleted due to workspace deletion",
"Error message should indicate system storage prevents deletion",
)
// Verify workspace still exists
workspaceRepo := &workspaces_repositories.WorkspaceRepository{}
workspaceAfterFailedDeletion, err := workspaceRepo.GetWorkspaceByID(workspaceA.ID)
assert.NoError(t, err, "Workspace should still exist after failed deletion")
assert.NotNil(t, workspaceAfterFailedDeletion)
assert.Equal(t, workspaceA.ID, workspaceAfterFailedDeletion.ID)
// Verify system storage still exists
repository := &StorageRepository{}
storageAfterFailedDeletion, err := repository.FindByID(savedSystemStorage.ID)
assert.NoError(t, err, "System storage should still exist after failed deletion")
assert.NotNil(t, storageAfterFailedDeletion)
assert.Equal(t, savedSystemStorage.ID, storageAfterFailedDeletion.ID)
assert.True(t, storageAfterFailedDeletion.IsSystem)
// Cleanup: Delete system storage first, then workspace can be deleted
deleteStorage(t, router, savedSystemStorage.ID, admin.Token)
workspaces_testing.DeleteWorkspace(workspaceA, admin.Token, router)
// Verify workspace was successfully deleted after storage removal
_, err = workspaceRepo.GetWorkspaceByID(workspaceA.ID)
assert.Error(t, err, "Workspace should be deleted after storage was removed")
}
func createRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
@@ -1983,6 +2121,7 @@ func createRouter() *gin.Engine {
}
audit_logs.SetupDependencies()
SetupDependencies()
GetStorageService().SetStorageDatabaseCounter(&mockStorageDatabaseCounter{})
return router

View File

@@ -25,6 +25,32 @@ func (s *StorageService) SetStorageDatabaseCounter(storageDatabaseCounter Storag
s.storageDatabaseCounter = storageDatabaseCounter
}
func (s *StorageService) OnBeforeWorkspaceDeletion(workspaceID uuid.UUID) error {
storages, err := s.storageRepository.FindByWorkspaceID(workspaceID)
if err != nil {
return fmt.Errorf("failed to get storages for workspace deletion: %w", err)
}
for _, storage := range storages {
if storage.IsSystem && storage.WorkspaceID != workspaceID {
// skip system storage from another workspace
continue
}
if storage.IsSystem && storage.WorkspaceID == workspaceID {
return fmt.Errorf(
"system storage cannot be deleted due to workspace deletion, please transfer or remove storage first",
)
}
if err := s.storageRepository.Delete(storage); err != nil {
return fmt.Errorf("failed to delete storage %s: %w", storage.ID, err)
}
}
return nil
}
func (s *StorageService) SaveStorage(
user *users_models.User,
workspaceID uuid.UUID,
@@ -351,18 +377,3 @@ func (s *StorageService) TransferStorageToWorkspace(
return nil
}
func (s *StorageService) OnBeforeWorkspaceDeletion(workspaceID uuid.UUID) error {
storages, err := s.storageRepository.FindByWorkspaceID(workspaceID)
if err != nil {
return fmt.Errorf("failed to get storages for workspace deletion: %w", err)
}
for _, storage := range storages {
if err := s.storageRepository.Delete(storage); err != nil {
return fmt.Errorf("failed to delete storage %s: %w", storage.ID, err)
}
}
return nil
}

View File

@@ -52,6 +52,10 @@ func (s *HealthcheckService) performHealthCheck() error {
if !s.backupBackgroundService.IsSchedulerRunning() {
return errors.New("backups are not running for more than 5 minutes")
}
if !s.backupBackgroundService.IsBackupNodesAvailable() {
return errors.New("no backup nodes available")
}
}
if config.GetEnv().IsProcessingNode {

View File

@@ -71,6 +71,7 @@ func tryInitVictoriaLogs() *VictoriaLogsWriter {
// Try to get config - this may fail early in startup
url := getVictoriaLogsURL()
username := getVictoriaLogsUsername()
password := getVictoriaLogsPassword()
if url == "" {
@@ -78,7 +79,7 @@ func tryInitVictoriaLogs() *VictoriaLogsWriter {
return nil
}
return NewVictoriaLogsWriter(url, password)
return NewVictoriaLogsWriter(url, username, password)
}
func ensureEnvLoaded() {
@@ -126,6 +127,10 @@ func getVictoriaLogsURL() string {
return os.Getenv("VICTORIA_LOGS_URL")
}
func getVictoriaLogsUsername() string {
return os.Getenv("VICTORIA_LOGS_USERNAME")
}
func getVictoriaLogsPassword() string {
return os.Getenv("VICTORIA_LOGS_PASSWORD")
}

View File

@@ -23,6 +23,7 @@ type logEntry struct {
type VictoriaLogsWriter struct {
url string
username string
password string
httpClient *http.Client
logChannel chan logEntry
@@ -33,11 +34,12 @@ type VictoriaLogsWriter struct {
logger *slog.Logger
}
func NewVictoriaLogsWriter(url, password string) *VictoriaLogsWriter {
func NewVictoriaLogsWriter(url, username, password string) *VictoriaLogsWriter {
ctx, cancel := context.WithCancel(context.Background())
writer := &VictoriaLogsWriter{
url: url,
username: username,
password: password,
httpClient: &http.Client{
Timeout: 10 * time.Second,
@@ -149,9 +151,9 @@ func (w *VictoriaLogsWriter) sendHTTP(entries []logEntry) error {
// Set headers
req.Header.Set("Content-Type", "application/x-ndjson")
// Set Basic Auth (password as username, empty password)
// Set Basic Auth (username:password)
if w.password != "" {
auth := base64.StdEncoding.EncodeToString([]byte(w.password + ":"))
auth := base64.StdEncoding.EncodeToString([]byte(w.username + ":" + w.password))
req.Header.Set("Authorization", "Basic "+auth)
}