initial
This commit is contained in:
34
Dockerfile
Normal file
34
Dockerfile
Normal 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
24
Makefile
Normal 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
49
README.md
Normal 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
BIN
build/vctp
Executable file
Binary file not shown.
30
cert.pem
Normal file
30
cert.pem
Normal 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-----
|
9
components/core/footer.templ
Normal file
9
components/core/footer.templ
Normal 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">
|
||||
© Nathan Coad (nathan.coad@dell.com)
|
||||
</div>
|
||||
</footer>
|
||||
}
|
40
components/core/footer_templ.go
Normal file
40
components/core/footer_templ.go
Normal 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\">© Nathan Coad (nathan.coad@dell.com)</div></footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
14
components/core/header.templ
Normal file
14
components/core/header.templ
Normal 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>
|
||||
}
|
55
components/core/header_templ.go
Normal file
55
components/core/header_templ.go
Normal 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
|
30
components/core/html.templ.txt
Normal file
30
components/core/html.templ.txt
Normal 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>
|
||||
}
|
8
components/home/home.templ.txt
Normal file
8
components/home/home.templ.txt
Normal 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>
|
||||
}
|
29
components/views/index.templ
Normal file
29
components/views/index.templ
Normal 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>
|
||||
}
|
105
components/views/index_templ.go
Normal file
105
components/views/index_templ.go
Normal 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
129
db/db.go
Normal 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
94
db/local.go
Normal 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
|
||||
}
|
24
db/migrations/20250321060416_init.sql
Normal file
24
db/migrations/20250321060416_init.sql
Normal 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
31
db/queries/db.go
Normal 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
78
db/queries/models.go
Normal 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
121
db/queries/query.sql
Normal 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
899
db/queries/query.sql.go
Normal 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
BIN
dist/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
dist/assets/.DS_Store
vendored
Normal file
BIN
dist/assets/.DS_Store
vendored
Normal file
Binary file not shown.
536
dist/assets/css/mvp.css
vendored
Normal file
536
dist/assets/css/mvp.css
vendored
Normal 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
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
788
dist/assets/css/output@dev.css
vendored
Normal 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
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
8
dist/dist.go
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
package dist
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed all:assets
|
||||
var AssetsDir embed.FS
|
47
go.mod
Normal file
47
go.mod
Normal 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
179
go.sum
Normal 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
303
internal/report/create.go
Normal 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
|
||||
}
|
80
internal/secrets/secrets.go
Normal file
80
internal/secrets/secrets.go
Normal 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
|
||||
}
|
67
internal/settings/settings.go
Normal file
67
internal/settings/settings.go
Normal 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
15
internal/tasks/tasks.go
Normal 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
|
||||
}
|
139
internal/utils/certOperations.go
Normal file
139
internal/utils/certOperations.go
Normal 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
68
internal/utils/utils.go
Normal 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
52
key.pem
Normal 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
100
log/log.go
Normal 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
232
main.go
Normal 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)
|
||||
}
|
61
server/handler/encryptData.go
Normal file
61
server/handler/encryptData.go
Normal 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
33
server/handler/handler.go
Normal 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
62
server/handler/home.go
Normal 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
22
server/handler/newSnow.go
Normal 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))
|
||||
}
|
||||
}
|
61
server/handler/reportDownload.go
Normal file
61
server/handler/reportDownload.go
Normal 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)
|
||||
}
|
42
server/handler/updateCleanup.go
Normal file
42
server/handler/updateCleanup.go
Normal 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")
|
||||
}
|
||||
}
|
18
server/middleware/cache.go
Normal file
18
server/middleware/cache.go
Normal 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)
|
||||
})
|
||||
}
|
35
server/middleware/logging.go
Normal file
35
server/middleware/logging.go
Normal 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)),
|
||||
)
|
||||
}
|
22
server/middleware/middleware.go
Normal file
22
server/middleware/middleware.go
Normal 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
292
server/models/models.go
Normal 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
62
server/router/router.go
Normal 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
179
server/server.go
Normal 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
8
settings.yaml
Normal 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
10
sqlc.yml
Normal 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
BIN
styles/.DS_Store
vendored
Normal file
Binary file not shown.
3
styles/input.css
Normal file
3
styles/input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
14
tailwind.config.js
Normal file
14
tailwind.config.js
Normal 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
10
update_module.sh
Executable 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
22
upgrade_htmx.sh
Executable 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
13
upgrade_sqlc.sh
Executable 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
18
upgrade_templ.sh
Executable 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
4
version/version.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package version
|
||||
|
||||
// Value is the version. This is set at build time.
|
||||
var Value = "dev"
|
Reference in New Issue
Block a user