[ci skip] more suggested improvements

This commit is contained in:
2026-02-06 15:35:18 +11:00
parent dfbaacb6f3
commit 0e3cf5aae9
24 changed files with 452 additions and 356 deletions

View File

@@ -34,9 +34,9 @@ steps:
path: /shared path: /shared
commands: commands:
- export PATH=/drone/src/pkg.tools:$PATH - export PATH=/drone/src/pkg.tools:$PATH
- go install github.com/a-h/templ/cmd/templ@latest - go install github.com/a-h/templ/cmd/templ@v0.3.977
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest - go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.29.0
- go install github.com/swaggo/swag/cmd/swag@latest - go install github.com/swaggo/swag/cmd/swag@v1.16.6
# - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest # - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
- sqlc generate - sqlc generate
- templ generate -path ./components - templ generate -path ./components

View File

@@ -86,6 +86,40 @@ Hourly and daily snapshot table retention can be configured in the settings file
- `settings.hourly_snapshot_max_age_days` (default: 60) - `settings.hourly_snapshot_max_age_days` (default: 60)
- `settings.daily_snapshot_max_age_months` (default: 12) - `settings.daily_snapshot_max_age_months` (default: 12)
## Runtime Environment Flags
These flags are read from the process environment (for example via `/etc/default/vctp` on systemd installs):
- `VCTP_ENCRYPTION_KEY`: optional explicit key source for credential encryption/decryption.
Recommended for stable behavior across host migrations/rebuilds.
- `VCTP_ENABLE_EXPERIMENTAL_POSTGRES`: set to `1` to enable experimental PostgreSQL driver startup.
- `VCTP_ENABLE_LEGACY_API`: set to `1` to re-enable deprecated legacy API endpoints temporarily.
## Credential Encryption Lifecycle
At startup, vCTP resolves `settings.vcenter_password` using this order:
1. If value starts with `enc:v1:`, decrypt using the active key.
2. If no prefix, attempt legacy ciphertext decryption (active key, then legacy fallback keys).
3. If decrypt fails and value length is greater than 2, treat value as plaintext.
When steps 2 or 3 succeed, vCTP rewrites the setting in-place to `enc:v1:<ciphertext>`.
Behavior notes:
- Plaintext values with length `<= 2` are rejected.
- Malformed ciphertext is rejected safely (short payloads do not panic).
- Legacy encrypted values can still be migrated forward automatically.
## Deprecated API Endpoints
These endpoints are considered legacy and are disabled by default unless `VCTP_ENABLE_LEGACY_API=1`:
- `/api/event/vm/create`
- `/api/event/vm/modify`
- `/api/event/vm/move`
- `/api/event/vm/delete`
- `/api/cleanup/updates`
- `/api/cleanup/vcenter`
When disabled, they return HTTP `410 Gone` with JSON error payload.
## Settings Reference ## Settings Reference
All configuration lives under the top-level `settings:` key in `vctp.yml`. All configuration lives under the top-level `settings:` key in `vctp.yml`.
@@ -94,7 +128,7 @@ General:
- `settings.log_output`: log format, `text` or `json` - `settings.log_output`: log format, `text` or `json`
Database: Database:
- `settings.database_driver`: `sqlite` or `postgres` - `settings.database_driver`: `sqlite` or `postgres` (experimental; requires `VCTP_ENABLE_EXPERIMENTAL_POSTGRES=1`)
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN - `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
HTTP/TLS: HTTP/TLS:
@@ -138,9 +172,9 @@ Filters/chargeback:
## Pre-requisite tools ## Pre-requisite tools
```shell ```shell
go install github.com/a-h/templ/cmd/templ@latest go install github.com/a-h/templ/cmd/templ@v0.3.977
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.29.0
go install github.com/swaggo/swag/cmd/swag@latest go install github.com/swaggo/swag/cmd/swag@v1.16.6
``` ```
## Database ## Database
@@ -163,6 +197,19 @@ Run `templ generate -path ./components` to generate code based on template files
## Documentation ## Documentation
Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs` Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs`
## Tests
Run the test suite:
```shell
go test ./...
```
Recommended static analysis:
```shell
go vet ./...
```
## CI/CD (Drone) ## CI/CD (Drone)
- `.drone.yml` defines a Docker pipeline: - `.drone.yml` defines a Docker pipeline:
- Restore/build caches for Go modules/tools. - Restore/build caches for Go modules/tools.

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@@ -186,20 +185,18 @@ LIMIT %d
} }
response := models.DailyCreationDiagnosticsResponse{ response := models.DailyCreationDiagnosticsResponse{
Status: "OK", Status: "OK",
Date: parsed.Format("2006-01-02"), Date: parsed.Format("2006-01-02"),
Table: tableName, Table: tableName,
TotalRows: totalRows, TotalRows: totalRows,
MissingCreationCount: missingTotal, MissingCreationCount: missingTotal,
MissingCreationPct: missingPct, MissingCreationPct: missingPct,
AvgIsPresentLtOneCount: avgIsPresentLtOne, AvgIsPresentLtOneCount: avgIsPresentLtOne,
MissingCreationPartialCount: missingPartialCount, MissingCreationPartialCount: missingPartialCount,
MissingByVcenter: byVcenter, MissingByVcenter: byVcenter,
Samples: samples, Samples: samples,
MissingCreationPartialSamples: partialSamples, MissingCreationPartialSamples: partialSamples,
} }
w.Header().Set("Content-Type", "application/json") writeJSON(w, http.StatusOK, response)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
} }

View File

@@ -57,9 +57,7 @@ func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
} }
h.Logger.Debug("encrypted plaintext payload", "input_length", len(plaintext)) h.Logger.Debug("encrypted plaintext payload", "input_length", len(plaintext))
w.Header().Set("Content-Type", "application/json") writeJSON(w, http.StatusOK, map[string]string{
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "OK", "status": "OK",
"message": cipherText, "message": cipherText,
"prefixed": encryptedValuePrefixV1 + cipherText, "prefixed": encryptedValuePrefixV1 + cipherText,

View File

@@ -0,0 +1,135 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"vctp/internal/secrets"
)
func newEncryptTestHandler() (*Handler, *secrets.Secrets) {
logger := newTestLogger()
key := []byte("0123456789abcdef0123456789abcdef")
secret := secrets.New(logger, key)
return &Handler{
Logger: logger,
Secret: secret,
}, secret
}
func decodeResponse(t *testing.T, rr *httptest.ResponseRecorder) map[string]string {
t.Helper()
var resp map[string]string
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to decode response body %q: %v", rr.Body.String(), err)
}
return resp
}
func TestEncryptDataRejectsWrongMethod(t *testing.T) {
h, _ := newEncryptTestHandler()
req := httptest.NewRequest(http.MethodGet, "/api/encrypt", nil)
rr := httptest.NewRecorder()
h.EncryptData(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected %d, got %d", http.StatusMethodNotAllowed, rr.Code)
}
resp := decodeResponse(t, rr)
if resp["status"] != "ERROR" {
t.Fatalf("expected status ERROR, got %#v", resp)
}
}
func TestEncryptDataRejectsInvalidJSON(t *testing.T) {
h, _ := newEncryptTestHandler()
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader("{"))
rr := httptest.NewRecorder()
h.EncryptData(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected %d, got %d", http.StatusBadRequest, rr.Code)
}
resp := decodeResponse(t, rr)
if resp["status"] != "ERROR" {
t.Fatalf("expected status ERROR, got %#v", resp)
}
}
func TestEncryptDataAcceptsPlaintextField(t *testing.T) {
h, secret := newEncryptTestHandler()
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"super-secret"}`))
rr := httptest.NewRecorder()
h.EncryptData(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
}
resp := decodeResponse(t, rr)
if resp["status"] != "OK" {
t.Fatalf("expected status OK, got %#v", resp)
}
if resp["ciphertext"] == "" || resp["prefixed"] == "" {
t.Fatalf("expected ciphertext+prefixed fields, got %#v", resp)
}
if !strings.HasPrefix(resp["prefixed"], encryptedValuePrefixV1) {
t.Fatalf("expected prefixed value with %q, got %q", encryptedValuePrefixV1, resp["prefixed"])
}
if !strings.EqualFold(resp["message"], resp["ciphertext"]) {
t.Fatalf("expected message to mirror ciphertext, got %#v", resp)
}
plain, err := secret.Decrypt(resp["ciphertext"])
if err != nil {
t.Fatalf("unable to decrypt ciphertext response: %v", err)
}
if string(plain) != "super-secret" {
t.Fatalf("unexpected decrypted value %q", string(plain))
}
}
func TestEncryptDataAcceptsLegacyValueField(t *testing.T) {
h, secret := newEncryptTestHandler()
body := bytes.NewBufferString(`{"value":"legacy-input"}`)
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", body)
rr := httptest.NewRecorder()
h.EncryptData(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
}
resp := decodeResponse(t, rr)
cipherText := resp["ciphertext"]
if cipherText == "" {
t.Fatalf("expected ciphertext in response, got %#v", resp)
}
plain, err := secret.Decrypt(cipherText)
if err != nil {
t.Fatalf("unable to decrypt ciphertext response: %v", err)
}
if string(plain) != "legacy-input" {
t.Fatalf("unexpected decrypted value %q", string(plain))
}
}
func TestEncryptDataRejectsMissingPayloadValue(t *testing.T) {
h, _ := newEncryptTestHandler()
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{}`))
rr := httptest.NewRecorder()
h.EncryptData(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected %d, got %d", http.StatusBadRequest, rr.Code)
}
resp := decodeResponse(t, rr)
if resp["status"] != "ERROR" {
t.Fatalf("expected status ERROR, got %#v", resp)
}
}

View File

@@ -0,0 +1,63 @@
package handler
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestDenyLegacyAPIDisabledByDefault(t *testing.T) {
t.Setenv(legacyAPIEnvVar, "")
h := &Handler{Logger: newTestLogger()}
rr := httptest.NewRecorder()
denied := h.denyLegacyAPI(rr, "/api/event/vm/create")
if !denied {
t.Fatal("expected legacy API to be denied by default")
}
if rr.Code != http.StatusGone {
t.Fatalf("expected %d, got %d", http.StatusGone, rr.Code)
}
if !strings.Contains(rr.Body.String(), "deprecated") {
t.Fatalf("unexpected response body: %s", rr.Body.String())
}
}
func TestDenyLegacyAPIEnabledViaEnv(t *testing.T) {
t.Setenv(legacyAPIEnvVar, "1")
h := &Handler{Logger: newTestLogger()}
rr := httptest.NewRecorder()
denied := h.denyLegacyAPI(rr, "/api/event/vm/create")
if denied {
t.Fatal("expected legacy API to be allowed when env var is set")
}
if rr.Body.Len() != 0 {
t.Fatalf("expected no response body write, got: %s", rr.Body.String())
}
}
func TestVmCreateEventHonorsLegacyGate(t *testing.T) {
h := &Handler{Logger: newTestLogger()}
t.Run("disabled", func(t *testing.T) {
t.Setenv(legacyAPIEnvVar, "")
req := httptest.NewRequest(http.MethodPost, "/api/event/vm/create", strings.NewReader("{invalid"))
rr := httptest.NewRecorder()
h.VmCreateEvent(rr, req)
if rr.Code != http.StatusGone {
t.Fatalf("expected %d, got %d", http.StatusGone, rr.Code)
}
})
t.Run("enabled", func(t *testing.T) {
t.Setenv(legacyAPIEnvVar, "1")
req := httptest.NewRequest(http.MethodPost, "/api/event/vm/create", strings.NewReader("{invalid"))
rr := httptest.NewRecorder()
h.VmCreateEvent(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected %d when gate is open, got %d", http.StatusBadRequest, rr.Code)
}
})
}

View File

@@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"vctp/internal/report" "vctp/internal/report"
@@ -24,12 +23,7 @@ func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request
reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx) reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx)
if err != nil { if err != nil {
h.Logger.Error("Failed to create report", "error", err) h.Logger.Error("Failed to create report", "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create xlsx report: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
})
return return
} }
@@ -58,12 +52,7 @@ func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) {
reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx) reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx)
if err != nil { if err != nil {
h.Logger.Error("Failed to create report", "error", err) h.Logger.Error("Failed to create report", "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create xlsx report: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
})
return return
} }

View File

@@ -0,0 +1,41 @@
package handler
import (
"encoding/json"
"net/http"
"vctp/server/models"
)
func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(payload)
}
func writeJSONStatus(w http.ResponseWriter, statusCode int, status string) {
writeJSON(w, statusCode, models.StatusResponse{
Status: status,
})
}
func writeJSONStatusMessage(w http.ResponseWriter, statusCode int, status, message string) {
writeJSON(w, statusCode, models.StatusMessageResponse{
Status: status,
Message: message,
})
}
func writeJSONOK(w http.ResponseWriter) {
writeJSONStatus(w, http.StatusOK, "OK")
}
func writeJSONOKMessage(w http.ResponseWriter, message string) {
writeJSONStatusMessage(w, http.StatusOK, "OK", message)
}
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
writeJSON(w, statusCode, models.ErrorResponse{
Status: "ERROR",
Message: message,
})
}

View File

@@ -2,13 +2,11 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"vctp/internal/settings" "vctp/internal/settings"
"vctp/internal/tasks" "vctp/internal/tasks"
"vctp/server/models"
) )
// SnapshotAggregateForce forces regeneration of a daily or monthly summary table. // SnapshotAggregateForce forces regeneration of a daily or monthly summary table.
@@ -102,18 +100,5 @@ func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request)
"granularity", granularity, "granularity", granularity,
"duration", time.Since(startedAt), "duration", time.Since(startedAt),
) )
w.Header().Set("Content-Type", "application/json") writeJSONOK(w)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
})
}
func writeJSONError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(models.ErrorResponse{
Status: "ERROR",
Message: message,
})
} }

View File

@@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -44,8 +43,5 @@ func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) {
} }
h.Logger.Info("Manual hourly snapshot completed", "duration", time.Since(started)) h.Logger.Info("Manual hourly snapshot completed", "duration", time.Since(started))
w.Header().Set("Content-Type", "application/json") writeJSONOK(w)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
})
} }

View File

@@ -2,9 +2,9 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"vctp/internal/report" "vctp/internal/report"
"vctp/server/models"
) )
// SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names. // SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names.
@@ -19,20 +19,28 @@ func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx := context.Background()
stats, err := report.MigrateSnapshotRegistry(ctx, h.Database) stats, err := report.MigrateSnapshotRegistry(ctx, h.Database)
if err != nil { if err != nil {
w.Header().Set("Content-Type", "application/json") writeJSON(w, http.StatusInternalServerError, models.SnapshotMigrationResponse{
w.WriteHeader(http.StatusInternalServerError) Status: "ERROR",
json.NewEncoder(w).Encode(map[string]interface{}{ Error: err.Error(),
"status": "ERROR", Stats: models.SnapshotMigrationStats{
"error": err.Error(), HourlyRenamed: stats.HourlyRenamed,
"stats": stats, HourlyRegistered: stats.HourlyRegistered,
DailyRegistered: stats.DailyRegistered,
MonthlyRegistered: stats.MonthlyRegistered,
Errors: stats.Errors,
},
}) })
return return
} }
w.Header().Set("Content-Type", "application/json") writeJSON(w, http.StatusOK, models.SnapshotMigrationResponse{
w.WriteHeader(http.StatusOK) Status: "OK",
json.NewEncoder(w).Encode(map[string]interface{}{ Stats: models.SnapshotMigrationStats{
"status": "OK", HourlyRenamed: stats.HourlyRenamed,
"stats": stats, HourlyRegistered: stats.HourlyRegistered,
DailyRegistered: stats.DailyRegistered,
MonthlyRegistered: stats.MonthlyRegistered,
Errors: stats.Errors,
},
}) })
} }

View File

@@ -1,13 +1,13 @@
package handler package handler
import ( import (
"encoding/json"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"vctp/internal/report" "vctp/internal/report"
"vctp/server/models"
) )
// SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk. // SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk.
@@ -54,15 +54,14 @@ func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http
regenerated++ regenerated++
} }
resp := map[string]interface{}{ resp := models.SnapshotRegenerateReportsResponse{
"status": "OK", Status: "OK",
"total": len(records), Total: len(records),
"regenerated": regenerated, Regenerated: regenerated,
"skipped": skipped, Skipped: skipped,
"errors": errors, Errors: errors,
"reports_dir": reportsDir, ReportsDir: reportsDir,
"snapshotType": "hourly", SnapshotType: "hourly",
} }
w.Header().Set("Content-Type", "application/json") writeJSON(w, http.StatusOK, resp)
json.NewEncoder(w).Encode(resp)
} }

View File

@@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -10,6 +9,7 @@ import (
"time" "time"
"vctp/db" "vctp/db"
"vctp/internal/report" "vctp/internal/report"
"vctp/server/models"
) )
// SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields. // SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields.
@@ -21,20 +21,18 @@ import (
// @Router /api/snapshots/repair [post] // @Router /api/snapshots/repair [post]
func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
return return
} }
h.Logger.Info("snapshot repair started", "scope", "daily") h.Logger.Info("snapshot repair started", "scope", "daily")
repaired, failed := h.repairDailySummaries(r.Context(), time.Now()) repaired, failed := h.repairDailySummaries(r.Context(), time.Now())
h.Logger.Info("snapshot repair finished", "daily_repaired", repaired, "daily_failed", failed) h.Logger.Info("snapshot repair finished", "daily_repaired", repaired, "daily_failed", failed)
resp := map[string]string{ writeJSON(w, http.StatusOK, models.SnapshotRepairResponse{
"status": "ok", Status: "OK",
"repaired": strconv.Itoa(repaired), Repaired: strconv.Itoa(repaired),
"failed": strconv.Itoa(failed), Failed: strconv.Itoa(failed),
} })
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
} }
func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repaired int, failed int) { func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repaired int, failed int) {
@@ -42,8 +40,8 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
dailyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "daily", "inventory_daily_summary_", "20060102", time.Time{}, now) dailyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "daily", "inventory_daily_summary_", "20060102", time.Time{}, now)
if err != nil { if err != nil {
h.Logger.Warn("failed to list daily summaries", "error", err) h.Logger.Warn("failed to list daily summaries", "error", err)
return 0, 1 return 0, 1
} }
for _, rec := range dailyRecs { for _, rec := range dailyRecs {
@@ -53,27 +51,27 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
if err := db.EnsureSummaryTable(ctx, dbConn, rec.TableName); err != nil { if err := db.EnsureSummaryTable(ctx, dbConn, rec.TableName); err != nil {
h.Logger.Warn("ensure summary table failed", "table", rec.TableName, "error", err) h.Logger.Warn("ensure summary table failed", "table", rec.TableName, "error", err)
failed++ failed++
continue continue
} }
hourlyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", dayStart, dayEnd) hourlyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", dayStart, dayEnd)
if err != nil || len(hourlyRecs) == 0 { if err != nil || len(hourlyRecs) == 0 {
h.Logger.Warn("no hourly snapshots for repair window", "table", rec.TableName, "error", err) h.Logger.Warn("no hourly snapshots for repair window", "table", rec.TableName, "error", err)
failed++ failed++
continue continue
} }
cols := []string{ cols := []string{
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`, `"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
`"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`, `"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`, `"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
`"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`, `"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`,
} }
union, err := buildUnionFromRecords(hourlyRecs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`) union, err := buildUnionFromRecords(hourlyRecs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
if err != nil { if err != nil {
h.Logger.Warn("failed to build union for repair", "table", rec.TableName, "error", err) h.Logger.Warn("failed to build union for repair", "table", rec.TableName, "error", err)
failed++ failed++
continue continue
} }
@@ -106,7 +104,7 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
// @Router /api/snapshots/repair/all [post] // @Router /api/snapshots/repair/all [post]
func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
return return
} }
@@ -135,15 +133,13 @@ func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) {
h.Logger.Info("repair suite step", "step", "monthly_refine") h.Logger.Info("repair suite step", "step", "monthly_refine")
monthlyRefined, monthlyFailed := h.refineMonthlyFromDaily(ctx, time.Now()) monthlyRefined, monthlyFailed := h.refineMonthlyFromDaily(ctx, time.Now())
resp := map[string]string{ writeJSON(w, http.StatusOK, models.SnapshotRepairSuiteResponse{
"status": "ok", Status: "OK",
"daily_repaired": strconv.Itoa(dailyRepaired), DailyRepaired: strconv.Itoa(dailyRepaired),
"daily_failed": strconv.Itoa(dailyFailed), DailyFailed: strconv.Itoa(dailyFailed),
"monthly_refined": strconv.Itoa(monthlyRefined), MonthlyRefined: strconv.Itoa(monthlyRefined),
"monthly_failed": strconv.Itoa(monthlyFailed), MonthlyFailed: strconv.Itoa(monthlyFailed),
} })
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
} }
func (h *Handler) refineMonthlyFromDaily(ctx context.Context, now time.Time) (refined int, failed int) { func (h *Handler) refineMonthlyFromDaily(ctx context.Context, now time.Time) (refined int, failed int) {
@@ -178,11 +174,11 @@ func (h *Handler) refineMonthlyFromDaily(ctx context.Context, now time.Time) (re
continue continue
} }
union, err := buildUnionFromRecords(recs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`) union, err := buildUnionFromRecords(recs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
if err != nil { if err != nil {
h.Logger.Warn("failed to build union for monthly refine", "table", summaryTable, "error", err) h.Logger.Warn("failed to build union for monthly refine", "table", summaryTable, "error", err)
failed++ failed++
continue continue
} }
if err := db.RefineCreationDeletionFromUnion(ctx, dbConn, summaryTable, union); err != nil { if err := db.RefineCreationDeletionFromUnion(ctx, dbConn, summaryTable, union); err != nil {

View File

@@ -2,7 +2,6 @@ package handler
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -62,24 +61,14 @@ func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request)
ctx := context.Background() ctx := context.Background()
tableName := r.URL.Query().Get("table") tableName := r.URL.Query().Get("table")
if tableName == "" { if tableName == "" {
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, "Missing table parameter")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": "Missing table parameter",
})
return return
} }
reportData, err := report.CreateTableReport(h.Logger, h.Database, ctx, tableName) reportData, err := report.CreateTableReport(h.Logger, h.Database, ctx, tableName)
if err != nil { if err != nil {
h.Logger.Error("Failed to create snapshot report", "error", err, "table", tableName) h.Logger.Error("Failed to create snapshot report", "error", err, "table", tableName)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create snapshot report: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to create snapshot report: '%s'", err),
})
return return
} }

View File

@@ -0,0 +1,10 @@
package handler
import (
"io"
"log/slog"
)
func newTestLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}

View File

@@ -43,12 +43,9 @@ func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
h.Logger.Error("Error received cleaning updates table", "error", err) h.Logger.Error("Error received cleaning updates table", "error", err)
w.WriteHeader(http.StatusInternalServerError) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Delete Request unsuccessful %s", err))
fmt.Fprintf(w, "Delete Request unsuccessful %s\n", err)
} else { } else {
h.Logger.Debug("Processed update cleanup successfully") h.Logger.Debug("Processed update cleanup successfully")
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Processed update cleanup successfully")
// TODO - return some JSON
fmt.Fprintf(w, "Processed update cleanup successfully")
} }
} }

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -35,21 +34,11 @@ func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
h.Logger.Error("No VMs found for vcenter", "url", vcUrl) h.Logger.Error("No VMs found for vcenter", "url", vcUrl)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("No match to vcenter details specified. vc_url: '%s'", vcUrl))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("No match to vcenter details specified. vc_url: '%s'", vcUrl),
})
return return
} else { } else {
h.Logger.Error("Error checking for vcenter to cleanup", "error", err, "url", vcUrl) h.Logger.Error("Error checking for vcenter to cleanup", "error", err, "url", vcUrl)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error checking for vcenter to cleanup. error: '%s'", err))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Error checking for vcenter to cleanup. error: '%s'", err),
})
return return
} }
} else { } else {
@@ -57,33 +46,18 @@ func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
err = h.Database.Queries().InventoryCleanupVcenter(ctx, vcUrl) err = h.Database.Queries().InventoryCleanupVcenter(ctx, vcUrl)
if err != nil { if err != nil {
h.Logger.Error("Error cleaning up VMs from Inventory table", "error", err, "url", vcUrl) h.Logger.Error("Error cleaning up VMs from Inventory table", "error", err, "url", vcUrl)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error cleaning up VMs from Inventory table. error: '%s'", err))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Error cleaning up VMs from Inventory table. error: '%s'", err),
})
return return
} else { } else {
// Successful cleanup // Successful cleanup
h.Logger.Debug("VMs successfully removed from inventory for vcenter", "url", vcUrl) h.Logger.Debug("VMs successfully removed from inventory for vcenter", "url", vcUrl)
w.Header().Set("Content-Type", "application/json") writeJSONOKMessage(w, fmt.Sprintf("Removed VMs from Inventory table for vcenter '%s'", vcUrl))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": fmt.Sprintf("Removed VMs from Inventory table for vcenter '%s'", vcUrl),
})
return return
} }
} }
} else { } else {
h.Logger.Error("Parameters not correctly specified", "url", vcUrl) h.Logger.Error("Parameters not correctly specified", "url", vcUrl)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Parameters not correctly specified. vc_url: '%s'", vcUrl))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Parameters not correctly specified. vc_url: '%s'", vcUrl),
})
return return
} }
} }

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -38,21 +37,11 @@ func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
h.Logger.Error("No VM found matching parameters", "vm_id", vmId, "datacenter_name", datacenterName) h.Logger.Error("No VM found matching parameters", "vm_id", vmId, "datacenter_name", datacenterName)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("No match to VM details specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("No match to VM details specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName),
})
return return
} else { } else {
h.Logger.Error("Error checking for VM to cleanup", "error", err, "vm_id", vmId, "datacenter_name", datacenterName) h.Logger.Error("Error checking for VM to cleanup", "error", err, "vm_id", vmId, "datacenter_name", datacenterName)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error checking for VM to cleanup. error: '%s'", err))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Error checking for VM to cleanup. error: '%s'", err),
})
return return
} }
} else { } else {
@@ -68,33 +57,18 @@ func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
err = h.Database.Queries().InventoryCleanup(ctx, params) err = h.Database.Queries().InventoryCleanup(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("Error cleaning up VM from Inventory table", "error", err, "vm_id", vmId, "datacenter_name", datacenterName) h.Logger.Error("Error cleaning up VM from Inventory table", "error", err, "vm_id", vmId, "datacenter_name", datacenterName)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error cleaning up VM from Inventory table. error: '%s'", err))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Error cleaning up VM from Inventory table. error: '%s'", err),
})
return return
} else { } else {
// Successful cleanup // Successful cleanup
h.Logger.Debug("VM successfully removed from inventory", "vm_name", vm.Name, "iid", vm.Iid, "vm_id", vmId, "datacenter_name", datacenterName) h.Logger.Debug("VM successfully removed from inventory", "vm_name", vm.Name, "iid", vm.Iid, "vm_id", vmId, "datacenter_name", datacenterName)
w.Header().Set("Content-Type", "application/json") writeJSONOKMessage(w, fmt.Sprintf("VM '%s' removed from Inventory table", vm.Name))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": fmt.Sprintf("VM '%s' removed from Inventory table", vm.Name),
})
return return
} }
} }
} else { } else {
h.Logger.Error("Parameters not correctly specified", "vm_id", vmId, "datacenter_name", datacenterName) h.Logger.Error("Parameters not correctly specified", "vm_id", vmId, "datacenter_name", datacenterName)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Parameters not correctly specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName))
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Parameters not correctly specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName),
})
return return
} }
} }

View File

@@ -20,11 +20,11 @@ import (
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
// @Produce text/plain // @Produce json
// @Param event body models.CloudEventReceived true "CloudEvent payload" // @Param event body models.CloudEventReceived true "CloudEvent payload"
// @Success 200 {string} string "Create event processed" // @Success 200 {object} models.StatusMessageResponse "Create event processed"
// @Failure 400 {string} string "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {string} string "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Router /api/event/vm/create [post] // @Router /api/event/vm/create [post]
func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
if h.denyLegacyAPI(w, "/api/event/vm/create") { if h.denyLegacyAPI(w, "/api/event/vm/create") {
@@ -41,8 +41,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.Logger.Error("Invalid data received", "error", err) h.Logger.Error("Invalid data received", "error", err)
fmt.Fprintf(w, "Invalid data received") writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
w.WriteHeader(http.StatusInternalServerError)
return return
} else { } else {
h.Logger.Debug("received input data", "length", len(reqBody)) h.Logger.Debug("received input data", "length", len(reqBody))
@@ -52,7 +51,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
var event models.CloudEventReceived var event models.CloudEventReceived
if err := json.Unmarshal(reqBody, &event); err != nil { if err := json.Unmarshal(reqBody, &event); err != nil {
h.Logger.Error("unable to decode json", "error", err) h.Logger.Error("unable to decode json", "error", err)
http.Error(w, "Invalid JSON body", http.StatusBadRequest) writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
return return
} else { } else {
h.Logger.Debug("successfully decoded JSON") h.Logger.Debug("successfully decoded JSON")
@@ -100,16 +99,13 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
result, err := h.Database.Queries().CreateEvent(context.Background(), params) result, err := h.Database.Queries().CreateEvent(context.Background(), params)
if err != nil { if err != nil {
h.Logger.Error("unable to perform database insert", "error", err) h.Logger.Error("unable to perform database insert", "error", err)
w.WriteHeader(http.StatusInternalServerError) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error: %v", err))
fmt.Fprintf(w, "Error : %v\n", err)
return return
} else { } else {
h.Logger.Debug("created database record", "insert_result", result) h.Logger.Debug("created database record", "insert_result", result)
} }
//h.Logger.Debug("received create request", "body", string(reqBody)) writeJSONOKMessage(w, fmt.Sprintf("Create request processed: %v", result))
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Create Request : %v\n", result)
} }
// prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c // prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c

View File

@@ -18,11 +18,11 @@ import (
// @Tags events // @Tags events
// @Deprecated // @Deprecated
// @Accept json // @Accept json
// @Produce text/plain // @Produce json
// @Param event body models.CloudEventReceived true "CloudEvent payload" // @Param event body models.CloudEventReceived true "CloudEvent payload"
// @Success 200 {string} string "Delete event processed" // @Success 200 {object} models.StatusMessageResponse "Delete event processed"
// @Failure 400 {string} string "Invalid request" // @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {string} string "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Router /api/event/vm/delete [post] // @Router /api/event/vm/delete [post]
func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
if h.denyLegacyAPI(w, "/api/event/vm/delete") { if h.denyLegacyAPI(w, "/api/event/vm/delete") {
@@ -36,8 +36,7 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.Logger.Error("Invalid data received", "error", err) h.Logger.Error("Invalid data received", "error", err)
fmt.Fprintf(w, "Invalid data received") writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
w.WriteHeader(http.StatusInternalServerError)
return return
} else { } else {
//h.Logger.Debug("received input data", "length", len(reqBody)) //h.Logger.Debug("received input data", "length", len(reqBody))
@@ -48,7 +47,7 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
if err := json.Unmarshal(reqBody, &event); err != nil { if err := json.Unmarshal(reqBody, &event); err != nil {
h.Logger.Error("unable to decode json", "error", err) h.Logger.Error("unable to decode json", "error", err)
prettyPrint(event) prettyPrint(event)
http.Error(w, "Invalid JSON body", http.StatusBadRequest) writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
return return
} else { } else {
h.Logger.Debug("successfully decoded deletion type cloud event", "vm_id", event.CloudEvent.Data.VM.VM.Value) h.Logger.Debug("successfully decoded deletion type cloud event", "vm_id", event.CloudEvent.Data.VM.VM.Value)
@@ -76,13 +75,10 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
h.Logger.Error("Error received marking VM as deleted", "error", err) h.Logger.Error("Error received marking VM as deleted", "error", err)
w.WriteHeader(http.StatusInternalServerError) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Delete Request unsuccessful %s", err))
fmt.Fprintf(w, "Delete Request unsuccessful %s\n", err)
} else { } else {
h.Logger.Debug("Processed VM Deletion event successfully") h.Logger.Debug("Processed VM Deletion event successfully")
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Processed VM Deletion event successfully")
// TODO - return some JSON
fmt.Fprintf(w, "Processed VM Deletion event successfully")
} }
//h.Logger.Debug("received delete request", "body", string(reqBody)) //h.Logger.Debug("received delete request", "body", string(reqBody))

View File

@@ -29,12 +29,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err) h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid data received: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Invalid data received: '%s'", err),
})
return return
} else { } else {
@@ -45,12 +40,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
var inData models.ImportReceived var inData models.ImportReceived
if err := json.Unmarshal(reqBody, &inData); err != nil { if err := json.Unmarshal(reqBody, &inData); err != nil {
h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err) h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to decode json request body: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to decode json request body: '%s'", err),
})
return return
} else { } else {
//h.Logger.Debug("successfully decoded JSON") //h.Logger.Debug("successfully decoded JSON")
@@ -59,12 +49,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(inData.Name, "vCLS-") { if strings.HasPrefix(inData.Name, "vCLS-") {
h.Logger.Info("Skipping internal vCLS VM import", "vm_name", inData.Name) h.Logger.Info("Skipping internal vCLS VM import", "vm_name", inData.Name)
w.Header().Set("Content-Type", "application/json") writeJSONOKMessage(w, fmt.Sprintf("Skipped internal VM '%s'", inData.Name))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": fmt.Sprintf("Skipped internal VM '%s'", inData.Name),
})
return return
} }
@@ -103,10 +88,5 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
h.Logger.Info("not adding vm to inventory table since record alraedy exists", "vm_id", inData.VmId, "datacenter_name", inData.Datacenter) h.Logger.Info("not adding vm to inventory table since record alraedy exists", "vm_id", inData.VmId, "datacenter_name", inData.Datacenter)
} }
w.Header().Set("Content-Type", "application/json") writeJSONOKMessage(w, fmt.Sprintf("Successfully processed import request for VM '%s'", inData.Name))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": fmt.Sprintf("Successfully processed import request for VM '%s'", inData.Name),
})
} }

View File

@@ -46,12 +46,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err) h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid data received: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Invalid data received: '%s'", err),
})
return return
} }
@@ -59,12 +54,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
var event models.CloudEventReceived var event models.CloudEventReceived
if err := json.Unmarshal(reqBody, &event); err != nil { if err := json.Unmarshal(reqBody, &event); err != nil {
h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err) h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to decode json request body: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to decode json request body: '%s'", err),
})
return return
} else { } else {
//h.Logger.Debug("successfully decoded JSON") //h.Logger.Debug("successfully decoded JSON")
@@ -74,12 +64,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
if event.CloudEvent.Data.ConfigChanges == nil { if event.CloudEvent.Data.ConfigChanges == nil {
h.Logger.Warn("Received event contains no config change") h.Logger.Warn("Received event contains no config change")
prettyPrint(event) prettyPrint(event)
w.Header().Set("Content-Type", "application/json") writeJSONStatusMessage(w, http.StatusAccepted, "OK", "Received update event successfully but no config changes were found")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": "Received update event successfully but no config changes were found",
})
return return
} else { } else {
h.Logger.Info("Received event contains config change info", "source", event.CloudEvent.Source, h.Logger.Info("Received event contains config change info", "source", event.CloudEvent.Source,
@@ -239,13 +224,8 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp) iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp)
if err2 != nil { if err2 != nil {
h.Logger.Error("Received error adding VM to inventory", "error", err2) h.Logger.Error("Received error adding VM to inventory", "error", err2)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
w.WriteHeader(http.StatusInternalServerError) event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2))
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2),
})
return return
} }
@@ -253,24 +233,14 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0} params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0}
} else { } else {
h.Logger.Error("Received zero for inventory id when adding VM to inventory") h.Logger.Error("Received zero for inventory id when adding VM to inventory")
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
w.WriteHeader(http.StatusInternalServerError) event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name))
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name),
})
return return
} }
} else { } else {
h.Logger.Error("unable to find existing inventory record for this VM", "error", err) h.Logger.Error("unable to find existing inventory record for this VM", "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but could not locate vm id '%s' and datacenter name '%s' within inventory table : %s",
w.WriteHeader(http.StatusInternalServerError) event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err))
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Valid request but could not locate vm id '%s' and datacenter name '%s' within inventory table : %s",
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err),
})
return return
} }
@@ -282,11 +252,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
if params.UpdateType == "diskChange" && invResult.ProvisionedDisk.Float64 == params.NewProvisionedDisk.Float64 { if params.UpdateType == "diskChange" && invResult.ProvisionedDisk.Float64 == params.NewProvisionedDisk.Float64 {
h.Logger.Info("VM update type was for disk size but current size of VM matches inventory record, no need for update record", h.Logger.Info("VM update type was for disk size but current size of VM matches inventory record, no need for update record",
"vm_name", invResult.Name, "db_value", invResult.ProvisionedDisk.Float64, "new_value", params.NewProvisionedDisk.Float64) "vm_name", invResult.Name, "db_value", invResult.ProvisionedDisk.Float64, "new_value", params.NewProvisionedDisk.Float64)
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Successfully processed vm modify event")
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": "Successfully processed vm modify event",
})
return return
} }
@@ -303,23 +269,17 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
result, err := h.Database.Queries().CreateUpdate(ctx, params) result, err := h.Database.Queries().CreateUpdate(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("unable to perform database insert", "error", err) h.Logger.Error("unable to perform database insert", "error", err)
w.WriteHeader(http.StatusInternalServerError) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error : %v", err))
fmt.Fprintf(w, "Error : %v\n", err)
return return
} else { } else {
h.Logger.Debug("created database record", "insert_result", result) h.Logger.Debug("created database record", "insert_result", result)
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Successfully processed vm modify event")
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": "Successfully processed vm modify event",
})
return return
} }
} else { } else {
h.Logger.Debug("Didn't find any configuration changes of interest", "id", event.CloudEvent.ID, h.Logger.Debug("Didn't find any configuration changes of interest", "id", event.CloudEvent.ID,
"vm", event.CloudEvent.Data.VM.Name, "config_changes", configChanges) "vm", event.CloudEvent.Data.VM.Name, "config_changes", configChanges)
w.WriteHeader(http.StatusAccepted) writeJSONStatusMessage(w, http.StatusAccepted, "OK", "Processed update event but no config changes were of interest")
fmt.Fprintf(w, "Processed update event but no config changes were of interest\n")
//prettyPrint(event.CloudEvent.Data.ConfigSpec) //prettyPrint(event.CloudEvent.Data.ConfigSpec)
} }
} }

View File

@@ -39,8 +39,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.Logger.Error("Invalid data received", "error", err) h.Logger.Error("Invalid data received", "error", err)
fmt.Fprintf(w, "Invalid data received") writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
w.WriteHeader(http.StatusInternalServerError)
return return
} else { } else {
//h.Logger.Debug("received input data", "length", len(reqBody)) //h.Logger.Debug("received input data", "length", len(reqBody))
@@ -51,12 +50,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
if err := json.Unmarshal(reqBody, &event); err != nil { if err := json.Unmarshal(reqBody, &event); err != nil {
h.Logger.Error("unable to unmarshal json", "error", err) h.Logger.Error("unable to unmarshal json", "error", err)
prettyPrint(reqBody) prettyPrint(reqBody)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to unmarshal JSON in request body: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to unmarshal JSON in request body: '%s'", err),
})
return return
} else { } else {
h.Logger.Debug("successfully decoded JSON") h.Logger.Debug("successfully decoded JSON")
@@ -66,12 +60,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
if event.CloudEvent.Data.OldParent == nil || event.CloudEvent.Data.NewParent == nil { if event.CloudEvent.Data.OldParent == nil || event.CloudEvent.Data.NewParent == nil {
h.Logger.Error("No resource pool data found in cloud event") h.Logger.Error("No resource pool data found in cloud event")
prettyPrint(event) prettyPrint(event)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusBadRequest, "CloudEvent missing resource pool data")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": "CloudEvent missing resource pool data",
})
return return
} }
@@ -92,13 +81,8 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp) iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp)
if err2 != nil { if err2 != nil {
h.Logger.Error("Received error adding VM to inventory", "error", err2) h.Logger.Error("Received error adding VM to inventory", "error", err2)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
w.WriteHeader(http.StatusInternalServerError) event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2))
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2),
})
return return
} }
@@ -106,13 +90,8 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0} params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0}
} else { } else {
h.Logger.Error("Received zero for inventory id when adding VM to inventory") h.Logger.Error("Received zero for inventory id when adding VM to inventory")
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
w.WriteHeader(http.StatusInternalServerError) event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name))
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name),
})
return return
} }
} }
@@ -142,22 +121,12 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
result, err := h.Database.Queries().CreateUpdate(ctx, params) result, err := h.Database.Queries().CreateUpdate(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("unable to perform database insert", "error", err) h.Logger.Error("unable to perform database insert", "error", err)
w.Header().Set("Content-Type", "application/json") writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to insert move event into database: '%s'", err))
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to insert move event into database: '%s'", err),
})
return return
} else { } else {
h.Logger.Debug("created database record", "insert_result", result) h.Logger.Debug("created database record", "insert_result", result)
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Successfully processed move event")
//fmt.Fprintf(w, "Processed update event: %v\n", result)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": "Successfully processed move event",
})
return return
} }
} }

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"vctp/db/queries" "vctp/db/queries"
"vctp/internal/vcenter" "vctp/internal/vcenter"
@@ -13,9 +12,9 @@ import (
// @Summary Refresh VM details // @Summary Refresh VM details
// @Description Queries vCenter and updates inventory records with missing details. // @Description Queries vCenter and updates inventory records with missing details.
// @Tags inventory // @Tags inventory
// @Produce text/plain // @Produce json
// @Success 200 {string} string "Update completed" // @Success 200 {object} models.StatusMessageResponse "Update completed"
// @Failure 500 {string} string "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Router /api/inventory/vm/update [post] // @Router /api/inventory/vm/update [post]
func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) { func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
var matchFound bool var matchFound bool
@@ -42,8 +41,8 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
results, err := h.Database.Queries().GetInventoryByVcenter(ctx, url) results, err := h.Database.Queries().GetInventoryByVcenter(ctx, url)
if err != nil { if err != nil {
h.Logger.Error("Unable to query inventory table", "error", err) h.Logger.Error("Unable to query inventory table", "error", err)
w.WriteHeader(http.StatusInternalServerError) writeJSONError(w, http.StatusInternalServerError, "Unable to query inventory table")
fmt.Fprintf(w, "Unable to query inventory table %s\n", err) return
} }
if len(results) == 0 { if len(results) == 0 {
@@ -115,7 +114,5 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
} }
h.Logger.Debug("Processed vm update successfully") h.Logger.Debug("Processed vm update successfully")
w.WriteHeader(http.StatusOK) writeJSONOKMessage(w, "Processed vm update successfully")
// TODO - return some JSON
fmt.Fprintf(w, "Processed vm update successfully")
} }