This commit is contained in:
2025-03-21 19:49:41 +11:00
commit 2ee078c992
60 changed files with 5410 additions and 0 deletions

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
## Build
FROM golang:1.22-alpine AS build
ARG VERSION='dev'
RUN apk update && apk add --no-cache curl
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
&& chmod +x tailwindcss-linux-x64 \
&& mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
RUN go install github.com/a-h/templ/cmd/templ@v0.2.663 \
&& go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
WORKDIR /app
COPY ./ /app
RUN templ generate -path ./components \
&& tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@${VERSION}.css --minify \
&& sqlc generate
RUN go build -ldflags="-s -w -X version.Value=${VERSION}" -o my-app
## Deploy
FROM gcr.io/distroless/static-debian12
WORKDIR /
COPY --from=build /app/my-app /my-app
EXPOSE 8080
CMD ["/my-app"]

24
Makefile Normal file
View File

@@ -0,0 +1,24 @@
format-templ:
@echo "Formatting templ files..."
@templ fmt .
generate-templ:
@echo "Generating templ files..."
@templ generate -path ./components
generate-templ-watch:
@echo "Generating templ files..."
@templ generate -path ./components -watch
generate-tailwind:
@echo "Generating tailwind files..."
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css
generate-tailwind-watch:
@echo "Generating tailwind files..."
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css --watch
generate-sql:
@echo "Generating sqlc ..."
@sqlc generate
run:
@echo "Running..."
@go run main.go
build:
@echo "Building..."
@go build -o ./build/wnzl-snow -ldflags="-s -w -X version.Value=1.0.0"

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# Go + HTMX Template
This is built from the template https://github.com/Piszmog/go-htmx-template that comes with everything you need to build a Web Application using Go (templ) and HTMX.
The template comes with a basic structure of using a SQL DB (`sqlc`), E2E testing (playwright), and styling (tailwindcss).
## Getting Started
Clone https://github.com/Piszmog/go-htmx-template
Once cloned, run the `update_module.sh` script to change the module to your module name.
```shell
./update_module my-new-module
```
## Technologies
A few different technologies are configured to help getting off the ground easier.
- [sqlc](https://sqlc.dev/) for database layer
- Stubbed to use SQLite
- This can be easily swapped with [sqlx](https://jmoiron.github.io/sqlx/)
- The script `upgrade_sqlc.sh` is available to upgrade GitHub Workflow files to latest sqlc version
- [Tailwind CSS](https://tailwindcss.com/) for styling
- Output is generated with the [CLI](https://tailwindcss.com/docs/installation)
- [templ](https://templ.guide/) for creating HTML
- The script `upgrade_templ.sh` is available to make upgrading easier
- [HTMX](https://htmx.org/) for HTML interaction
- The script `upgrade_htmx.sh` is available to make upgrading easier
- [goose](https://github.com/pressly/goose) for DB migrations
### DB
This is the directory that `sqlc` generates to. Update `queries.sql` to build
your database operations.
Once `queries.sql` is updated, run `make generate-sql` to update the generated models
#### DB Migrations
This project now uses [goose](https://github.com/pressly/goose) for DB migrations.
Install via `brew install goose` on a mac, or install via golang with command `go install github.com/pressly/goose/v3/cmd/goose@latest`
Create a new up/down migration file with this command
```shell
goose -dir db/migrations sqlite3 ./db.sqlite3 create init sql
```

BIN
build/vctp Executable file

Binary file not shown.

30
cert.pem Normal file
View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFIjCCAwqgAwIBAgIRAJPUpRr9NQCFpDrCPlyM7JYwDQYJKoZIhvcNAQELBQAw
DzENMAsGA1UEChMERFRNUzAeFw0yNDA5MTIwMDU4NDFaFw0yNTA5MTIwMDU4NDFa
MA8xDTALBgNVBAoTBERUTVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
AQDJhxPJHo/SqsEURebiRufmXRDYhl67PdglOGI6JC/VjCaqu42DYMO9tF6vuHBD
4/+/UpMBuOGTw+m6nEKdKKIZBuG0Hvka4bS0YPDHvqHZMWHK9qdPbmrB8q1N8W9F
Z+1gjIdeCK1wBjMTnfSxWOznKjffsz170bhOE6CHB4CP9nLMcShrvDcg0kTSbzM1
ptlxWGI5vabkx9spPU65NWEjd645Z4kMIutJZJjVKPlAqRr/fFeNgE5A9VhwzBot
5ZupKIZYuUMAMYkXjOhdqxDbWqfc8lkUvk5Jd9XtYB81uzvfpoH28Th4ILAdZd9h
vPh38sB/FyxSeKPHmW0IP1r8+EW/57ZPnQrGAOWs2z8Fp812vwQV0sr8j1B5NiTz
yu7I47vDdKZxulWS48frASxFFz4OojZAsoP6LipAMPolGIrnFn1/6UlOPnSVqkUb
D36PEIkOzE9AzMaSD/qs+k9lVHo+VTHL/qmz6u6COQiPL0u/ZInMcQmzJecCSDm4
Z8oVGeth+2vzlPo1iQHqptYYbln7oLaQZuCqdm4oNb+Hdyflhv8O78hUq7tcnQ8M
7/0jvlB6m1e4vuAxldBkaWr/cQudtzZnTiwHf2a3TH0h3ID0Gecog+hWb7EcAI2D
IK6Ji3uEBLag39XFuMzF/jLIUtYMz0Mb48v3Opbd8qe3aQIDAQABo3kwdzAOBgNV
HQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB
/zAdBgNVHQ4EFgQUZKrZX7aNZvhyG7HgIPznTYDeQ8QwIAYDVR0RBBkwF4IPbmF0
aG1icDE0LmxvY2FshwQKAAGhMA0GCSqGSIb3DQEBCwUAA4ICAQCqKmraTwKEGsRz
DqYSu5IDuYiXymBsIgT9A2prnd7ztX35CDi4n2msFKX0njcOAkBnapzSpDM1pbF8
a7H3F+pBfPZf1Cecgj1vooTImJgsS8SBR/otIjAQrX0G1DqcHoRfTDJ0iZZgE031
59dqMJOQDUI7KrLm4hhqCsLt9TR1MYi9V5rDZOXBO6rQ78VBrD0q3aJI/27xlPeJ
T5rEmj0HQrNfFs1QQD5JOd5EDILJnioE8wR4l14vfrVhNzr5xt7OQk6GT7txIuyK
l+k0xfClTN7PUZNcO3MnM/XeNZwIy1G0Dfi4BfdVBzR2vK7wZOJnwK02SMUrxez7
ZbtMra5hlDTT3KAgyYZs/u4rGjxG7r92A1vog3cuoVbrH3oEgqCHV6WId7ipxrmw
/3/S6X8M99jybg6Ac55pamiqx6PcJSnCKFwme1O+6yp0LEAIoRSdB8OkC6FSVmXs
tU5sNl4tqWL3BymVoiz7wpqJdZZEsinC9oE58nTN6k6pcd5pN04A/Uc7VySqN6Br
19v8Tn6JLIb/TLhUFGc25yMsDtgLN+bWC12RN2DXRakeM+330rKFxKgvIjNJzEPm
1HdxlfEMdRP1F3gTgHH8rVkh2a5VWlgnw3qKaOzjtFIdqGob7d1FJoaQgbD81Wqa
nNtoW4JrPFzi9P6EpTqci0IUCapgkQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,9 @@
package core
templ Footer() {
<footer class="fixed p-1 bottom-0 bg-gray-100 w-full border-t">
<div class="rounded-lg p-4 text-xs italic text-gray-700 text-center">
&copy; Nathan Coad (nathan.coad@dell.com)
</div>
</footer>
}

View File

@@ -0,0 +1,40 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Footer() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"fixed p-1 bottom-0 bg-gray-100 w-full border-t\"><div class=\"rounded-lg p-4 text-xs italic text-gray-700 text-center\">&copy; Nathan Coad (nathan.coad@dell.com)</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,14 @@
package core
import "wnzl-snow/version"
templ Header() {
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="wnzl-snow API endpoint"/>
<title>wnzl-snow API</title>
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
</head>
}

View File

@@ -0,0 +1,55 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "wnzl-snow/version"
func Header() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"wnzl-snow API endpoint\"><title>wnzl-snow API</title><script src=\"/assets/js/htmx@v2.0.2.min.js\"></script><link href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/assets/css/output@" + version.Value + ".css")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 12, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" rel=\"stylesheet\"></head>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,30 @@
package core
import "wnzl-snow/version"
templ HTML(title string, content templ.Component) {
<!DOCTYPE html>
<html lang="en">
@head(title)
@body(content)
</html>
}
templ head(title string) {
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="Hello world"/>
<title>{ title }</title>
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
</head>
}
templ body(content templ.Component) {
<body class="flex flex-col min-h-screen">
<main class="flex-grow">
@content
</main>
</body>
}

View File

@@ -0,0 +1,8 @@
package home
templ Home() {
<div class="text-center">
<h1 class="text-5xl font-bold">Welcome!</h1>
<p class="text-indigo-200 mt-4">This is a simple home screen.</p>
</div>
}

View File

@@ -0,0 +1,29 @@
package views
import (
"wnzl-snow/components/core"
)
type BuildInfo struct {
BuildTime string
SHA1Ver string
GoVersion string
}
templ Index(info BuildInfo) {
<!DOCTYPE html>
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen">
<main class="flex-grow">
<div>
<h1 class="text-5xl font-bold">Build Information</h1>
<p class="mt-4"><strong>Build Time:</strong> {info.BuildTime}</p>
<p class="mt-4"><strong>SHA1 Version:</strong> {info.SHA1Ver}</p>
<p class="mt-4"><strong>Go Runtime Version:</strong> {info.GoVersion}</p>
</div>
</main>
</body>
@core.Footer()
</html>
}

View File

@@ -0,0 +1,105 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"wnzl-snow/components/core"
)
type BuildInfo struct {
BuildTime string
SHA1Ver string
GoVersion string
}
func Index(info BuildInfo) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.Header().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"flex flex-col min-h-screen\"><main class=\"flex-grow\"><div><h1 class=\"text-5xl font-bold\">Build Information</h1><p class=\"mt-4\"><strong>Build Time:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 21, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"mt-4\"><strong>SHA1 Version:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 22, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"mt-4\"><strong>Go Runtime Version:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 23, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.Footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

129
db/db.go Normal file
View File

@@ -0,0 +1,129 @@
package db
import (
"database/sql"
"embed"
"log/slog"
"reflect"
"wnzl-snow/db/queries"
"github.com/jmoiron/sqlx"
"github.com/pressly/goose/v3"
)
//go:embed migrations/*.sql
var migrations embed.FS
type Database interface {
DB() *sqlx.DB
Queries() *queries.Queries
Logger() *slog.Logger
Close() error
}
func New(logger *slog.Logger, url string) (Database, error) {
db, err := newLocalDB(logger, url)
if err != nil {
return nil, err
}
if err = db.db.Ping(); err != nil {
return nil, err
}
return db, nil
}
// Migrate runs the migrations on the database. Assumes the database is SQLite.
func Migrate(db Database) error {
goose.SetBaseFS(migrations)
if err := goose.SetDialect("sqlite3"); err != nil {
panic(err)
}
if err := goose.Up(db.DB().DB, "migrations"); err != nil {
panic(err)
}
// TODO - replace with goose
/*
driver, err := sqlite3.WithInstance(db.DB(), &sqlite3.Config{})
if err != nil {
return fmt.Errorf("failed to create database driver: %w", err)
}
iofsDriver, err := iofs.New(migrations, "migrations")
if err != nil {
return fmt.Errorf("failed to create iofs: %w", err)
}
defer iofsDriver.Close()
m, err := migrate.NewWithInstance("iofs", iofsDriver, "sqlite3", driver)
if err != nil {
return fmt.Errorf("failed to create migration: %w", err)
}
return m.Up()
*/
return nil
}
// ConvertToSQLParams is a utility function that generically converts a struct to a corresponding sqlc-generated struct
func ConvertToSQLParams(input interface{}, output interface{}) {
inputVal := reflect.ValueOf(input).Elem()
outputVal := reflect.ValueOf(output).Elem()
for i := 0; i < outputVal.NumField(); i++ {
outputField := outputVal.Field(i)
inputField := inputVal.FieldByName(outputVal.Type().Field(i).Name)
if !inputField.IsValid() || !outputField.CanSet() {
continue
}
// Handle fields of type sql.NullString, sql.NullInt64, and normal string/int64 fields
switch outputField.Type() {
case reflect.TypeOf(sql.NullString{}):
// Handle sql.NullString
if inputField.Kind() == reflect.Ptr && inputField.IsNil() {
outputField.Set(reflect.ValueOf(sql.NullString{Valid: false}))
} else {
outputField.Set(reflect.ValueOf(sql.NullString{String: inputField.String(), Valid: true}))
}
case reflect.TypeOf(sql.NullInt64{}):
// Handle sql.NullInt64
if inputField.Int() == 0 {
outputField.Set(reflect.ValueOf(sql.NullInt64{Valid: false}))
} else {
outputField.Set(reflect.ValueOf(sql.NullInt64{Int64: inputField.Int(), Valid: true}))
}
case reflect.TypeOf(sql.NullFloat64{}):
// Handle sql.NullFloat64
if inputField.Float() == 0 {
outputField.Set(reflect.ValueOf(sql.NullFloat64{Valid: false}))
} else {
outputField.Set(reflect.ValueOf(sql.NullFloat64{Float64: inputField.Float(), Valid: true}))
}
case reflect.TypeOf(""):
// Handle normal string fields
if inputField.Kind() == reflect.Ptr && inputField.IsNil() {
outputField.SetString("") // Set to empty string if input is nil
} else {
outputField.SetString(inputField.String())
}
case reflect.TypeOf(int64(0)):
// Handle normal int64 fields
outputField.SetInt(inputField.Int())
case reflect.TypeOf(float64(0)):
// Handle normal float64 fields
outputField.SetFloat(inputField.Float())
}
}
}

94
db/local.go Normal file
View File

@@ -0,0 +1,94 @@
package db
import (
"database/sql"
"fmt"
"log/slog"
"wnzl-snow/db/queries"
//_ "github.com/tursodatabase/libsql-client-go/libsql"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
type LocalDB struct {
logger *slog.Logger
db *sqlx.DB
queries *queries.Queries
}
type DB struct {
writeDB *sql.DB
readDB *sql.DB
}
var _ Database = (*LocalDB)(nil)
func (d *LocalDB) DB() *sqlx.DB {
return d.db
}
func (d *LocalDB) Queries() *queries.Queries {
return d.queries
}
func (d *LocalDB) Logger() *slog.Logger {
return d.logger
}
func (d *LocalDB) Close() error {
fmt.Println("Shutting database")
d.logger.Debug("test")
return d.db.Close()
}
func newLocalDB(logger *slog.Logger, path string) (*LocalDB, error) {
// TODO - work out if https://kerkour.com/sqlite-for-servers is possible without using sqlx
/*
writeDB, err := sql.Open("sqlite3", "file:"+path)
if err != nil {
logger.Error("can't create writedb connection", "error", err)
return nil, err
}
writeDB.SetMaxOpenConns(1)
readDB, err := sql.Open("sqlite3", "file:"+path)
if err != nil {
logger.Error("can't create readdb connection", "error", err)
return nil, err
}
readDB.SetMaxOpenConns(max(4, runtime.NumCPU()))
*/
//db, err := sql.Open("libsql", "file:"+path)
db, err := sqlx.Open("sqlite", "file:"+path)
if err != nil {
logger.Error("can't open database connection", "error", err)
return nil, err
}
db.SetMaxOpenConns(1)
// Execute PRAGMA commands
pragmas := []string{
"PRAGMA journal_mode = WAL;",
"PRAGMA busy_timeout = 5000;",
"PRAGMA synchronous = NORMAL;",
"PRAGMA cache_size = 1000000000;",
"PRAGMA foreign_keys = true;",
"PRAGMA temp_store = MEMORY;",
}
for _, pragma := range pragmas {
logger.Debug("Setting pragma", "pragma", pragma)
_, err := db.Exec(pragma)
if err != nil {
logger.Error("failed to execute pragma statement", "stmt", pragma, "error", err)
return nil, err
}
}
return &LocalDB{logger: logger, db: db, queries: queries.New(db)}, nil
}

View File

@@ -0,0 +1,24 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS "Incoming" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
incident_number TEXT,
description TEXT,
short_description TEXT,
urgency TEXT,
impact TEXT,
state TEXT,
external_id TEXT,
work_notes TEXT,
assignment_group TEXT,
assigned_to TEXT,
category TEXT,
subcategory TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS "Incoming";
-- +goose StatementEnd

31
db/queries/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package queries
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

78
db/queries/models.go Normal file
View File

@@ -0,0 +1,78 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package queries
import (
"database/sql"
)
type Events struct {
Eid int64
CloudId string
Source string
EventTime sql.NullInt64
ChainId string
VmId sql.NullString
EventKey sql.NullString
DatacenterName sql.NullString
ComputeResourceName sql.NullString
UserName sql.NullString
Processed int64
DatacenterId sql.NullString
ComputeResourceId sql.NullString
VmName sql.NullString
EventType sql.NullString
}
type Inventory struct {
Iid int64
Name string
Vcenter string
VmId sql.NullString
EventKey sql.NullString
CloudId sql.NullString
CreationTime sql.NullInt64
DeletionTime sql.NullInt64
ResourcePool sql.NullString
VmType sql.NullString
Datacenter sql.NullString
Cluster sql.NullString
Folder sql.NullString
ProvisionedDisk sql.NullFloat64
InitialVcpus sql.NullInt64
InitialRam sql.NullInt64
IsTemplate interface{}
PoweredOn interface{}
SrmPlaceholder interface{}
VmUuid sql.NullString
}
type InventoryHistory struct {
Hid int64
InventoryId sql.NullInt64
ReportDate sql.NullInt64
UpdateTime sql.NullInt64
PreviousVcpus sql.NullInt64
PreviousRam sql.NullInt64
PreviousResourcePool sql.NullString
PreviousProvisionedDisk sql.NullFloat64
}
type Updates struct {
Uid int64
InventoryId sql.NullInt64
UpdateTime sql.NullInt64
UpdateType string
NewVcpus sql.NullInt64
NewRam sql.NullInt64
NewResourcePool sql.NullString
EventKey sql.NullString
EventId sql.NullString
NewProvisionedDisk sql.NullFloat64
UserName sql.NullString
PlaceholderChange sql.NullString
Name sql.NullString
RawChangeString []byte
}

121
db/queries/query.sql Normal file
View File

@@ -0,0 +1,121 @@
-- name: ListInventory :many
SELECT * FROM "Inventory"
ORDER BY "Name";
-- name: GetReportInventory :many
SELECT * FROM "Inventory"
ORDER BY "CreationTime";
-- name: GetInventoryByName :many
SELECT * FROM "Inventory"
WHERE "Name" = ?;
-- name: GetInventoryByVcenter :many
SELECT * FROM "Inventory"
WHERE "Vcenter" = ?;
-- name: GetInventoryVmId :one
SELECT * FROM "Inventory"
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: GetInventoryVmUuid :one
SELECT * FROM "Inventory"
WHERE "VmUuid" = sqlc.arg('vmUuid') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: GetInventoryVcUrl :many
SELECT * FROM "Inventory"
WHERE "Vcenter" = sqlc.arg('vc');
-- name: GetInventoryEventId :one
SELECT * FROM "Inventory"
WHERE "CloudId" = ? LIMIT 1;
-- name: CreateInventory :one
INSERT INTO "Inventory" (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
-- name: InventoryUpdate :exec
UPDATE "Inventory"
SET "VmUuid" = sqlc.arg('uuid'), "SrmPlaceholder" = sqlc.arg('srmPlaceholder')
WHERE "Iid" = sqlc.arg('iid');
-- name: InventoryMarkDeleted :exec
UPDATE "Inventory"
SET "DeletionTime" = sqlc.arg('deletionTime')
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: InventoryCleanup :exec
DELETE FROM "Inventory"
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName')
RETURNING *;
-- name: InventoryCleanupVcenter :exec
DELETE FROM "Inventory"
WHERE "Vcenter" = sqlc.arg('vc')
RETURNING *;
-- name: InventoryCleanupTemplates :exec
DELETE FROM "Inventory"
WHERE "IsTemplate" = "TRUE"
RETURNING *;
-- name: CreateUpdate :one
INSERT INTO "Updates" (
"InventoryId", "Name", "EventKey", "EventId", "UpdateTime", "UpdateType", "NewVcpus", "NewRam", "NewResourcePool", "NewProvisionedDisk", "UserName", "PlaceholderChange", "RawChangeString"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
-- name: GetReportUpdates :many
SELECT * FROM "Updates"
ORDER BY "UpdateTime";
-- name: GetVmUpdates :many
SELECT * FROM "Updates"
WHERE "UpdateType" = sqlc.arg('updateType') AND "InventoryId" = sqlc.arg('InventoryId');
-- name: CleanupUpdates :exec
DELETE FROM "Updates"
WHERE "UpdateType" = sqlc.arg('updateType') AND "UpdateTime" <= sqlc.arg('updateTime')
RETURNING *;
-- name: CleanupUpdatesNullVm :exec
DELETE FROM "Updates"
WHERE "InventoryId" IS NULL
RETURNING *;
-- name: CreateEvent :one
INSERT INTO "Events" (
"CloudId", "Source", "EventTime", "ChainId", "VmId", "VmName", "EventType", "EventKey", "DatacenterId", "DatacenterName", "ComputeResourceId", "ComputeResourceName", "UserName"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
-- name: ListEvents :many
SELECT * FROM "Events"
ORDER BY "EventTime";
-- name: ListUnprocessedEvents :many
SELECT * FROM "Events"
WHERE "Processed" = 0
AND "EventTime" > sqlc.arg('eventTime')
ORDER BY "EventTime";
-- name: UpdateEventsProcessed :exec
UPDATE "Events"
SET "Processed" = 1
WHERE "Eid" = sqlc.arg('eid');
-- name: CreateInventoryHistory :one
INSERT INTO "InventoryHistory" (
"InventoryId", "ReportDate", "UpdateTime", "PreviousVcpus", "PreviousRam", "PreviousResourcePool", "PreviousProvisionedDisk"
) VALUES(
?, ?, ?, ?, ?, ?, ?
)
RETURNING *;

899
db/queries/query.sql.go Normal file
View File

@@ -0,0 +1,899 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: query.sql
package queries
import (
"context"
"database/sql"
)
const cleanupUpdates = `-- name: CleanupUpdates :exec
DELETE FROM "Updates"
WHERE "UpdateType" = ?1 AND "UpdateTime" <= ?2
RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString
`
type CleanupUpdatesParams struct {
UpdateType string
UpdateTime sql.NullInt64
}
func (q *Queries) CleanupUpdates(ctx context.Context, arg CleanupUpdatesParams) error {
_, err := q.db.ExecContext(ctx, cleanupUpdates, arg.UpdateType, arg.UpdateTime)
return err
}
const cleanupUpdatesNullVm = `-- name: CleanupUpdatesNullVm :exec
DELETE FROM "Updates"
WHERE "InventoryId" IS NULL
RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString
`
func (q *Queries) CleanupUpdatesNullVm(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, cleanupUpdatesNullVm)
return err
}
const createEvent = `-- name: CreateEvent :one
INSERT INTO "Events" (
"CloudId", "Source", "EventTime", "ChainId", "VmId", "VmName", "EventType", "EventKey", "DatacenterId", "DatacenterName", "ComputeResourceId", "ComputeResourceName", "UserName"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType
`
type CreateEventParams struct {
CloudId string
Source string
EventTime sql.NullInt64
ChainId string
VmId sql.NullString
VmName sql.NullString
EventType sql.NullString
EventKey sql.NullString
DatacenterId sql.NullString
DatacenterName sql.NullString
ComputeResourceId sql.NullString
ComputeResourceName sql.NullString
UserName sql.NullString
}
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Events, error) {
row := q.db.QueryRowContext(ctx, createEvent,
arg.CloudId,
arg.Source,
arg.EventTime,
arg.ChainId,
arg.VmId,
arg.VmName,
arg.EventType,
arg.EventKey,
arg.DatacenterId,
arg.DatacenterName,
arg.ComputeResourceId,
arg.ComputeResourceName,
arg.UserName,
)
var i Events
err := row.Scan(
&i.Eid,
&i.CloudId,
&i.Source,
&i.EventTime,
&i.ChainId,
&i.VmId,
&i.EventKey,
&i.DatacenterName,
&i.ComputeResourceName,
&i.UserName,
&i.Processed,
&i.DatacenterId,
&i.ComputeResourceId,
&i.VmName,
&i.EventType,
)
return i, err
}
const createInventory = `-- name: CreateInventory :one
INSERT INTO "Inventory" (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
type CreateInventoryParams struct {
Name string
Vcenter string
VmId sql.NullString
VmUuid sql.NullString
EventKey sql.NullString
CloudId sql.NullString
CreationTime sql.NullInt64
ResourcePool sql.NullString
VmType sql.NullString
IsTemplate interface{}
Datacenter sql.NullString
Cluster sql.NullString
Folder sql.NullString
ProvisionedDisk sql.NullFloat64
InitialVcpus sql.NullInt64
InitialRam sql.NullInt64
SrmPlaceholder interface{}
PoweredOn interface{}
}
func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams) (Inventory, error) {
row := q.db.QueryRowContext(ctx, createInventory,
arg.Name,
arg.Vcenter,
arg.VmId,
arg.VmUuid,
arg.EventKey,
arg.CloudId,
arg.CreationTime,
arg.ResourcePool,
arg.VmType,
arg.IsTemplate,
arg.Datacenter,
arg.Cluster,
arg.Folder,
arg.ProvisionedDisk,
arg.InitialVcpus,
arg.InitialRam,
arg.SrmPlaceholder,
arg.PoweredOn,
)
var i Inventory
err := row.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
)
return i, err
}
const createInventoryHistory = `-- name: CreateInventoryHistory :one
INSERT INTO "InventoryHistory" (
"InventoryId", "ReportDate", "UpdateTime", "PreviousVcpus", "PreviousRam", "PreviousResourcePool", "PreviousProvisionedDisk"
) VALUES(
?, ?, ?, ?, ?, ?, ?
)
RETURNING Hid, InventoryId, ReportDate, UpdateTime, PreviousVcpus, PreviousRam, PreviousResourcePool, PreviousProvisionedDisk
`
type CreateInventoryHistoryParams struct {
InventoryId sql.NullInt64
ReportDate sql.NullInt64
UpdateTime sql.NullInt64
PreviousVcpus sql.NullInt64
PreviousRam sql.NullInt64
PreviousResourcePool sql.NullString
PreviousProvisionedDisk sql.NullFloat64
}
func (q *Queries) CreateInventoryHistory(ctx context.Context, arg CreateInventoryHistoryParams) (InventoryHistory, error) {
row := q.db.QueryRowContext(ctx, createInventoryHistory,
arg.InventoryId,
arg.ReportDate,
arg.UpdateTime,
arg.PreviousVcpus,
arg.PreviousRam,
arg.PreviousResourcePool,
arg.PreviousProvisionedDisk,
)
var i InventoryHistory
err := row.Scan(
&i.Hid,
&i.InventoryId,
&i.ReportDate,
&i.UpdateTime,
&i.PreviousVcpus,
&i.PreviousRam,
&i.PreviousResourcePool,
&i.PreviousProvisionedDisk,
)
return i, err
}
const createUpdate = `-- name: CreateUpdate :one
INSERT INTO "Updates" (
"InventoryId", "Name", "EventKey", "EventId", "UpdateTime", "UpdateType", "NewVcpus", "NewRam", "NewResourcePool", "NewProvisionedDisk", "UserName", "PlaceholderChange", "RawChangeString"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString
`
type CreateUpdateParams struct {
InventoryId sql.NullInt64
Name sql.NullString
EventKey sql.NullString
EventId sql.NullString
UpdateTime sql.NullInt64
UpdateType string
NewVcpus sql.NullInt64
NewRam sql.NullInt64
NewResourcePool sql.NullString
NewProvisionedDisk sql.NullFloat64
UserName sql.NullString
PlaceholderChange sql.NullString
RawChangeString []byte
}
func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Updates, error) {
row := q.db.QueryRowContext(ctx, createUpdate,
arg.InventoryId,
arg.Name,
arg.EventKey,
arg.EventId,
arg.UpdateTime,
arg.UpdateType,
arg.NewVcpus,
arg.NewRam,
arg.NewResourcePool,
arg.NewProvisionedDisk,
arg.UserName,
arg.PlaceholderChange,
arg.RawChangeString,
)
var i Updates
err := row.Scan(
&i.Uid,
&i.InventoryId,
&i.UpdateTime,
&i.UpdateType,
&i.NewVcpus,
&i.NewRam,
&i.NewResourcePool,
&i.EventKey,
&i.EventId,
&i.NewProvisionedDisk,
&i.UserName,
&i.PlaceholderChange,
&i.Name,
&i.RawChangeString,
)
return i, err
}
const getInventoryByName = `-- name: GetInventoryByName :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "Name" = ?
`
func (q *Queries) GetInventoryByName(ctx context.Context, name string) ([]Inventory, error) {
rows, err := q.db.QueryContext(ctx, getInventoryByName, name)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Inventory
for rows.Next() {
var i Inventory
if err := rows.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInventoryByVcenter = `-- name: GetInventoryByVcenter :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "Vcenter" = ?
`
func (q *Queries) GetInventoryByVcenter(ctx context.Context, vcenter string) ([]Inventory, error) {
rows, err := q.db.QueryContext(ctx, getInventoryByVcenter, vcenter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Inventory
for rows.Next() {
var i Inventory
if err := rows.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInventoryEventId = `-- name: GetInventoryEventId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "CloudId" = ? LIMIT 1
`
func (q *Queries) GetInventoryEventId(ctx context.Context, cloudid sql.NullString) (Inventory, error) {
row := q.db.QueryRowContext(ctx, getInventoryEventId, cloudid)
var i Inventory
err := row.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
)
return i, err
}
const getInventoryVcUrl = `-- name: GetInventoryVcUrl :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "Vcenter" = ?1
`
func (q *Queries) GetInventoryVcUrl(ctx context.Context, vc string) ([]Inventory, error) {
rows, err := q.db.QueryContext(ctx, getInventoryVcUrl, vc)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Inventory
for rows.Next() {
var i Inventory
if err := rows.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInventoryVmId = `-- name: GetInventoryVmId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "VmId" = ?1 AND "Datacenter" = ?2
`
type GetInventoryVmIdParams struct {
VmId sql.NullString
DatacenterName sql.NullString
}
func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdParams) (Inventory, error) {
row := q.db.QueryRowContext(ctx, getInventoryVmId, arg.VmId, arg.DatacenterName)
var i Inventory
err := row.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
)
return i, err
}
const getInventoryVmUuid = `-- name: GetInventoryVmUuid :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
WHERE "VmUuid" = ?1 AND "Datacenter" = ?2
`
type GetInventoryVmUuidParams struct {
VmUuid sql.NullString
DatacenterName sql.NullString
}
func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuidParams) (Inventory, error) {
row := q.db.QueryRowContext(ctx, getInventoryVmUuid, arg.VmUuid, arg.DatacenterName)
var i Inventory
err := row.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
)
return i, err
}
const getReportInventory = `-- name: GetReportInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
ORDER BY "CreationTime"
`
func (q *Queries) GetReportInventory(ctx context.Context) ([]Inventory, error) {
rows, err := q.db.QueryContext(ctx, getReportInventory)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Inventory
for rows.Next() {
var i Inventory
if err := rows.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getReportUpdates = `-- name: GetReportUpdates :many
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM "Updates"
ORDER BY "UpdateTime"
`
func (q *Queries) GetReportUpdates(ctx context.Context) ([]Updates, error) {
rows, err := q.db.QueryContext(ctx, getReportUpdates)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Updates
for rows.Next() {
var i Updates
if err := rows.Scan(
&i.Uid,
&i.InventoryId,
&i.UpdateTime,
&i.UpdateType,
&i.NewVcpus,
&i.NewRam,
&i.NewResourcePool,
&i.EventKey,
&i.EventId,
&i.NewProvisionedDisk,
&i.UserName,
&i.PlaceholderChange,
&i.Name,
&i.RawChangeString,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getVmUpdates = `-- name: GetVmUpdates :many
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM "Updates"
WHERE "UpdateType" = ?1 AND "InventoryId" = ?2
`
type GetVmUpdatesParams struct {
UpdateType string
InventoryId sql.NullInt64
}
func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]Updates, error) {
rows, err := q.db.QueryContext(ctx, getVmUpdates, arg.UpdateType, arg.InventoryId)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Updates
for rows.Next() {
var i Updates
if err := rows.Scan(
&i.Uid,
&i.InventoryId,
&i.UpdateTime,
&i.UpdateType,
&i.NewVcpus,
&i.NewRam,
&i.NewResourcePool,
&i.EventKey,
&i.EventId,
&i.NewProvisionedDisk,
&i.UserName,
&i.PlaceholderChange,
&i.Name,
&i.RawChangeString,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const inventoryCleanup = `-- name: InventoryCleanup :exec
DELETE FROM "Inventory"
WHERE "VmId" = ?1 AND "Datacenter" = ?2
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
type InventoryCleanupParams struct {
VmId sql.NullString
DatacenterName sql.NullString
}
func (q *Queries) InventoryCleanup(ctx context.Context, arg InventoryCleanupParams) error {
_, err := q.db.ExecContext(ctx, inventoryCleanup, arg.VmId, arg.DatacenterName)
return err
}
const inventoryCleanupTemplates = `-- name: InventoryCleanupTemplates :exec
DELETE FROM "Inventory"
WHERE "IsTemplate" = "TRUE"
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, inventoryCleanupTemplates)
return err
}
const inventoryCleanupVcenter = `-- name: InventoryCleanupVcenter :exec
DELETE FROM "Inventory"
WHERE "Vcenter" = ?1
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
func (q *Queries) InventoryCleanupVcenter(ctx context.Context, vc string) error {
_, err := q.db.ExecContext(ctx, inventoryCleanupVcenter, vc)
return err
}
const inventoryMarkDeleted = `-- name: InventoryMarkDeleted :exec
UPDATE "Inventory"
SET "DeletionTime" = ?1
WHERE "VmId" = ?2 AND "Datacenter" = ?3
`
type InventoryMarkDeletedParams struct {
DeletionTime sql.NullInt64
VmId sql.NullString
DatacenterName sql.NullString
}
func (q *Queries) InventoryMarkDeleted(ctx context.Context, arg InventoryMarkDeletedParams) error {
_, err := q.db.ExecContext(ctx, inventoryMarkDeleted, arg.DeletionTime, arg.VmId, arg.DatacenterName)
return err
}
const inventoryUpdate = `-- name: InventoryUpdate :exec
UPDATE "Inventory"
SET "VmUuid" = ?1, "SrmPlaceholder" = ?2
WHERE "Iid" = ?3
`
type InventoryUpdateParams struct {
Uuid sql.NullString
SrmPlaceholder interface{}
Iid int64
}
func (q *Queries) InventoryUpdate(ctx context.Context, arg InventoryUpdateParams) error {
_, err := q.db.ExecContext(ctx, inventoryUpdate, arg.Uuid, arg.SrmPlaceholder, arg.Iid)
return err
}
const listEvents = `-- name: ListEvents :many
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM "Events"
ORDER BY "EventTime"
`
func (q *Queries) ListEvents(ctx context.Context) ([]Events, error) {
rows, err := q.db.QueryContext(ctx, listEvents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Events
for rows.Next() {
var i Events
if err := rows.Scan(
&i.Eid,
&i.CloudId,
&i.Source,
&i.EventTime,
&i.ChainId,
&i.VmId,
&i.EventKey,
&i.DatacenterName,
&i.ComputeResourceName,
&i.UserName,
&i.Processed,
&i.DatacenterId,
&i.ComputeResourceId,
&i.VmName,
&i.EventType,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listInventory = `-- name: ListInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
ORDER BY "Name"
`
func (q *Queries) ListInventory(ctx context.Context) ([]Inventory, error) {
rows, err := q.db.QueryContext(ctx, listInventory)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Inventory
for rows.Next() {
var i Inventory
if err := rows.Scan(
&i.Iid,
&i.Name,
&i.Vcenter,
&i.VmId,
&i.EventKey,
&i.CloudId,
&i.CreationTime,
&i.DeletionTime,
&i.ResourcePool,
&i.VmType,
&i.Datacenter,
&i.Cluster,
&i.Folder,
&i.ProvisionedDisk,
&i.InitialVcpus,
&i.InitialRam,
&i.IsTemplate,
&i.PoweredOn,
&i.SrmPlaceholder,
&i.VmUuid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUnprocessedEvents = `-- name: ListUnprocessedEvents :many
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM "Events"
WHERE "Processed" = 0
AND "EventTime" > ?1
ORDER BY "EventTime"
`
func (q *Queries) ListUnprocessedEvents(ctx context.Context, eventtime sql.NullInt64) ([]Events, error) {
rows, err := q.db.QueryContext(ctx, listUnprocessedEvents, eventtime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Events
for rows.Next() {
var i Events
if err := rows.Scan(
&i.Eid,
&i.CloudId,
&i.Source,
&i.EventTime,
&i.ChainId,
&i.VmId,
&i.EventKey,
&i.DatacenterName,
&i.ComputeResourceName,
&i.UserName,
&i.Processed,
&i.DatacenterId,
&i.ComputeResourceId,
&i.VmName,
&i.EventType,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateEventsProcessed = `-- name: UpdateEventsProcessed :exec
UPDATE "Events"
SET "Processed" = 1
WHERE "Eid" = ?1
`
func (q *Queries) UpdateEventsProcessed(ctx context.Context, eid int64) error {
_, err := q.db.ExecContext(ctx, updateEventsProcessed, eid)
return err
}

BIN
dist/.DS_Store vendored Normal file

Binary file not shown.

BIN
dist/assets/.DS_Store vendored Normal file

Binary file not shown.

536
dist/assets/css/mvp.css vendored Normal file
View File

@@ -0,0 +1,536 @@
/* MVP.css v1.14 - https://github.com/andybrewer/mvp */
:root {
--active-brightness: 0.85;
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-link: #118bee;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-table: #118bee;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-table: left;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
/* --width-content: 1080px;*/
}
@media (prefers-color-scheme: dark) {
:root[color-mode="user"] {
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-link: #0097fc;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-table: #0097fc;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Layout */
article aside {
background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
line-height: var(--line-height);
margin: 0;
overflow-x: hidden;
padding: 0;
}
footer,
header,
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 1rem 1rem;
}
hr {
background-color: var(--color-bg-secondary);
border: none;
height: 1px;
margin: 2rem 0;
width: 100%;
}
section {
display: flex;
flex-wrap: wrap;
justify-content: var(--justify-important);
}
section img,
article img {
max-width: 100%;
}
section pre {
overflow: auto;
}
section aside {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
margin: 1rem;
padding: 1.25rem;
width: var(--width-card);
}
section aside:hover {
box-shadow: var(--box-shadow) var(--color-bg-secondary);
}
[hidden] {
display: none;
}
/* Headers */
article header,
div header,
main header {
padding-top: 0;
}
header {
text-align: var(--justify-important);
}
header a b,
header a em,
header a i,
header a strong {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
header nav img {
margin: 1rem 0;
}
section header {
padding-top: 0;
width: 100%;
}
/* Nav */
nav {
align-items: center;
display: flex;
font-weight: bold;
justify-content: space-between;
margin-bottom: 7rem;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline-block;
margin: 0 0.5rem;
position: relative;
text-align: left;
}
/* Nav Dropdown */
nav ul li:hover ul {
display: block;
}
nav ul li ul {
background: var(--color-bg);
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: none;
height: auto;
left: -2px;
padding: .5rem 1rem;
position: absolute;
top: 1.7rem;
white-space: nowrap;
width: auto;
z-index: 1;
}
nav ul li ul::before {
/* fill gap above to make mousing over them easier */
content: "";
position: absolute;
left: 0;
right: 0;
top: -0.5rem;
height: 0.5rem;
}
nav ul li ul li,
nav ul li ul li a {
display: block;
}
/* Typography */
code,
samp {
background-color: var(--color-accent);
border-radius: var(--border-radius);
color: var(--color-text);
display: inline-block;
margin: 0 0.1rem;
padding: 0 0.5rem;
}
details {
margin: 1.3rem 0;
}
details summary {
font-weight: bold;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: var(--line-height);
}
mark {
padding: 0.1rem;
}
ol li,
ul li {
padding: 0.2rem 0;
}
p {
margin: 0.75rem 0;
padding: 0;
width: 100%;
}
pre {
margin: 1rem 0;
max-width: var(--width-card-wide);
padding: 1rem 0;
}
pre code,
pre samp {
display: block;
max-width: var(--width-card-wide);
padding: 0.5rem 2rem;
white-space: pre-wrap;
}
small {
color: var(--color-text-secondary);
}
sup {
background-color: var(--color-secondary);
border-radius: var(--border-radius);
color: var(--color-bg);
font-size: xx-small;
font-weight: bold;
margin: 0.2rem;
padding: 0.2rem 0.3rem;
position: relative;
top: -2px;
}
/* Links */
a {
color: var(--color-link);
display: inline-block;
font-weight: bold;
text-decoration: underline;
}
a:active {
filter: brightness(var(--active-brightness));
}
a:hover {
filter: brightness(var(--hover-brightness));
}
a b,
a em,
a i,
a strong,
button,
input[type="submit"] {
border-radius: var(--border-radius);
display: inline-block;
font-size: medium;
font-weight: bold;
line-height: var(--line-height);
margin: 0.5rem 0;
padding: 1rem 2rem;
}
button,
input[type="submit"] {
font-family: var(--font-family);
}
button:active,
input[type="submit"]:active {
filter: brightness(var(--active-brightness));
}
button:hover,
input[type="submit"]:hover {
cursor: pointer;
filter: brightness(var(--hover-brightness));
}
a b,
a strong,
button,
input[type="submit"] {
background-color: var(--color-link);
border: 2px solid var(--color-link);
color: var(--color-bg);
}
a em,
a i {
border: 2px solid var(--color-link);
border-radius: var(--border-radius);
color: var(--color-link);
display: inline-block;
padding: 1rem 2rem;
}
article aside a {
color: var(--color-secondary);
}
/* Images */
figure {
margin: 0;
padding: 0;
}
figure img {
max-width: 100%;
}
figure figcaption {
color: var(--color-text-secondary);
}
/* Forms */
button:disabled,
input:disabled {
background: var(--color-bg-secondary);
border-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: not-allowed;
}
button[disabled]:hover,
input[type="submit"][disabled]:hover {
filter: none;
}
form {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: block;
max-width: var(--width-card-wide);
min-width: var(--width-card);
padding: 1.5rem;
text-align: var(--justify-normal);
}
form header {
margin: 1.5rem 0;
padding: 1.5rem 0;
}
input,
label,
select,
textarea {
display: block;
font-size: inherit;
max-width: var(--width-card-wide);
}
input[type="checkbox"],
input[type="radio"] {
display: inline-block;
}
input[type="checkbox"]+label,
input[type="radio"]+label {
display: inline-block;
font-weight: normal;
position: relative;
top: 1px;
}
input[type="range"] {
padding: 0.4rem 0;
}
input,
select,
textarea {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.4rem 0.8rem;
}
input[type="text"],
textarea {
width: calc(100% - 1.6rem);
}
input[readonly],
textarea[readonly] {
background-color: var(--color-bg-secondary);
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
/* Popups */
dialog {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
z-index: 999;
}
/* Tables */
table {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
border-spacing: 0;
display: inline-block;
max-width: 100%;
overflow-x: auto;
padding: 0;
/*white-space: nowrap;*/
}
table td,
table th,
table tr {
padding: 0.4rem 0.8rem;
text-align: var(--justify-table);
}
table thead {
background-color: var(--color-table);
border-collapse: collapse;
border-radius: var(--border-radius);
color: var(--color-bg);
margin: 0;
padding: 0;
}
table thead th:first-child {
border-top-left-radius: var(--border-radius);
}
table thead th:last-child {
border-top-right-radius: var(--border-radius);
}
table thead th:first-child,
table tr td:first-child {
text-align: var(--justify-normal);
}
table tr:nth-child(even) {
background-color: var(--color-accent);
}
/* Quotes */
blockquote {
display: block;
font-size: x-large;
line-height: var(--line-height);
margin: 1rem auto;
max-width: var(--width-card-medium);
padding: 1.5rem 1rem;
text-align: var(--justify-important);
}
blockquote footer {
color: var(--color-text-secondary);
display: block;
font-size: small;
line-height: var(--line-height);
padding: 1.5rem 0;
}
/* Scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: rgb(202, 202, 232) auto;
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(202, 202, 232);
border-radius: 10px;
}

1
dist/assets/css/output@0.0.1.css vendored Normal file

File diff suppressed because one or more lines are too long

788
dist/assets/css/output@dev.css vendored Normal file
View File

@@ -0,0 +1,788 @@
/*
! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #6b7280;
opacity: 1;
}
input::placeholder,textarea::placeholder {
color: #6b7280;
opacity: 1;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-date-and-time-value {
min-height: 1.5em;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
[multiple],[size]:where(select:not([size="1"])) {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
[type='checkbox'],[type='radio'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
[type='checkbox'] {
border-radius: 0px;
}
[type='radio'] {
border-radius: 100%;
}
[type='checkbox']:focus,[type='radio']:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
[type='checkbox']:checked,[type='radio']:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
@media (forced-colors: active) {
[type='checkbox']:checked {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='radio']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}
@media (forced-colors: active) {
[type='radio']:checked {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
border-color: transparent;
background-color: currentColor;
}
[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
@media (forced-colors: active) {
[type='checkbox']:indeterminate {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
}
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
[type='file'] {
background: unset;
border-color: inherit;
border-width: 0;
border-radius: 0;
padding: 0;
font-size: unset;
line-height: inherit;
}
[type='file']:focus {
outline: 1px solid ButtonText;
outline: 1px auto -webkit-focus-ring-color;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.mt-4 {
margin-top: 1rem;
}
.flex {
display: flex;
}
.min-h-screen {
min-height: 100vh;
}
.flex-grow {
flex-grow: 1;
}
.flex-col {
flex-direction: column;
}
.text-center {
text-align: center;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.font-bold {
font-weight: 700;
}
.text-indigo-200 {
--tw-text-opacity: 1;
color: rgb(199 210 254 / var(--tw-text-opacity));
}

1
dist/assets/js/htmx@v2.0.2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

8
dist/dist.go vendored Normal file
View File

@@ -0,0 +1,8 @@
package dist
import (
"embed"
)
//go:embed all:assets
var AssetsDir embed.FS

47
go.mod Normal file
View File

@@ -0,0 +1,47 @@
module wnzl-snow
go 1.24.1
require (
github.com/a-h/templ v0.2.778
github.com/go-co-op/gocron/v2 v2.12.1
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.22.1
github.com/xuri/excelize/v2 v2.9.0
gopkg.in/yaml.v2 v2.4.0
modernc.org/sqlite v1.33.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

179
go.sum Normal file
View File

@@ -0,0 +1,179 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo=
github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc=
github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo=
github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY=
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/gc/v3 v3.0.0 h1:JNEAEd0e/lnR1nlJemLPwS44KfBLBp4SAvZEZFaxfYU=
modernc.org/gc/v3 v3.0.0/go.mod h1:LG5UO1Ran4OO0JRKz2oNiXhR5nNrgz0PzH7UKhz0aMU=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/memory v1.9.0 h1:smV8d5mrOAvj5QIYbc2XLSRWvAIyPI+kQHqxZaxEqCM=
modernc.org/memory v1.9.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

303
internal/report/create.go Normal file
View File

@@ -0,0 +1,303 @@
package report
import (
"bytes"
"context"
"database/sql"
"fmt"
"log/slog"
"reflect"
"strconv"
"time"
"unicode/utf8"
"wnzl-snow/db"
"github.com/xuri/excelize/v2"
)
func CreateInventoryReport(logger *slog.Logger, Database db.Database, ctx context.Context) ([]byte, error) {
//var xlsx *excelize.File
sheetName := "Inventory Report"
var buffer bytes.Buffer
var cell string
logger.Debug("Querying inventory table")
results, err := Database.Queries().GetReportInventory(ctx)
if err != nil {
logger.Error("Unable to query inventory table", "error", err)
return nil, err
}
if len(results) == 0 {
logger.Error("Empty inventory results")
return nil, fmt.Errorf("Empty inventory results")
}
// Create excel workbook
xlsx := excelize.NewFile()
err = xlsx.SetSheetName("Sheet1", sheetName)
if err != nil {
logger.Error("Error setting sheet name", "error", err, "sheet_name", sheetName)
return nil, err
}
// Set the document properties
err = xlsx.SetDocProps(&excelize.DocProperties{
Creator: "json2excel",
Created: time.Now().Format(time.RFC3339),
})
if err != nil {
logger.Error("Error setting document properties", "error", err, "sheet_name", sheetName)
}
// Use reflection to determine column headings from the first item
firstItem := results[0]
v := reflect.ValueOf(firstItem)
typeOfItem := v.Type()
// Create column headers dynamically
for i := 0; i < v.NumField(); i++ {
column := string(rune('A'+i)) + "1" // A1, B1, C1, etc.
xlsx.SetCellValue(sheetName, column, typeOfItem.Field(i).Name)
}
// Set autofilter on heading row
cell, _ = excelize.CoordinatesToCellName(v.NumField(), 1)
filterRange := "A1:" + cell
logger.Debug("Setting autofilter", "range", filterRange)
// As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks)
err = xlsx.AutoFilter(sheetName, filterRange, nil)
if err != nil {
logger.Error("Error setting autofilter", "error", err)
}
// Bold top row
headerStyle, err := xlsx.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
},
})
if err != nil {
logger.Error("Error generating header style", "error", err)
} else {
err = xlsx.SetRowStyle(sheetName, 1, 1, headerStyle)
if err != nil {
logger.Error("Error setting header style", "error", err)
}
}
// Populate the Excel file with data from the Inventory table
for i, item := range results {
v = reflect.ValueOf(item)
for j := 0; j < v.NumField(); j++ {
column := string(rune('A'+j)) + strconv.Itoa(i+2) // Start from row 2
value := getFieldValue(v.Field(j))
xlsx.SetCellValue(sheetName, column, value)
}
}
// Freeze top row
err = xlsx.SetPanes(sheetName, &excelize.Panes{
Freeze: true,
Split: false,
XSplit: 0,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
Selection: []excelize.Selection{
{SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"},
},
})
if err != nil {
logger.Error("Error freezing top row", "error", err)
}
// Set column autowidth
/*
err = SetColAutoWidth(xlsx, sheetName)
if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err)
}
*/
// Save the Excel file into a byte buffer
if err := xlsx.Write(&buffer); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func CreateUpdatesReport(logger *slog.Logger, Database db.Database, ctx context.Context) ([]byte, error) {
//var xlsx *excelize.File
sheetName := "Updates Report"
var buffer bytes.Buffer
var cell string
logger.Debug("Querying updates table")
results, err := Database.Queries().GetReportUpdates(ctx)
if err != nil {
logger.Error("Unable to query updates table", "error", err)
return nil, err
}
if len(results) == 0 {
logger.Error("Empty updates results")
return nil, fmt.Errorf("Empty updates results")
}
// Create excel workbook
xlsx := excelize.NewFile()
err = xlsx.SetSheetName("Sheet1", sheetName)
if err != nil {
logger.Error("Error setting sheet name", "error", err, "sheet_name", sheetName)
return nil, err
}
// Set the document properties
err = xlsx.SetDocProps(&excelize.DocProperties{
Creator: "json2excel",
Created: time.Now().Format(time.RFC3339),
})
if err != nil {
logger.Error("Error setting document properties", "error", err, "sheet_name", sheetName)
}
// Use reflection to determine column headings from the first item
firstItem := results[0]
v := reflect.ValueOf(firstItem)
typeOfItem := v.Type()
// Create column headers dynamically
for i := 0; i < v.NumField(); i++ {
column := string(rune('A'+i)) + "1" // A1, B1, C1, etc.
xlsx.SetCellValue(sheetName, column, typeOfItem.Field(i).Name)
}
// Set autofilter on heading row
cell, _ = excelize.CoordinatesToCellName(v.NumField(), 1)
filterRange := "A1:" + cell
logger.Debug("Setting autofilter", "range", filterRange)
// As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks)
err = xlsx.AutoFilter(sheetName, filterRange, nil)
if err != nil {
logger.Error("Error setting autofilter", "error", err)
}
// Bold top row
headerStyle, err := xlsx.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
},
})
if err != nil {
logger.Error("Error generating header style", "error", err)
} else {
err = xlsx.SetRowStyle(sheetName, 1, 1, headerStyle)
if err != nil {
logger.Error("Error setting header style", "error", err)
}
}
// Populate the Excel file with data from the Inventory table
for i, item := range results {
v = reflect.ValueOf(item)
for j := 0; j < v.NumField(); j++ {
column := string(rune('A'+j)) + strconv.Itoa(i+2) // Start from row 2
value := getFieldValue(v.Field(j))
xlsx.SetCellValue(sheetName, column, value)
}
}
// Freeze top row
err = xlsx.SetPanes(sheetName, &excelize.Panes{
Freeze: true,
Split: false,
XSplit: 0,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
Selection: []excelize.Selection{
{SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"},
},
})
if err != nil {
logger.Error("Error freezing top row", "error", err)
}
// Set column autowidth
/*
err = SetColAutoWidth(xlsx, sheetName)
if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err)
}
*/
// Save the Excel file into a byte buffer
if err := xlsx.Write(&buffer); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// Helper function to get the actual value of sql.Null types
func getFieldValue(field reflect.Value) interface{} {
switch field.Kind() {
case reflect.Struct:
// Handle sql.Null types based on their concrete type
switch field.Interface().(type) {
case sql.NullString:
ns := field.Interface().(sql.NullString)
if ns.Valid {
return ns.String
}
return ""
case sql.NullInt64:
ni := field.Interface().(sql.NullInt64)
if ni.Valid {
return ni.Int64
}
return -1
case sql.NullFloat64:
nf := field.Interface().(sql.NullFloat64)
if nf.Valid {
return nf.Float64
}
return nil
case sql.NullBool:
nb := field.Interface().(sql.NullBool)
if nb.Valid {
return nb.Bool
}
return false
}
}
return field.Interface() // Return the value as-is for non-sql.Null types
}
// Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446
func SetColAutoWidth(xlsx *excelize.File, sheetName string) error {
// Autofit all columns according to their text content
cols, err := xlsx.GetCols(sheetName)
if err != nil {
return err
}
for idx, col := range cols {
largestWidth := 0
for _, rowCell := range col {
cellWidth := utf8.RuneCountInString(rowCell) + 2 // + 2 for margin
if cellWidth > largestWidth {
largestWidth = cellWidth
}
}
//fmt.Printf("SetColAutoWidth calculated largest width for column index '%d' is '%d'\n", idx, largestWidth)
name, err := excelize.ColumnNumberToName(idx + 1)
if err != nil {
return err
}
xlsx.SetColWidth(sheetName, name, name, float64(largestWidth))
}
// No errors at this point
return nil
}

View File

@@ -0,0 +1,80 @@
package secrets
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"log/slog"
)
type Secrets struct {
Logger *slog.Logger
EncryptionKey []byte
}
func New(logger *slog.Logger, key []byte) *Secrets {
return &Secrets{
Logger: logger,
EncryptionKey: key,
}
}
// Encrypt function that encrypts data using AES256-GCM and returns base64 encoded ciphertext
func (s *Secrets) Encrypt(plainText []byte) (string, error) {
block, err := aes.NewCipher(s.EncryptionKey)
if err != nil {
return "", err
}
// Create a new GCM cipher
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Create a nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// Encrypt the plaintext using AES256-GCM
cipherText := gcm.Seal(nonce, nonce, plainText, nil)
// Return the base64 encoded ciphertext
return base64.StdEncoding.EncodeToString(cipherText), nil
}
// Decrypt function that decrypts base64 encoded AES256-GCM ciphertext
func (s *Secrets) Decrypt(base64CipherText string) ([]byte, error) {
// Decode the base64 ciphertext
cipherText, err := base64.StdEncoding.DecodeString(base64CipherText)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(s.EncryptionKey)
if err != nil {
return nil, err
}
// Create a new GCM cipher
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Extract the nonce from the ciphertext
nonceSize := gcm.NonceSize()
nonce, cipherText := cipherText[:nonceSize], cipherText[nonceSize:]
// Decrypt the ciphertext
plainText, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return nil, err
}
return plainText, nil
}

View File

@@ -0,0 +1,67 @@
package settings
import (
"errors"
"fmt"
"log/slog"
"os"
"wnzl-snow/internal/utils"
"gopkg.in/yaml.v2"
)
type Settings struct {
SettingsPath string
Logger *slog.Logger
Values *SettingsYML
}
// SettingsYML struct holds various runtime data that is too cumbersome to specify via command line, eg replacement properties
type SettingsYML struct {
Settings struct {
TenantsToFilter []string `yaml:"tenants_to_filter"`
NodeChargeClusters []string `yaml:"node_charge_clusters"`
SrmActiveActiveVms []string `yaml:"srm_activeactive_vms"`
VcenterAddresses []string `yaml:"vcenter_addresses"`
} `yaml:"settings"`
}
func New(logger *slog.Logger, settingsPath string) *Settings {
return &Settings{
SettingsPath: utils.GetFilePath(settingsPath),
Logger: logger,
}
}
func (s *Settings) ReadYMLSettings() error {
// Create config structure
var settings SettingsYML
// Check for empty filename
if len(s.SettingsPath) == 0 {
return errors.New("settings file path not specified")
}
//path := utils.GetFilePath(settingsPath)
// Open config file
file, err := os.Open(s.SettingsPath)
if err != nil {
return fmt.Errorf("unable to open settings file : '%s'", err)
}
s.Logger.Debug("Opened settings yaml file", "file_path", s.SettingsPath)
defer file.Close()
// Init new YAML decode
d := yaml.NewDecoder(file)
// Start YAML decoding from file
if err := d.Decode(&settings); err != nil {
return fmt.Errorf("unable to decode settings file : '%s'", err)
}
s.Logger.Debug("Updating settings", "settings", settings)
s.Values = &settings
return nil
}

15
internal/tasks/tasks.go Normal file
View File

@@ -0,0 +1,15 @@
package tasks
import (
"log/slog"
"wnzl-snow/db"
"wnzl-snow/internal/settings"
)
// CronTask stores runtime information to be used by tasks
type CronTask struct {
Logger *slog.Logger
Database db.Database
Settings *settings.Settings
//VcCreds *vcenter.VcenterLogin
}

View File

@@ -0,0 +1,139 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"os"
"path/filepath"
"time"
)
func GenerateCerts(tlsCert string, tlsKey string) {
// @see https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
// @see https://golang.org/src/crypto/tls/generate_cert.go
validFrom := ""
validFor := 365 * 24 * time.Hour
isCA := true
// Get the hostname
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
// Check that the directory exists
relativePath := filepath.Dir(tlsCert)
log.Printf("GenerateCerts relative path for file creation is '%s'\n", relativePath)
_, err = os.Stat(relativePath)
if os.IsNotExist(err) {
log.Printf("Certificate path does not exist, creating %s before generating certificate\n", relativePath)
os.MkdirAll(relativePath, os.ModePerm)
}
// Generate a private key
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
}
var notBefore time.Time
if len(validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"DTMS"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
template.DNSNames = append(template.DNSNames, hostname)
// Add in all the non-local IPs
ifaces, err := net.Interfaces()
if err != nil {
log.Printf("Error enumerating interfaces: %v\n", err)
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
log.Printf("Oops: %v\n", err)
}
for _, address := range addrs {
// check the address type and if it is not a loopback then add it to the list
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
template.IPAddresses = append(template.IPAddresses, ipnet.IP)
}
}
}
}
if isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certOut, err := os.Create(tlsCert)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsCert, err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsCert, err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsCert, err)
}
log.Printf("wrote %s\n", tlsCert)
keyOut, err := os.OpenFile(tlsKey, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", tlsKey, err)
return
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", tlsKey, err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", tlsKey, err)
}
log.Printf("wrote %s\n", tlsKey)
}

68
internal/utils/utils.go Normal file
View File

@@ -0,0 +1,68 @@
package utils
import (
"context"
"log"
"log/slog"
"net"
"os"
"path/filepath"
"time"
)
const rsaBits = 4096
func GetFilePath(path string) string {
// Check for empty filename
if len(path) == 0 {
return ""
}
// check if filename exists
if _, err := os.Stat(path); os.IsNotExist((err)) {
slog.Info("File not found, searching in same directory as binary", "filename", path)
// if not, check that it exists in the same directory as the currently executing binary
ex, err2 := os.Executable()
if err2 != nil {
slog.Error("Error determining binary path", "error", err)
return ""
}
binaryPath := filepath.Dir(ex)
path = filepath.Join(binaryPath, path)
}
return path
}
// Get preferred outbound ip of this machine
// @see https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go
func GetOutboundIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP
}
// Check if a file exists from https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func SleepWithContext(ctx context.Context, d time.Duration) {
timer := time.NewTimer(d)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
}
}

52
key.pem Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDJhxPJHo/SqsEU
RebiRufmXRDYhl67PdglOGI6JC/VjCaqu42DYMO9tF6vuHBD4/+/UpMBuOGTw+m6
nEKdKKIZBuG0Hvka4bS0YPDHvqHZMWHK9qdPbmrB8q1N8W9FZ+1gjIdeCK1wBjMT
nfSxWOznKjffsz170bhOE6CHB4CP9nLMcShrvDcg0kTSbzM1ptlxWGI5vabkx9sp
PU65NWEjd645Z4kMIutJZJjVKPlAqRr/fFeNgE5A9VhwzBot5ZupKIZYuUMAMYkX
jOhdqxDbWqfc8lkUvk5Jd9XtYB81uzvfpoH28Th4ILAdZd9hvPh38sB/FyxSeKPH
mW0IP1r8+EW/57ZPnQrGAOWs2z8Fp812vwQV0sr8j1B5NiTzyu7I47vDdKZxulWS
48frASxFFz4OojZAsoP6LipAMPolGIrnFn1/6UlOPnSVqkUbD36PEIkOzE9AzMaS
D/qs+k9lVHo+VTHL/qmz6u6COQiPL0u/ZInMcQmzJecCSDm4Z8oVGeth+2vzlPo1
iQHqptYYbln7oLaQZuCqdm4oNb+Hdyflhv8O78hUq7tcnQ8M7/0jvlB6m1e4vuAx
ldBkaWr/cQudtzZnTiwHf2a3TH0h3ID0Gecog+hWb7EcAI2DIK6Ji3uEBLag39XF
uMzF/jLIUtYMz0Mb48v3Opbd8qe3aQIDAQABAoICAQCrZ50XeUwAdUVFdfLbUE2b
LFrQnvDhtscpWRyKsQ6SReL5Yg4JyPqTVl8We/vYcoqqcpQgadxK7t3T32X0/4Nn
X+gGaDWdfI1SwgTpDyXfclXn6AQD1jks/rgSTCBE2xEWBlB4VU8Wsd8tdzKQyL6u
GsVtGalYr9ZfaegmEOZzC702T6R+hZYp702j4fqfTmsxMWhSDhFuEuI/4Cod8t3M
6dUgdAQnc2fFg69N1cyyB2K0HFDnRFLKgyKWxbIaiWjs5k/mFKR8/KTKiAl26bGr
sB6IGQRVVUuGx0sH27KP49EX9yohG6fY3IyOZIArRCvc8XZyYgZLmtpKQR9wXVqH
u/W2fsW9K/4qZFm9xSq8puBfwqdqWuU9jcetI+5SreHgi74mSPwX4uvSjPH0LE0N
RbGP745bJpoLrLxRSVlfK7MOcZ6sMgPfcJbOoKQ9Ixd1uwfFl4EppEw5bO5p7ha3
O1Ks54H6S3FPCUpSP60WpzlfzcB//6bW6h4AZZWHTFGW8brU38v2UXhLqWHqYYXY
5XYln78cZk0yQpo3hIVm/VoP+lvUtb9elk2OH/eq7QUy1YEocrhzeKl16snGFWli
hDiIpPEJPA0jOLng2D3n2Udpqu+YAVuNfVjirXV3G8jz1sECKI3drNkvplaOMVA3
frfTLGVXlAhCkRDnv+9OUQKCAQEA/ZOXTf2ydxgG6YMt+W/cMobYcWLfmL1M3jR6
H0vBIA5S4r/vvireXemfjjalpQlOnc7iDy/g85QOl5JXsDkgUpsEtucLOZmO8p/O
2x03PMN2/p5ChQKWBAX9DifkJfq+KIEaukNUZwidozY7VsOdpm8oMrFi1rDGvmO8
X+zpYPUDZa1QYJ5HPewRJ8bu3fvmsSthq/c7OiNTVO7c7BS0eaD2uhlrP25lBlik
AC+moyROAYz72C+UKgV67uqKS7nNCV+5DxwYfGXKGH+HqTfrwfFAQnQ2EW86qWcV
9ddWvH1njQ5oyKdNsiT4EJruxBUhR32em5uZboz3Ny1/QCF2XQKCAQEAy3QkRZ59
sujUQILxOedK7A0luidZkg4y2DgvxfqD0tggvPMEpHt4eIs/VbZNFDplhF/xIqxO
+muox5oP5CZtT6uQJKPomPYwAeSDwj3prWPCuN9IAiO8kJyyOsTErzzAVIpp0loP
f10H2OR5aZTi9xlyrFggadcfcWDTYJie1PhfLlI8q//GK/3WaC0sqY+45AIdzF2C
GAFskgLIG8lw8B7vVMkrTG7NvWeWYGprkvHBYk50rpYyN3HBKdYXLxGVZa2R9ErC
fp4nXfE7cxb9hEF+gondYE+kWOUGrbt8CUskjtM02Ghy/3BBdETezQm9VTU24c5W
Delps/Oft23cfQKCAQEA8f/16T+SH2h1yEsiCCiCHFJmpCd5PtqCTpoEZnO0lvQ0
lCQbGADvgO7iECYFBbERLzfutBG8fJmzJ2JJf2u1dkBtSmTLaKgtLU3oa2LBgaMF
oE/HKgtiweo5LFZTvQ9EhYFY0aRKG560Wrv5+37qqJjy4xY5Pq0sryyd7Wo6/AY4
vMOfruW/FdCCOtT2yDGMY08EgsqK4pwt/iOMAV5tMpq0fLyfhsTDc0ALHAZd4NpX
+9Dh1NrrawMZ5IM/gq+Kh0SsZ0gn8ihuq9yhypQM0mV8Ly+bHh5p/Jvd8GYXaAHC
XnL++2f3lWkEuUwzTDziKskvIlBoKIItuKIaDEkjHQKCAQA57NhC5gv9uyJrN5y8
kqiXKmsaZAexeM9rFhwr7tZHWnd/tGUW+3xiPasLmTq6zqGGndZGu+5EiBBmu+tY
VsR6kWsZs30Ox9EFSO1miUAVuLiA8DlNOqV+Os7XwpwJFG/h4gKc/D07Ut5gFzSm
Jdi0547pmLGp0LDIE/w6nVljY0+eHZXelpTyg3Pe9zWnFR9b82pOfDUE5qrlq1sT
5jd8diJH7nqgaU+0ZGDBFKuKC8C2X4YDCwpDFOUdJzI0mXoCl/ddKUfUyuyEZ6E4
dEw7snX1bpET0bkGAjbzkdSPxNAkxede0eYj6e8Izm7sy6AXOW5lvdxsW49Xx74q
eGH1AoIBAFXy/VngFsE6ghxVJljWQSfK8Ut4Hc/bVFpJeLmasi65V/xb0Uc62ZqX
OmHha3Ol41djrdVRtKrTPxVtDXsnAjZOPXQYckM5DthUMUJEe9BUqCqSz1M/f1pt
B8tM3N/GxBWIoCAhutMmnXOU2Q4mBJ/NPYIrx/PWjybV3Lb3/WNj8r7avWIX2i76
oT2rRosbl3y4W7Agagtea6td1aMWFb3hQj9vqXTHfs3UAq9XTj4ZAWg3PKXKEjhn
Qt8qrQ9+4lQ/QjwMJcLtwDSHCLCvm+zCa4fCQB6zZI43hZbc/wYjm/93l044u+pi
cJBMGs/FdD+HKvEm/wPj8KFkiZMObOY=
-----END PRIVATE KEY-----

100
log/log.go Normal file
View File

@@ -0,0 +1,100 @@
package log
import (
"log/slog"
"os"
)
// New creates a new logger with the given level and output.
func New(level Level, output Output) *slog.Logger {
var h slog.Handler
switch output {
case OutputJson:
h = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog(), AddSource: true})
case OutputText:
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog(), AddSource: true})
default:
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog(), AddSource: true})
}
return slog.New(h)
}
// Level represents the log level.
type Level string
// ToSlog converts the level to slog.Level.
func (l Level) ToSlog() slog.Level {
switch l {
case LevelDebug:
return slog.LevelDebug
case LevelInfo:
return slog.LevelInfo
case LevelWarn:
return slog.LevelWarn
case LevelError:
return slog.LevelError
default:
return slog.LevelInfo
}
}
const (
// LogDebug is the debug log level.
LevelDebug Level = "debug"
// LogInfo is the info log level.
LevelInfo Level = "info"
// LogWarn is the warn log level.
LevelWarn Level = "warn"
// LogError is the error log level.
LevelError Level = "error"
)
// ToLevel converts the level to Level.
func ToLevel(level string) Level {
switch level {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}
// GetLevel returns the log level from the environment variable.
func GetLevel() Level {
level := os.Getenv("LOG_LEVEL")
return ToLevel(level)
}
// Output represents the log output.
type Output string
const (
// OutputJson is the JSON log output.
OutputJson Output = "json"
// OutputText is the text log output.
OutputText Output = "text"
)
// ToOutput converts the output to Output.
func ToOutput(output string) Output {
switch output {
case "json":
return OutputJson
case "text":
return OutputText
default:
return OutputText
}
}
// GetOutput returns the log output from the environment variable.
func GetOutput() Output {
output := os.Getenv("LOG_OUTPUT")
return ToOutput(output)
}

232
main.go Normal file
View File

@@ -0,0 +1,232 @@
package main
import (
"fmt"
"os"
"runtime"
"time"
"wnzl-snow/db"
"wnzl-snow/internal/settings"
utils "wnzl-snow/internal/utils"
"wnzl-snow/log"
"wnzl-snow/server"
"wnzl-snow/server/router"
"github.com/go-co-op/gocron/v2"
"github.com/joho/godotenv"
)
var (
bindDisableTls bool
sha1ver string // sha1 revision used to build the program
buildTime string // when the executable was built
cronFrequency time.Duration
cronInvFrequency time.Duration
encryptionKey = []byte("5L1l3B5KvwOCzUHMAlCgsgUTRAYMfSpa")
)
func main() {
// Load data from environment file
envFilename := utils.GetFilePath(".env")
err := godotenv.Load(envFilename)
if err != nil {
panic("Error loading .env file")
}
logger := log.New(
log.GetLevel(),
log.GetOutput(),
)
//ctx, cancel := context.WithCancel(context.Background())
// Configure database
database, err := db.New(logger, utils.GetFilePath("db.sqlite3"))
if err != nil {
logger.Error("Failed to create database", "error", err)
os.Exit(1)
}
defer database.Close()
//defer database.DB().Close()
if err = db.Migrate(database); err != nil {
logger.Error("failed to migrate database", "error", err)
os.Exit(1)
}
// Load settings from yaml
settingsFile := os.Getenv("SETTINGS_FILE")
if settingsFile == "" {
settingsFile = "settings.yaml"
}
// TODO - how to pass this to the other packages that will need this info?
s := settings.New(logger, settingsFile)
err = s.ReadYMLSettings()
//s, err := settings.ReadYMLSettings(logger, settingsFile)
if err != nil {
logger.Error("failed to open yaml settings file", "error", err, "filename", settingsFile)
//os.Exit(1)
} else {
logger.Debug("Loaded yaml settings", "contents", s)
}
// Determine bind IP
bindIP := os.Getenv("BIND_IP")
if bindIP == "" {
bindIP = utils.GetOutboundIP().String()
}
// Determine bind port
bindPort := os.Getenv("BIND_PORT")
if bindPort == "" {
bindPort = "9443"
}
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
//logger.Info("Will listen on address", "ip", bindIP, "port", bindPort)
// Determine bind disable TLS
bindDisableTlsEnv := os.Getenv("BIND_DISABLE_TLS")
if bindDisableTlsEnv == "true" {
bindDisableTls = true
}
// Get file names for TLS cert/key
tlsCertFilename := os.Getenv("TLS_CERT_FILE")
if tlsCertFilename != "" {
tlsCertFilename = utils.GetFilePath(tlsCertFilename)
} else {
tlsCertFilename = "./cert.pem"
}
tlsKeyFilename := os.Getenv("TLS_KEY_FILE")
if tlsKeyFilename != "" {
tlsKeyFilename = utils.GetFilePath(tlsKeyFilename)
} else {
tlsKeyFilename = "./privkey.pem"
}
// Generate certificate if required
if !(utils.FileExists(tlsCertFilename) && utils.FileExists(tlsKeyFilename)) {
logger.Warn("Specified TLS certificate or private key do not exist", "certificate", tlsCertFilename, "tls-key", tlsKeyFilename)
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)
}
// Prepare the task scheduler
c, err := gocron.NewScheduler()
if err != nil {
logger.Error("failed to create scheduler", "error", err)
os.Exit(1)
}
/*
// Load vcenter credentials from .env
a := secrets.New(logger, encryptionKey)
vcEp := os.Getenv("VCENTER_PASSWORD")
if len(vcEp) == 0 {
logger.Error("No vcenter password configured")
os.Exit(1)
}
vcPass, err := a.Decrypt(vcEp)
if err != nil {
logger.Error("failed to decrypt vcenter credentials. Assuming un-encrypted", "error", err)
vcPass = []byte(vcEp)
//os.Exit(1)
}
creds := vcenter.VcenterLogin{
//insecureString := os.Getenv("VCENTER_INSECURE")
Username: os.Getenv("VCENTER_USERNAME"),
Password: string(vcPass),
}
// Prepare the task scheduler
c, err := gocron.NewScheduler()
if err != nil {
logger.Error("failed to create scheduler", "error", err)
os.Exit(1)
}
// Pass useful information to the cron jobs
ct := &tasks.CronTask{
Logger: logger,
Database: database,
Settings: s,
VcCreds: &creds,
}
cronFrequencyString := os.Getenv("VCENTER_EVENT_POLLING_SECONDS")
if cronFrequencyString != "" {
cronFrequency, err = time.ParseDuration(cronFrequencyString)
if err != nil {
slog.Error("Can't convert VCENTER_EVENT_POLLING_SECONDS value to time duration. Defaulting to 60s", "value", cronFrequencyString, "error", err)
cronFrequency = time.Second * 60
}
} else {
cronFrequency = time.Second * 60
}
logger.Debug("Setting VM event polling cronjob frequency to", "frequency", cronFrequency)
cronInventoryFrequencyString := os.Getenv("VCENTER_INVENTORY_POLLING_SECONDS")
if cronInventoryFrequencyString != "" {
cronInvFrequency, err = time.ParseDuration(cronInventoryFrequencyString)
if err != nil {
slog.Error("Can't convert VCENTER_INVENTORY_POLLING_SECONDS value to time duration. Defaulting to 7200", "value", cronInventoryFrequencyString, "error", err)
cronInvFrequency = time.Second * 7200
}
} else {
cronInvFrequency = time.Second * 7200
}
logger.Debug("Setting VM inventory polling cronjob frequency to", "frequency", cronInvFrequency)
// start background processing for events stored in events table
startsAt := time.Now().Add(time.Second * 10)
job, err := c.NewJob(
gocron.DurationJob(cronFrequency),
gocron.NewTask(func() {
ct.RunVmCheck(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithStartAt(gocron.WithStartDateTime(startsAt)),
)
if err != nil {
logger.Error("failed to start event processing cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created event processing cron job", "job", job.ID(), "starting_at", startsAt)
// start background checks of vcenter inventory
startsAt2 := time.Now().Add(cronInvFrequency)
job2, err := c.NewJob(
gocron.DurationJob(cronInvFrequency),
gocron.NewTask(func() {
ct.RunVcenterPoll(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithStartAt(gocron.WithStartDateTime(startsAt2)),
)
if err != nil {
logger.Error("failed to start vcenter inventory cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created vcenter inventory cron job", "job", job2.ID(), "starting_at", startsAt2)
// start cron scheduler
c.Start()
*/
// Start server
r := router.New(logger, database, buildTime, sha1ver, runtime.Version(), s)
svr := server.New(
logger,
c,
//cancel,
bindAddress,
server.WithRouter(r),
server.SetTls(bindDisableTls),
server.SetCertificate(tlsCertFilename),
server.SetPrivateKey(tlsKeyFilename),
)
//logger.Debug("Server configured", "object", svr)
svr.StartAndWait()
os.Exit(0)
}

View File

@@ -0,0 +1,61 @@
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
//ctx := context.Background()
var cipherText string
reqBody, err := io.ReadAll(r.Body)
if err != nil {
h.Logger.Error("Invalid data received", "error", err)
fmt.Fprintf(w, "Invalid data received")
w.WriteHeader(http.StatusInternalServerError)
return
} else {
h.Logger.Debug("received input data", "length", len(reqBody))
}
// get the json input
var input map[string]string
if err := json.Unmarshal(reqBody, &input); err != nil {
h.Logger.Error("unable to unmarshal json", "error", err)
prettyPrint(reqBody)
w.Header().Set("Content-Type", "application/json")
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
} else {
h.Logger.Debug("successfully decoded JSON")
//prettyPrint(input)
}
//cipher, err := h.Secret.Encrypt()
for k := range input {
//h.Logger.Debug("foo", "key", k, "value", input[k])
cipherText, err = h.Secret.Encrypt([]byte(input[k]))
if err != nil {
h.Logger.Error("Unable to encrypt", "error", err)
} else {
h.Logger.Debug("Encrypted plaintext", "length", len(input[k]), "ciphertext", cipherText)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "OK",
"message": cipherText,
})
return
}
}
// return the result
}

33
server/handler/handler.go Normal file
View File

@@ -0,0 +1,33 @@
package handler
import (
"context"
"log/slog"
"net/http"
"wnzl-snow/db"
"wnzl-snow/internal/secrets"
"wnzl-snow/internal/settings"
"github.com/a-h/templ"
)
// Handler handles requests.
type Handler struct {
Logger *slog.Logger
Database db.Database
BuildTime string
SHA1Ver string
GoVersion string
//VcCreds *vcenter.VcenterLogin
Secret *secrets.Secrets
Settings *settings.Settings
}
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
if err := t.Render(ctx, w); err != nil {
h.Logger.Error("Failed to render component", "error", err)
}
}

62
server/handler/home.go Normal file
View File

@@ -0,0 +1,62 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"
"wnzl-snow/components/views"
)
// Home handles the home page.
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
//h.html(r.Context(), w, http.StatusOK, core.HTML("Example Site", home.Home()))
// Render the template
/*
err := home.Home(h.BuildTime, h.SHA1Ver, h.GoVersion).Render(r.Context(), w)
if err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
*/
info := views.BuildInfo{
BuildTime: h.BuildTime,
SHA1Ver: h.SHA1Ver,
GoVersion: h.GoVersion,
}
err := views.Index(info).Render(r.Context(), w)
if err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}
// prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c
func prettyPrint(args ...interface{}) {
var caller string
timeNow := time.Now().Format("01-02-2006 15:04:05")
prefix := fmt.Sprintf("[%s] %s -- ", "PrettyPrint", timeNow)
_, fileName, fileLine, ok := runtime.Caller(1)
if ok {
caller = fmt.Sprintf("%s:%d", fileName, fileLine)
} else {
caller = ""
}
fmt.Printf("\n%s%s\n", prefix, caller)
if len(args) == 2 {
label := args[0]
value := args[1]
s, _ := json.MarshalIndent(value, "", "\t")
fmt.Printf("%s%s: %s\n", prefix, label, string(s))
} else {
s, _ := json.MarshalIndent(args, "", "\t")
fmt.Printf("%s%s\n", prefix, string(s))
}
}

22
server/handler/newSnow.go Normal file
View File

@@ -0,0 +1,22 @@
package handler
import (
"fmt"
"io"
"net/http"
)
// NewSnow receives data from the DMSP Snow New() function
func (h *Handler) NewSnow(w http.ResponseWriter, r *http.Request) {
var ()
reqBody, err := io.ReadAll(r.Body)
if err != nil {
h.Logger.Error("Invalid data received", "error", err)
fmt.Fprintf(w, "Invalid data received")
w.WriteHeader(http.StatusInternalServerError)
return
} else {
h.Logger.Debug("received input data", "length", len(reqBody))
}
}

View File

@@ -0,0 +1,61 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"wnzl-snow/internal/report"
)
func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Generate the XLSX report
reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx)
if err != nil {
h.Logger.Error("Failed to create report", "error", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
})
return
}
// Set HTTP headers to indicate file download
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", `attachment; filename="inventory_report.xlsx"`)
w.Header().Set("File-Name", "inventory_report.xlsx")
// Write the XLSX file to the HTTP response
w.Write(reportData)
}
func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Generate the XLSX report
reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx)
if err != nil {
h.Logger.Error("Failed to create report", "error", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"status": "ERROR",
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
})
return
}
// Set HTTP headers to indicate file download
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", `attachment; filename="updates_report.xlsx"`)
w.Header().Set("File-Name", "updates_report.xlsx")
// Write the XLSX file to the HTTP response
w.Write(reportData)
}

View File

@@ -0,0 +1,42 @@
package handler
import (
"context"
"fmt"
"net/http"
)
// VmUpdate receives the CloudEvent for a VM modification or move
func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
/*
// Get the current time
now := time.Now()
// Get the start of the current day (midnight today)
midnightToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// Convert to Unix time
unixTime := midnightToday.Unix()
// create the database parameters
params := queries.CleanupUpdatesParams{
UpdateType: "diskchange",
UpdateTime: sql.NullInt64{Int64: unixTime, Valid: unixTime > 0},
}
h.Logger.Debug("database params", "params", params)
err := h.Database.Queries().CleanupUpdates(context.Background(), params)
*/
//err := h.Database.Queries().InventoryCleanupTemplates(context.Background())
err := h.Database.Queries().CleanupUpdatesNullVm(context.Background())
if err != nil {
h.Logger.Error("Error received cleaning updates table", "error", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Delete Request unsuccessful %s\n", err)
} else {
h.Logger.Debug("Processed update cleanup successfully")
w.WriteHeader(http.StatusOK)
// TODO - return some JSON
fmt.Fprintf(w, "Processed update cleanup successfully")
}
}

View File

@@ -0,0 +1,18 @@
package middleware
import (
"net/http"
"wnzl-snow/version"
)
// CacheMiddleware sets the Cache-Control header based on the version.
func CacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if version.Value == "dev" {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,35 @@
package middleware
import (
"log/slog"
"net/http"
"time"
)
// LoggingMiddleware represents a logging middleware.
type LoggingMiddleware struct {
logger *slog.Logger
handler http.Handler
}
// NewLoggingMiddleware creates a new logging middleware with the given logger and handler.
func NewLoggingMiddleware(logger *slog.Logger, handler http.Handler) *LoggingMiddleware {
return &LoggingMiddleware{
logger: logger,
handler: handler,
}
}
// ServeHTTP logs the request and calls the next handler.
func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
l.handler.ServeHTTP(w, r)
l.logger.Debug(
"Request recieved",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote", r.RemoteAddr),
slog.Duration("duration", time.Since(start)),
)
}

View File

@@ -0,0 +1,22 @@
package middleware
import "net/http"
type Handler func(http.Handler) http.Handler
func Chain(handlers ...Handler) Handler {
if len(handlers) == 0 {
return defaultHandler
}
return func(next http.Handler) http.Handler {
for i := len(handlers) - 1; i >= 0; i-- {
next = handlers[i](next)
}
return next
}
}
func defaultHandler(next http.Handler) http.Handler {
return next
}

292
server/models/models.go Normal file
View File

@@ -0,0 +1,292 @@
package models
import (
"encoding/json"
)
type IncidentResponse struct {
ImportSet string `json:"import_set"`
StagingTable string `json:"staging_table"`
Result []struct {
TransformMap string `json:"transform_map"`
Table string `json:"table"`
DisplayName string `json:"display_name"`
DisplayValue string `json:"display_value"`
RecordLink string `json:"record_link"`
Status string `json:"status"`
SysID string `json:"sys_id"`
}
}
type Incident struct {
IncidentNumber string `json:"incident_number,omitempty"` // The incident number in ServiceNow. If blank, creates a new incident, if populated with a valid value it updates that record.
Description string `json:"description,omitempty"`
ShortDescription string `json:"short_description,omitempty"`
// 1 = Critical , 2 = High, 3 = Medium, 4 = Low
Urgency string `json:"urgency,omitempty"` // integer value, 1-4
// 1 = Critical , 2 = High, 3 = Medium, 4 = Low
Impact string `json:"impact,omitempty"` // integer value, 1-4
// State: 1 = New ; 2 = In Progress ; 3 = On Hold ; 6 = Resolved ; 7 = Closed ; 8 = Cancelled
State string `json:"state,omitempty"` // integer value, 1-6 (6 is resolved)
// Associated DeviceID (UUID from CPDB)
ExternalID string `json:"external_id,omitempty"` // CPDB UUID for the configuration item
WorkNotes string `json:"work_notes,omitempty"` // Additional notes
AssignmentGroup string `json:"assignment_group,omitempty"`
AssignedTo string `json:"assigned_to,omitempty"`
// Cat/SubCategory to populate the Incident appropriately
Category string `json:"category,omitempty"`
SubCategory string `json:"subcategory,omitempty"`
}
type CloudEventReceived struct {
CloudEvent struct {
ID string `json:"id"`
Specversion string `json:"specversion"`
Source string `json:"source"`
Type string `json:"type"`
Time string `json:"time"` // Modified from time.Time
Data struct {
ChainID int `json:"ChainId"`
ChangeTag string `json:"ChangeTag"`
ComputeResource struct {
ComputeResource struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"ComputeResource"`
Name string `json:"Name"`
} `json:"ComputeResource"`
CreatedTime string `json:"CreatedTime"` // Modified from time.Time
Datacenter struct {
Datacenter struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Datacenter"`
Name string `json:"Name"`
} `json:"Datacenter"`
Ds interface{} `json:"Ds"`
Dvs interface{} `json:"Dvs"`
FullFormattedMessage string `json:"FullFormattedMessage"`
Host struct {
Host struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Host"`
Name string `json:"Name"`
} `json:"Host"`
Key int `json:"Key"`
Net interface{} `json:"Net"`
NewParent *CloudEventResourcePool `json:"NewParent"`
OldParent *CloudEventResourcePool `json:"OldParent"`
SrcTemplate *CloudEventVm `json:"SrcTemplate"`
Template bool `json:"Template"`
UserName string `json:"UserName"`
VM struct {
Name string `json:"Name"`
VM struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Vm"`
} `json:"Vm"`
ConfigSpec *json.RawMessage `json:"configSpec"`
ConfigChanges *ConfigChangesReceived `json:"configChanges"` // Modified to separate struct
} `json:"data"`
} `json:"cloudEvent"`
}
type CloudEventResourcePool struct {
Name string `json:"Name"`
ResourcePool struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"ResourcePool"`
}
type CloudEventVm struct {
Name string `json:"Name"`
VM struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Vm"`
}
type ImportReceived struct {
Name string `json:"Name"`
Vcenter string `json:"Vcenter"`
VmId string `json:"VmId"`
InitialRam int `json:"InitialRam"`
PowerState int `json:"PowerState"`
CreationTime int `json:"CreationTime"`
InitialVcpus int `json:"InitialVcpus"`
ProvisionedDisk float64 `json:"ProvisionedDisk"`
Folder string `json:"Folder"`
ResourcePool string `json:"ResourcePool"`
Datacenter string `json:"Datacenter"`
Cluster string `json:"Cluster"`
}
type ConfigChangesReceived struct {
Modified string `json:"modified"`
}
// This probably needs more fields added so not in use yet
type ConfigSpec struct {
AlternateGuestName string `json:"AlternateGuestName"`
Annotation string `json:"Annotation"`
BootOptions any `json:"BootOptions"`
ChangeTrackingEnabled any `json:"ChangeTrackingEnabled"`
ChangeVersion string `json:"ChangeVersion"`
ConsolePreferences any `json:"ConsolePreferences"`
CPUAffinity any `json:"CpuAffinity"`
CPUAllocation any `json:"CpuAllocation"`
CPUFeatureMask any `json:"CpuFeatureMask"`
CPUHotAddEnabled any `json:"CpuHotAddEnabled"`
CPUHotRemoveEnabled any `json:"CpuHotRemoveEnabled"`
CreateDate string `json:"CreateDate"` // Modified from time.Time
Crypto any `json:"Crypto"`
DeviceChange []struct {
Backing any `json:"Backing"`
Device struct {
Backing *BackingSpec `json:"Backing,omitempty"`
CapacityInBytes int `json:"CapacityInBytes"`
CapacityInKB int `json:"CapacityInKB"`
Connectable struct {
AllowGuestControl bool `json:"AllowGuestControl"`
Connected bool `json:"Connected"`
MigrateConnect string `json:"MigrateConnect"`
StartConnected bool `json:"StartConnected"`
Status string `json:"Status"`
} `json:"Connectable"`
ControllerKey int `json:"ControllerKey"`
DeviceInfo struct {
Label string `json:"Label"`
Summary string `json:"Summary"`
} `json:"DeviceInfo"`
ExternalID string `json:"ExternalId"`
MacAddress string `json:"MacAddress"`
ResourceAllocation struct {
Limit int `json:"Limit"`
Reservation int `json:"Reservation"`
Share struct {
Level string `json:"Level"`
Shares int `json:"Shares"`
} `json:"Share"`
} `json:"ResourceAllocation"`
SlotInfo any `json:"SlotInfo"`
UnitNumber int `json:"UnitNumber"`
UptCompatibilityEnabled bool `json:"UptCompatibilityEnabled"`
WakeOnLanEnabled bool `json:"WakeOnLanEnabled"`
DiskObjectID string `json:"DiskObjectId"`
Iofilter any `json:"Iofilter"`
Key int `json:"Key"`
NativeUnmanagedLinkedClone any `json:"NativeUnmanagedLinkedClone"`
Shares any `json:"Shares"`
StorageIOAllocation struct {
Limit int `json:"Limit"`
Reservation any `json:"Reservation"`
Shares struct {
Level string `json:"Level"`
Shares int `json:"Shares"`
} `json:"Shares"`
} `json:"StorageIOAllocation"`
VDiskID any `json:"VDiskId"`
VFlashCacheConfigInfo any `json:"VFlashCacheConfigInfo"`
} `json:"Device,omitempty"`
FileOperation string `json:"FileOperation"`
Operation string `json:"Operation"`
Profile []struct {
ProfileData struct {
ExtensionKey string `json:"ExtensionKey"`
ObjectData string `json:"ObjectData"` // Modified from time.Time
} `json:"ProfileData"`
ProfileID string `json:"ProfileId"`
ProfileParams any `json:"ProfileParams"`
ReplicationSpec any `json:"ReplicationSpec"`
} `json:"Profile"`
} `json:"DeviceChange"`
ExtraConfig any `json:"ExtraConfig"`
Files struct {
FtMetadataDirectory string `json:"FtMetadataDirectory"`
LogDirectory string `json:"LogDirectory"`
SnapshotDirectory string `json:"SnapshotDirectory"`
SuspendDirectory string `json:"SuspendDirectory"`
VMPathName string `json:"VmPathName"`
} `json:"Files"`
Firmware string `json:"Firmware"`
Flags any `json:"Flags"`
FtInfo any `json:"FtInfo"`
GuestAutoLockEnabled any `json:"GuestAutoLockEnabled"`
GuestID string `json:"GuestId"`
GuestMonitoringModeInfo any `json:"GuestMonitoringModeInfo"`
InstanceUUID string `json:"InstanceUuid"`
LatencySensitivity any `json:"LatencySensitivity"`
LocationID string `json:"LocationId"`
ManagedBy any `json:"ManagedBy"`
MaxMksConnections int `json:"MaxMksConnections"`
MemoryAffinity any `json:"MemoryAffinity"`
MemoryAllocation any `json:"MemoryAllocation"`
MemoryHotAddEnabled any `json:"MemoryHotAddEnabled"`
MemoryMB int `json:"MemoryMB"`
MemoryReservationLockedToMax any `json:"MemoryReservationLockedToMax"`
MessageBusTunnelEnabled any `json:"MessageBusTunnelEnabled"`
MigrateEncryption string `json:"MigrateEncryption"`
Name string `json:"Name"`
NestedHVEnabled any `json:"NestedHVEnabled"`
NetworkShaper any `json:"NetworkShaper"`
NpivDesiredNodeWwns int `json:"NpivDesiredNodeWwns"`
NpivDesiredPortWwns int `json:"NpivDesiredPortWwns"`
NpivNodeWorldWideName any `json:"NpivNodeWorldWideName"`
NpivOnNonRdmDisks any `json:"NpivOnNonRdmDisks"`
NpivPortWorldWideName any `json:"NpivPortWorldWideName"`
NpivTemporaryDisabled any `json:"NpivTemporaryDisabled"`
NpivWorldWideNameOp string `json:"NpivWorldWideNameOp"`
NpivWorldWideNameType string `json:"NpivWorldWideNameType"`
NumCPUs int `json:"NumCPUs"`
NumCoresPerSocket int `json:"NumCoresPerSocket"`
PowerOpInfo any `json:"PowerOpInfo"`
RepConfig any `json:"RepConfig"`
ScheduledHardwareUpgradeInfo any `json:"ScheduledHardwareUpgradeInfo"`
SevEnabled any `json:"SevEnabled"`
SgxInfo any `json:"SgxInfo"`
SwapPlacement string `json:"SwapPlacement"`
Tools any `json:"Tools"`
UUID string `json:"Uuid"`
VAppConfig any `json:"VAppConfig"`
VAppConfigRemoved any `json:"VAppConfigRemoved"`
VAssertsEnabled any `json:"VAssertsEnabled"`
VPMCEnabled any `json:"VPMCEnabled"`
VcpuConfig any `json:"VcpuConfig"`
Version string `json:"Version"`
VirtualICH7MPresent any `json:"VirtualICH7MPresent"`
VirtualSMCPresent any `json:"VirtualSMCPresent"`
VMProfile any `json:"VmProfile"`
}
type BackingSpec struct {
Port struct {
ConnectionCookie int `json:"ConnectionCookie"`
PortKey string `json:"PortKey"`
PortgroupKey string `json:"PortgroupKey"`
SwitchUUID string `json:"SwitchUuid"`
} `json:"Port"`
BackingObjectID string `json:"BackingObjectId"`
ChangeID string `json:"ChangeId"`
ContentID string `json:"ContentId"`
Datastore struct {
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Datastore"`
DeltaDiskFormat string `json:"DeltaDiskFormat"`
DeltaDiskFormatVariant string `json:"DeltaDiskFormatVariant"`
DeltaGrainSize int `json:"DeltaGrainSize"`
DigestEnabled any `json:"DigestEnabled"`
DiskMode string `json:"DiskMode"`
EagerlyScrub bool `json:"EagerlyScrub"`
FileName string `json:"FileName"`
KeyID any `json:"KeyId"`
Parent any `json:"Parent"`
Sharing string `json:"Sharing"`
Split any `json:"Split"`
ThinProvisioned bool `json:"ThinProvisioned"`
UUID string `json:"Uuid"`
WriteThrough any `json:"WriteThrough"`
}

62
server/router/router.go Normal file
View File

@@ -0,0 +1,62 @@
package router
import (
"log/slog"
"net/http"
"net/http/pprof"
"wnzl-snow/db"
"wnzl-snow/dist"
"wnzl-snow/internal/settings"
"wnzl-snow/server/handler"
"wnzl-snow/server/middleware"
)
func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver string, goVersion string, settings *settings.Settings) http.Handler {
h := &handler.Handler{
Logger: logger,
Database: database,
BuildTime: buildTime,
SHA1Ver: sha1ver,
GoVersion: goVersion,
//VcCreds: creds,
//Secret: secret,
Settings: settings,
}
mux := http.NewServeMux()
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
mux.HandleFunc("/", h.Home)
mux.HandleFunc("/api/now/import/x_dusa2_itom_inc_imp", h.NewSnow)
// mux.HandleFunc("/api/event/vm/create", h.VmCreateEvent)
// mux.HandleFunc("/api/event/vm/modify", h.VmModifyEvent)
// mux.HandleFunc("/api/event/vm/move", h.VmMoveEvent)
// mux.HandleFunc("/api/event/vm/delete", h.VmDeleteEvent)
// mux.HandleFunc("/api/import/vm", h.VmImport)
// // Use this when we need to manually remove a VM from the database to clean up
// mux.HandleFunc("/api/inventory/vm/delete", h.VmCleanup)
// // add missing data to VMs
// //mux.HandleFunc("/api/inventory/vm/update", h.VmUpdateDetails)
// // temporary endpoint
// mux.HandleFunc("/api/cleanup/updates", h.UpdateCleanup)
// //mux.HandleFunc("/api/cleanup/vcenter", h.VcCleanup)
// mux.HandleFunc("/api/report/inventory", h.InventoryReportDownload)
// mux.HandleFunc("/api/report/updates", h.UpdateReportDownload)
// // endpoint for encrypting vcenter credential
// mux.HandleFunc("/api/encrypt", h.EncryptData)
// Register pprof handlers
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return middleware.NewLoggingMiddleware(logger, mux)
}

179
server/server.go Normal file
View File

@@ -0,0 +1,179 @@
package server
import (
"context"
"crypto/tls"
"log/slog"
"net/http"
"os"
"os/signal"
"time"
"github.com/go-co-op/gocron/v2"
)
// Server represents an HTTP server.
type Server struct {
srv *http.Server
logger *slog.Logger
cron gocron.Scheduler
cancel context.CancelFunc
disableTls bool
tlsCertFilename string
tlsKeyFilename string
encryptionKey string
}
// New creates a new server with the given logger, address and options.
func New(logger *slog.Logger, cron gocron.Scheduler, addr string, opts ...Option) *Server {
// Set some options for TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
InsecureSkipVerify: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
srv := &http.Server{
Addr: addr,
//WriteTimeout: 120 * time.Second,
WriteTimeout: 0,
ReadTimeout: 30 * time.Second,
TLSConfig: tlsConfig,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
// Set the initial server values
server := &Server{
srv: srv,
logger: logger,
cron: cron,
//cancel: cancel,
}
// Apply any options
for _, opt := range opts {
opt(server)
}
return server
}
// Option represents a server option.
type Option func(*Server)
// WithWriteTimeout sets the write timeout.
func WithWriteTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.srv.WriteTimeout = timeout
}
}
// WithReadTimeout sets the read timeout.
func WithReadTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.srv.ReadTimeout = timeout
}
}
// WithRouter sets the handler.
func WithRouter(handler http.Handler) Option {
return func(s *Server) {
s.srv.Handler = handler
}
}
// SetKey sets the encryption key we use when generating secrets
func SetKey(key string) Option {
return func(s *Server) {
s.encryptionKey = key
}
}
// SetTls sets the disable tls value
func SetTls(disableTls bool) Option {
return func(s *Server) {
s.disableTls = disableTls
}
}
// SetCertificate sets the path to the certificate used for TLS, in PEM format
func SetCertificate(tlsCertFilename string) Option {
return func(s *Server) {
//fmt.Printf("Setting tlsCertFilename to '%s'\n", tlsCertFilename)
s.tlsCertFilename = tlsCertFilename
}
}
// SetPrivateKey sets the path to the private key used for TLS, in PEM format
func SetPrivateKey(tlsKeyFilename string) Option {
return func(s *Server) {
s.tlsKeyFilename = tlsKeyFilename
}
}
// StartAndWait starts the server and waits for a signal to shut down.
func (s *Server) StartAndWait() {
s.Start()
s.GracefulShutdown()
}
// Start starts the server.
func (s *Server) Start() {
go func() {
if s.disableTls {
s.logger.Info("starting server", "port", s.srv.Addr)
if err := s.srv.ListenAndServe(); err != nil {
s.logger.Error("failed to start server", "error", err)
os.Exit(1)
}
} else {
s.logger.Info("starting TLS server", "port", s.srv.Addr, "cert", s.tlsCertFilename, "key", s.tlsKeyFilename)
if err := s.srv.ListenAndServeTLS(s.tlsCertFilename, s.tlsKeyFilename); err != nil && err != http.ErrServerClosed {
s.logger.Error("failed to start server", "error", err)
os.Exit(1)
}
}
}()
}
// GracefulShutdown shuts down the server gracefully.
func (s *Server) GracefulShutdown() {
c := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(c, os.Interrupt)
// Block until we receive our signal.
<-c
// Create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
_ = s.srv.Shutdown(ctx)
s.logger.Info("runing cron shutdown")
err := s.cron.Shutdown()
if err != nil {
s.logger.Error("error shutting cron", "error", err)
}
s.logger.Info("runing cancel")
s.cancel()
// Optionally, you could run srv.Shutdown in a goroutine and block on
// <-ctx.Done() if your application should wait for other services
// to finalize based on context cancellation.
s.logger.Info("shutting down")
//os.Exit(0)
}

8
settings.yaml Normal file
View File

@@ -0,0 +1,8 @@
settings:
tenants_to_filter:
- "DecomVM"
node_charge_clusters:
- ".*CMD.*"
srm_activeactive_vms:
vcenter_addresses:
- "https://vc.lab.local/sdk"

10
sqlc.yml Normal file
View File

@@ -0,0 +1,10 @@
version: 2
sql:
- engine: sqlite
queries:
- db/queries/query.sql
schema: db/migrations
gen:
go:
package: queries
out: db/queries

BIN
styles/.DS_Store vendored Normal file

Binary file not shown.

3
styles/input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
tailwind.config.js Normal file
View File

@@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./components/**/*.templ",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}

10
update_module.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <new module name>"
exit 1
fi
new_module=$1
find . -type d -name .git -prune -o -type f -exec sed -i '' -e "s/wnzl-snow/${new_module}/g" {} \;

22
upgrade_htmx.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <new htmx version>"
exit 1
fi
old_version=""
new_version=$1
for filename in "./dist/assets/js"/*; do
if [[ "$filename" == "./dist/assets/js/htmx"* ]]; then
old_version=$(echo "$filename" | awk -F'@' '{gsub(/\.min\.js/, "", $2); print $2}')
break
fi
done
curl -sL -o "./dist/assets/js/htmx@${new_version}.min.js" "https://github.com/bigskysoftware/htmx/releases/download/${new_version}/htmx.min.js"
sed -i '' -e "s/${old_version}/${new_version}/g" "./components/core/html.templ"
rm "./dist/assets/js/htmx@${old_version}.min.js"

13
upgrade_sqlc.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
url=$(curl -s 'https://downloads.sqlc.dev/')
new_version=$(echo "$url" | sed -e 's/<li>/<li>\n/g' | tr -d '\n' | sed -e 's/<li>/\n<li>/g' | sed -e 's/<\/li>/<\/li>\n/g' | grep -o '<li>[^<]*<a[^>]*>[^<]*</a>[^<]*</li>' | sed -e 's/<[^>]*>//g' | tail -n 1 | awk '{$1=$1;print}')
old_version=$(grep 'sqlc-version' .github/workflows/ci.yml | awk '{print $2}' | tr -d "'" | head -n 1)
for file in ".github/workflows"/*; do
if [ -f "$file" ]; then
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
fi
done

18
upgrade_templ.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
old_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
go install github.com/a-h/templ/cmd/templ@latest
go get -u github.com/a-h/templ
go mod tidy
new_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
sed -i '' -e "s/${old_version}/${new_version}/g" "Dockerfile"
for file in ".github/workflows"/*; do
if [ -f "$file" ]; then
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
fi
done

4
version/version.go Normal file
View File

@@ -0,0 +1,4 @@
package version
// Value is the version. This is set at build time.
var Value = "dev"