diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..2be1044 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,16 @@ +## Contributing + +1. Install go (on arch: `pacman -S go`) +2. In the root directory of the repo, run `podman compose up -d` +3. Obtain [golang migrate](https://github.com/golang-migrate/migrate). You may need to download the CLI directly from a release +4. In `backend`, run `migrate -source file://$(pwd)/migrations -database postgres://postgres:test@localhost:5432/postgres?sslmode=disable up` +5. In `backend`, run `go run .` + +### Adding new dependencies +`go mod tidy` + +### Formatting +`gofmt -w -s .` + +### Creating new DB migrations +Create a new file with an incremented prefix number, a human readable string, and ending with `.up.sql`, with a corresponding `.down.sql` that has the same number prefix to revert your change. Apply these with golang-migrate diff --git a/backend/config.yaml b/backend/config.yaml new file mode 100644 index 0000000..b4b4f13 --- /dev/null +++ b/backend/config.yaml @@ -0,0 +1,4 @@ +tracing: + enabled: false +environment: "local" +version: "local-dev" diff --git a/backend/config/context.go b/backend/config/context.go new file mode 100644 index 0000000..498dbd1 --- /dev/null +++ b/backend/config/context.go @@ -0,0 +1,23 @@ +package config + +import ( + "context" + + "github.com/spf13/viper" +) + +type configContextKey string + +const contextKey configContextKey = "config" + +func ContextWithConfig(ctx context.Context, config *viper.Viper) context.Context { + return context.WithValue(ctx, contextKey, config) +} + +func FromContext(ctx context.Context) *viper.Viper { + v := ctx.Value(contextKey) + if v == nil { + panic("config not available in context, somewhere it got lost") + } + return v.(*viper.Viper) +} diff --git a/backend/config/default.go b/backend/config/default.go new file mode 100644 index 0000000..e75f014 --- /dev/null +++ b/backend/config/default.go @@ -0,0 +1,48 @@ +package config + +import ( + "context" + "fmt" + "sync" + + "github.com/spf13/viper" +) + +var once sync.Once + +/* +* GetConfig determines what config file to look at based on the environment, and constructs a config to use + */ +func GetConfig(_ context.Context) *viper.Viper { + config := viper.New() + config.SetEnvPrefix("cluesheet") + + config.BindEnv("config_path") + config.BindEnv("environment") + config.SetDefault("environment", "local") + + if config.IsSet("config_path") { + config.SetConfigFile(config.GetString("config_path")) + } else { + config.SetConfigType("yaml") + config.AddConfigPath(".") + config.AddConfigPath("/opt/cluesheet") + + switch config.GetString("environment") { + case "local": + config.SetConfigName("config.yaml") + case "develop": + config.SetConfigName("config.dev") + case "prod": + config.SetConfigName("config.prod") + default: + panic(fmt.Sprintf("unknown environment '%s', can't look for config", config.GetString("environment"))) + } + } + err := config.ReadInConfig() + if err != nil { + panic(fmt.Sprintf(err.Error())) + } + + return config +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..ff2275b --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,95 @@ +module csh/cluesheet + +go 1.24.2 + +require ( + github.com/DataDog/dd-trace-go/contrib/gorilla/mux/v2 v2.1.0-dev.1 + github.com/DataDog/dd-trace-go/contrib/jackc/pgx.v5/v2 v2.1.0-dev.1 + github.com/DataDog/dd-trace-go/v2 v2.1.0-dev.1 + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/jackc/pgx/v5 v5.6.0 + github.com/spf13/viper v1.20.1 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/DataDog/appsec-internal-go v1.11.2 // indirect + github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 // indirect + github.com/DataDog/datadog-go/v5 v5.6.0 // indirect + github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.1.0-dev.1 // indirect + github.com/DataDog/go-libddwaf/v3 v3.5.4 // indirect + github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 // indirect + github.com/DataDog/go-sqllexer v0.1.0 // indirect + github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect + github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 // indirect + github.com/DataDog/sketches-go v1.4.7 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/outcaste-io/ristretto v0.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.1 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/collector/component v0.120.0 // indirect + go.opentelemetry.io/collector/pdata v1.26.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.120.0 // indirect + go.opentelemetry.io/collector/semconv v0.120.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..f4fcd1f --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,318 @@ +github.com/DataDog/appsec-internal-go v1.11.2 h1:Q00pPMQzqMIw7jT2ObaORIxBzSly+deS0Ely9OZ/Bj0= +github.com/DataDog/appsec-internal-go v1.11.2/go.mod h1:9YppRCpElfGX+emXOKruShFYsdPq7WEPq/Fen4tYYpk= +github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1 h1:XHITEDEb6NVc9n+myS8KJhdK0vKOvY0BTWSFrFynm4s= +github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.64.0-rc.1/go.mod h1:lzCtnMSGZm/3RMk5RBRW/6IuK1TNbDXx1ttHTxN5Ykc= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1 h1:63L66uiNazsZs1DCmb5aDv/YAkCqn6xKqc0aYeATkQ8= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.64.0-rc.1/go.mod h1:3BS4G7V1y7jhSgrbqPx2lGxBb/YomYwUP0wjwr+cBHc= +github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1 h1:8+4sv0i+na4QMjggZrQNFspbVHu7iaZU6VWeupPMdbA= +github.com/DataDog/datadog-agent/pkg/proto v0.64.0-rc.1/go.mod h1:q324yHcBN5hIeCU8eoinM7lP9c7MOA2FTj7oeWAl3Pc= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1 h1:MpUmwDTz+UQN/Pyng5GwvomH7LYjdcFhVVNMnxT4Rvc= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.64.0-rc.1/go.mod h1:QHiOw0sFriX2whwein+Puv69CqJcbOQnocUBo2IahNk= +github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1 h1:5PbiZw511B+qESc7PxxWY5ubiBtVnLFqC+UZKZAB3xo= +github.com/DataDog/datadog-agent/pkg/trace v0.64.0-rc.1/go.mod h1:AkapH6q9UZLoRQuhlOPiibRFqZtaKPMwtzZwYjjzgK0= +github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1 h1:5UHDao4MdRwRsf4ZEvMSbgoujHY/2Aj+TQ768ZrPXq8= +github.com/DataDog/datadog-agent/pkg/util/log v0.64.0-rc.1/go.mod h1:ZEm+kWbgm3alAsoVbYFM10a+PIxEW5KoVhV3kwiCuxE= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1 h1:yqzXiCXrBXsQrbsFCTele7SgM6nK0bElDmBM0lsueIE= +github.com/DataDog/datadog-agent/pkg/util/scrubber v0.64.0-rc.1/go.mod h1:9ZfE6J8Ty8xkgRuoH1ip9kvtlq6UaHwPOqxe9NJbVUE= +github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1 h1:eg+XW2CzOwFa//bjoXiw4xhNWWSdEJbMSC4TFcx6lVk= +github.com/DataDog/datadog-agent/pkg/version v0.64.0-rc.1/go.mod h1:DgOVsfSRaNV4GZNl/qgoZjG3hJjoYUNWPPhbfTfTqtY= +github.com/DataDog/datadog-go/v5 v5.6.0 h1:2oCLxjF/4htd55piM75baflj/KoE6VYS7alEUqFvRDw= +github.com/DataDog/datadog-go/v5 v5.6.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= +github.com/DataDog/dd-trace-go/contrib/gorilla/mux/v2 v2.1.0-dev.1 h1:OkAqfX+hxHahpsMqhpgeqwMEkvHCGl/fksyPYRkdKuo= +github.com/DataDog/dd-trace-go/contrib/gorilla/mux/v2 v2.1.0-dev.1/go.mod h1:mE0tTsneO9+27G2vpoIUw6h7MOjiArZPD+VKZRIAQqw= +github.com/DataDog/dd-trace-go/contrib/jackc/pgx.v5/v2 v2.1.0-dev.1 h1:u3h0GOV7ZG/3X+aZ2JdfbrTS1BZ5lHHHT3nOz+DALEE= +github.com/DataDog/dd-trace-go/contrib/jackc/pgx.v5/v2 v2.1.0-dev.1/go.mod h1:An8ZP52eO0NRktZGttU2lKkawGN7ibiYzLQeXXIqNTk= +github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.1.0-dev.1 h1:8zuFXEvSyuPt6/RcrTXAmeb9Lv5StoKMehBsfB/Jcw4= +github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.1.0-dev.1/go.mod h1:ANaTW4tFkPxEgq1jS18hNJdM8V/NCGkzku8Up31/OHA= +github.com/DataDog/dd-trace-go/v2 v2.1.0-dev.1 h1:e4wSB4blUd5OouFSKPEydsWnMReKHHUIaCOPwsQ++Ko= +github.com/DataDog/dd-trace-go/v2 v2.1.0-dev.1/go.mod h1:WBtf7TA9bWr5uA8DjOyw1qlSKe3bw9gN5nc0Ta9dHFE= +github.com/DataDog/go-libddwaf/v3 v3.5.4 h1:cLV5lmGhrUBnHG50EUXdqPQAlJdVCp9n3aQ5bDWJEAg= +github.com/DataDog/go-libddwaf/v3 v3.5.4/go.mod h1:HoLUHdj0NybsPBth/UppTcg8/DKA4g+AXuk8cZ6nuoo= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 h1:bpitH5JbjBhfcTG+H2RkkiUXpYa8xSuIPnyNtTaSPog= +github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6/go.mod h1:quaQJ+wPN41xEC458FCpTwyROZm3MzmTZ8q8XOXQiPs= +github.com/DataDog/go-sqllexer v0.1.0 h1:QGBH68R4PFYGUbZjNjsT4ESHCIhO9Mmiz+SMKI7DzaY= +github.com/DataDog/go-sqllexer v0.1.0/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc= +github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4= +github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0 h1:GlvoS6hJN0uANUC3fjx72rOgM4StAKYo2HtQGaasC7s= +github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.26.0/go.mod h1:mYQmU7mbHH6DrCaS8N6GZcxwPoeNfyuopUoLQltwSzs= +github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= +github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= +github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg= +github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= +github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 h1:lK/3zr73guK9apbXTcnDnYrC0YCQ25V3CIULYz3k2xU= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= +github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= +github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= +github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY= +github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI= +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/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI= +github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v0.120.0 h1:YHEQ6NuBI6FQHKW24OwrNg2IJ0EUIg4RIuwV5YQ6PSI= +go.opentelemetry.io/collector/component v0.120.0/go.mod h1:Ya5O+5NWG9XdhJPnOVhKtBrNXHN3hweQbB98HH4KPNU= +go.opentelemetry.io/collector/component/componentstatus v0.120.0 h1:hzKjI9+AIl8A/saAARb47JqabWsge0kMp8NSPNiCNOQ= +go.opentelemetry.io/collector/component/componentstatus v0.120.0/go.mod h1:kbuAEddxvcyjGLXGmys3nckAj4jTGC0IqDIEXAOr3Ag= +go.opentelemetry.io/collector/component/componenttest v0.120.0 h1:vKX85d3lpxj/RoiFQNvmIpX9lOS80FY5svzOYUyeYX0= +go.opentelemetry.io/collector/component/componenttest v0.120.0/go.mod h1:QDLboWF2akEqAGyvje8Hc7GfXcrZvQ5FhmlWvD5SkzY= +go.opentelemetry.io/collector/consumer v1.26.0 h1:0MwuzkWFLOm13qJvwW85QkoavnGpR4ZObqCs9g1XAvk= +go.opentelemetry.io/collector/consumer v1.26.0/go.mod h1:I/ZwlWM0sbFLhbStpDOeimjtMbWpMFSoGdVmzYxLGDg= +go.opentelemetry.io/collector/consumer/consumertest v0.120.0 h1:iPFmXygDsDOjqwdQ6YZcTmpiJeQDJX+nHvrjTPsUuv4= +go.opentelemetry.io/collector/consumer/consumertest v0.120.0/go.mod h1:HeSnmPfAEBnjsRR5UY1fDTLlSrYsMsUjufg1ihgnFJ0= +go.opentelemetry.io/collector/consumer/xconsumer v0.120.0 h1:dzM/3KkFfMBIvad+NVXDV+mA+qUpHyu5c70TFOjDg68= +go.opentelemetry.io/collector/consumer/xconsumer v0.120.0/go.mod h1:eOf7RX9CYC7bTZQFg0z2GHdATpQDxI0DP36F9gsvXOQ= +go.opentelemetry.io/collector/pdata v1.26.0 h1:o7nP0RTQOG0LXk55ZZjLrxwjX8x3wHF7Z7xPeOaskEA= +go.opentelemetry.io/collector/pdata v1.26.0/go.mod h1:18e8/xDZsqyj00h/5HM5GLdJgBzzG9Ei8g9SpNoiMtI= +go.opentelemetry.io/collector/pdata/pprofile v0.120.0 h1:lQl74z41MN9a0M+JFMZbJVesjndbwHXwUleVrVcTgc8= +go.opentelemetry.io/collector/pdata/pprofile v0.120.0/go.mod h1:4zwhklS0qhjptF5GUJTWoCZSTYE+2KkxYrQMuN4doVI= +go.opentelemetry.io/collector/pdata/testdata v0.120.0 h1:Zp0LBOv3yzv/lbWHK1oht41OZ4WNbaXb70ENqRY7HnE= +go.opentelemetry.io/collector/pdata/testdata v0.120.0/go.mod h1:PfezW5Rzd13CWwrElTZRrjRTSgMGUOOGLfHeBjj+LwY= +go.opentelemetry.io/collector/pipeline v0.120.0 h1:QQQbnLCYiuOqmxIRQ11cvFGt+SXq0rypK3fW8qMkzqQ= +go.opentelemetry.io/collector/pipeline v0.120.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/processor v0.120.0 h1:No+I65ybBLVy4jc7CxcsfduiBrm7Z6kGfTnekW3hx1A= +go.opentelemetry.io/collector/processor v0.120.0/go.mod h1:4zaJGLZCK8XKChkwlGC/gn0Dj4Yke04gQCu4LGbJGro= +go.opentelemetry.io/collector/processor/processortest v0.120.0 h1:R+VSVSU59W0/mPAcyt8/h1d0PfWN6JI2KY5KeMICXvo= +go.opentelemetry.io/collector/processor/processortest v0.120.0/go.mod h1:me+IVxPsj4IgK99I0pgKLX34XnJtcLwqtgTuVLhhYDI= +go.opentelemetry.io/collector/processor/xprocessor v0.120.0 h1:mBznj/1MtNqmu6UpcoXz6a63tU0931oWH2pVAt2+hzo= +go.opentelemetry.io/collector/processor/xprocessor v0.120.0/go.mod h1:Nsp0sDR3gE+GAhi9d0KbN0RhOP+BK8CGjBRn8+9d/SY= +go.opentelemetry.io/collector/semconv v0.120.0 h1:iG9N78c2IZN4XOH7ZSdAQJBbaHDTuPnTlbQjKV9uIPY= +go.opentelemetry.io/collector/semconv v0.120.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= +k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= diff --git a/backend/log/context.go b/backend/log/context.go new file mode 100644 index 0000000..e7996ab --- /dev/null +++ b/backend/log/context.go @@ -0,0 +1,60 @@ +package log + +import ( + "context" + + "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" + "go.uber.org/zap" + + "csh/cluesheet/config" +) + +type logContextKey string + +const contextKey logContextKey = "log" + +/** +* Instantiate a new logger +* Be sure to defer logger.Sync() to ensure it's flushed before we exit + */ +func GetLogger(ctx context.Context) *zap.Logger { + // TODO we should probably handle the error + var logger *zap.Logger + + switch config.FromContext(ctx).GetString("environment") { + case "local": + logger, _ = zap.NewDevelopment() + case "dev": + logger, _ = zap.NewDevelopment() + case "prod": + logger, _ = zap.NewProduction() + } + + return logger +} + +func ContextWithLogger(ctx context.Context, logger *zap.Logger) context.Context { + return context.WithValue(ctx, contextKey, logger) +} + +func FromContext(ctx context.Context) *zap.Logger { + l := ctx.Value(contextKey) + var logger *zap.Logger + if l == nil { + logger = zap.L() + } else { + logger = l.(*zap.Logger) + } + + config := config.FromContext(ctx) + if span, ok := tracer.SpanFromContext(ctx); ok { + spanContext := span.Context() + logger = logger.With( + zap.String("dd.trace_id", spanContext.TraceID()), + zap.Uint64("dd.span_id", spanContext.SpanID()), + zap.String("dd.version", config.GetString("version")), + zap.String("dd.env", config.GetString("environment")), + ) + } + return logger +} diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..15a5cc6 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,473 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "slices" + "time" + + "csh/cluesheet/config" + "csh/cluesheet/log" + + "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" + + muxtrace "github.com/DataDog/dd-trace-go/contrib/gorilla/mux/v2" + "github.com/gorilla/mux" + + pgxtrace "github.com/DataDog/dd-trace-go/contrib/jackc/pgx.v5/v2" + "github.com/jackc/pgx/v5" + + "github.com/google/uuid" + + "go.uber.org/zap" +) + +const ( + connStr = "postgres://postgres:test@localhost:5432/postgres?sslmode=disable" +) + +func main() { + ctx := context.Background() + + ctx = config.ContextWithConfig(ctx, config.GetConfig(ctx)) + ctx = log.ContextWithLogger(ctx, log.GetLogger(ctx)) + + if config.FromContext(ctx).GetBool("tracing.enabled") { + tracer.Start( + tracer.WithEnv(config.FromContext(ctx).GetString("env")), + tracer.WithService("cluesheet"), + // tracer.WithServiceVersion(), // TODO add once we have a git commit + ) + defer tracer.Stop() + log.FromContext(ctx).Debug("started tracing") + } + + conn, err := pgxtrace.NewPool(ctx, connStr) + if err != nil { + log.FromContext(ctx).Fatal("failed setting up db pool", zap.Error(err)) + } + defer conn.Close() + + router := muxtrace.NewRouter() + + // Pass the logger and config in context to all requests + router.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctx = config.ContextWithConfig(ctx, config.GetConfig(ctx)) + ctx = log.ContextWithLogger(ctx, log.GetLogger(ctx)) + r = r.WithContext(ctx) + h.ServeHTTP(rw, r) + }) + }) + + router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(r.RemoteAddr)) + }) + + v1 := router.PathPrefix("/api/v1/").Subrouter() + v1.Path("/cluesheet").Methods("GET").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rows, err := conn.Query(r.Context(), `select id, name, origin_id, created_by, created_at, edited_by, edited_at, visibility, owners, groups from cluesheet`) + if err != nil { + http.Error(rw, fmt.Sprintf("failed getting cluesheets: '%s'", err), 500) + return + } + + cluesheets, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Cluesheet]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed getting cluesheet: '%s'", err), 500) + return + } + + for _, cluesheet := range cluesheets { + clues, err := GetClues(ctx, conn, cluesheet.Id) + if err != nil { + http.Error(rw, fmt.Sprintf("failed resolving clues: '%s'", err), 500) + return + } + cluesheet.Clues = &clues + } + + data, err := json.Marshal(cluesheets) + if err != nil { + http.Error(rw, fmt.Sprintf("failed marshalling cluesheets: '%s'", err), 500) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + v1.Path("/cluesheet/{id}").Methods("GET").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + id, err := uuid.Parse(vars["id"]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse uuid '%s': '%s'", vars["id"], err), 400) + return + } + + rows, err := conn.Query(r.Context(), `select id, name, origin_id, created_by, created_at, edited_by, edited_at, visibility, owners, groups from cluesheet where id = $1`, id) + if err != nil { + http.Error(rw, fmt.Sprintf("failed getting cluesheet '%s': '%s'", vars["id"], err), 500) + return + } + + cluesheet, err := pgx.CollectOneRow[Cluesheet](rows, pgx.RowToStructByNameLax[Cluesheet]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed getting cluesheet '%s': '%s'", vars["id"], err), 500) + return + } + + clues, err := GetClues(ctx, conn, cluesheet.Id) + if err != nil { + http.Error(rw, fmt.Sprintf("failed resolving clues '%s': '%s'", vars["id"], err), 500) + return + } + cluesheet.Clues = &clues + + data, err := json.Marshal(cluesheet) + if err != nil { + http.Error(rw, fmt.Sprintf("failed marshalling cluesheet '%s': '%s'", vars["id"], err), 500) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + + /* + POST /api/v1/cluesheet: create a cluesheet + params: + - ID (generated) + - Name + - Origin (optional) + - creator + - created time (generated) + - editor (from creator) + - edited time (generated) + - visibility (optional, default hidden) + - Owners (optional, default creator) + - Groups (optional) + */ + v1.Path("/cluesheet").Methods("POST").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to read request: %s", err.Error()), 400) + return + } + + var params struct { + Name string + Origin *uuid.UUID + Creator string + Visibility *string + Owners []string + Groups []string + } + + err = json.Unmarshal(body, ¶ms) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse request: %s", err.Error()), 400) + return + } + if params.Name == "" { + http.Error(rw, "Name must not be empty", 400) + return + } + if params.Creator == "" { + http.Error(rw, "Creator must not be empty", 400) + } + + if params.Visibility == nil || *params.Visibility == "" { + // TODO this isn't canonical + local := "hidden" + params.Visibility = &local + } + + if !slices.Contains(params.Owners, params.Creator) { + params.Owners = append(params.Owners, params.Creator) + } + + if params.Groups == nil { + params.Groups = []string{} + } + + newSheet := Cluesheet{ + Id: uuid.New(), + Name: params.Name, + Origin_id: params.Origin, + Created_by: params.Creator, + Created_at: time.Now(), + Edited_by: params.Creator, + Edited_at: time.Now(), + Visibility: *params.Visibility, + Owners: params.Owners, + Groups: params.Groups, + } + _, err = conn.Exec(ctx, `insert into cluesheet (id, name, origin_id, created_by, created_at, edited_by, edited_at, visibility, owners, groups) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, newSheet.Id, newSheet.Name, newSheet.Origin_id, newSheet.Created_by, newSheet.Created_at, newSheet.Edited_by, newSheet.Edited_at, newSheet.Visibility, newSheet.Owners, newSheet.Groups) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to persist cluesheet: %s", err), 500) + return + } + + data, err := json.Marshal(newSheet) + if err != nil { + http.Error(rw, fmt.Sprintf("failed marshalling cluesheet: '%s'", err), 500) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + + // POST /api/v1/cluesheet/{id}/clue + // params: + // - description + // - rules are TBD + // - creator + // - created and edited are derived/generated + // - tags (optional) + // - parent_clue_id (optional) + v1.Path("/cluesheet/{id}/clue").Methods("POST").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := uuid.Parse(vars["id"]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse uuid '%s': '%s'", vars["id"], err), 400) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to read request: %s", err.Error()), 400) + return + } + + var params struct { + Description string + Creator string + Tags []string + ParentClueId *uuid.UUID `json:"parent_clue_id"` + } + + err = json.Unmarshal(body, ¶ms) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse request: %s", err.Error()), 400) + return + } + + if params.Description == "" { + http.Error(rw, "Description must not be empty", 400) + return + } + + if params.Creator == "" { + http.Error(rw, "Creator must not be empty", 400) + return + } + + if params.Tags == nil { + params.Tags = []string{} + } + + newClue := Clue{ + Id: uuid.New(), + Description: params.Description, + Tags: params.Tags, + Origin_id: id, + Created_by: params.Creator, + Created_at: time.Now(), + Edited_by: params.Creator, + Edited_at: time.Now(), + Children: []*Clue{}, + } + + tx, err := conn.Begin(r.Context()) + if err != nil { + http.Error(rw, "failed to store clue", 500) + fmt.Println(err.Error()) + return + } + defer tx.Rollback(r.Context()) + + _, err = tx.Exec(r.Context(), `insert into clue(id, description, tags, origin_id, created_by, created_at, edited_by, edited_at) values ($1, $2, $3, $4, $5, $6, $7, $8)`, + newClue.Id, + newClue.Description, + newClue.Tags, + newClue.Origin_id, + newClue.Created_by, + newClue.Created_at, + newClue.Edited_by, + newClue.Edited_at, + ) + if err != nil { + http.Error(rw, "failed to store clue", 500) + fmt.Println(err.Error()) + return + } + + var cr *ClueRelation = nil + + fmt.Printf("%v\n%s\n", params, body) + + if params.ParentClueId != nil { + cr = &ClueRelation{ + Id: uuid.New(), + Parent_id: *params.ParentClueId, + Child_id: newClue.Id, + } + + // TODO validate parent exists on same sheet + + _, err = tx.Exec(r.Context(), `insert into clue_relation(id, parent_id, child_id) values ($1, $2, $3)`, cr.Id, cr.Parent_id, cr.Child_id) + if err != nil { + http.Error(rw, "failed to store clue parent", 500) + fmt.Println(err.Error()) + return + } + } + + err = tx.Commit(r.Context()) + if err != nil { + http.Error(rw, "failed to store clue with parent", 500) + fmt.Println(err.Error()) + return + } + + data, err := json.Marshal(struct { + Clue Clue + ClueRelation *ClueRelation `json:",omitempty,omitzero"` + }{Clue: newClue, ClueRelation: cr}) + if err != nil { + http.Error(rw, "Failed to marshal data", 500) + fmt.Println(err.Error()) + return + } + + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + + v1.Path("/cluesheet/{cluesheet_id}/participation/{ipa_uid}").Methods("GET").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cluesheet_id, err := uuid.Parse(vars["cluesheet_id"]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse uuid '%s': '%s'", vars["id"], err), 400) + return + } + + ipa_uid := vars["ipa_uid"] + // todo validation + + rows, err := conn.Query(r.Context(), `select * from user_participation where cluesheet_id = $1 and ipa_uid = $2`, cluesheet_id, ipa_uid) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to query for user participation on '%s' for user '%s': %s", cluesheet_id, ipa_uid, err), 500) + return + } + + participation, err := pgx.CollectOneRow[UserParticipation](rows, pgx.RowToStructByNameLax[UserParticipation]) + if err == pgx.ErrNoRows { + // if no stored result, there's no hiding + participation = UserParticipation{ + Cluesheet_id: cluesheet_id, + Ipa_uid: ipa_uid, + Hidden: false, + } + } else if err != nil { + http.Error(rw, fmt.Sprintf("failed to query for user participation on '%s' for user '%s': %s", cluesheet_id, ipa_uid, err), 500) + return + } + + data, err := json.Marshal(participation) + if err != nil { + http.Error(rw, "Failed to marshal data", 500) + fmt.Println(err.Error()) + return + } + + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + + v1.Path("/cluesheet/{cluesheet_id}/participation/{ipa_uid}").Methods("POST").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cluesheet_id, err := uuid.Parse(vars["cluesheet_id"]) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse uuid '%s': '%s'", vars["id"], err), 400) + return + } + + ipa_uid := vars["ipa_uid"] + // todo validation + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to read request: %s", err.Error()), 400) + return + } + + var params struct { + Hidden bool + } + + err = json.Unmarshal(body, ¶ms) + if err != nil { + http.Error(rw, fmt.Sprintf("failed to parse request: %s", err.Error()), 400) + return + } + + participation := UserParticipation{Cluesheet_id: cluesheet_id, Ipa_uid: ipa_uid, Hidden: params.Hidden} + + log.FromContext(r.Context()).Info("participation", zap.Any("participation", participation)) + _, err = conn.Exec(r.Context(), `insert into user_participation(cluesheet_id, ipa_uid, hidden) values ($1, $2, $3) on conflict (cluesheet_id, ipa_uid) do update set hidden = $3`, participation.Cluesheet_id, participation.Ipa_uid, participation.Hidden) + if err != nil { + http.Error(rw, "failed to store clue parent", 500) + fmt.Println(err.Error()) + return + } + + data, err := json.Marshal(participation) + if err != nil { + http.Error(rw, "Failed to marshal data", 500) + fmt.Println(err.Error()) + return + } + + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: router, + } + + // channel to prevent the main thread from exiting too early + shutdown := make(chan struct{}) + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + ctx, cancel := context.WithTimeout(ctx, time.Second*15) + defer cancel() + go func() { + select { + case <-c: + // Immediately stop on a repeated sigterm + cancel() + case <-ctx.Done(): + } + }() + log.FromContext(ctx).Info("shutting down gracefully") + srv.Shutdown(ctx) + shutdown <- struct{}{} + }() + + log.FromContext(ctx).Info("starting http server") + srv.ListenAndServe() + <-shutdown + log.FromContext(ctx).Info("exiting...") +} diff --git a/backend/migrations/001_init.up.sql b/backend/migrations/001_init.up.sql new file mode 100644 index 0000000..25cfdc7 --- /dev/null +++ b/backend/migrations/001_init.up.sql @@ -0,0 +1,53 @@ +/* todo constraints and the like */ +CREATE TABLE cluesheet ( + id UUID, + name text, + origin_id UUID, /* originating cluesheet */ + created_by text, /* ipa unique id */ + created_at timestamp, /* no timezone by default */ + edited_by text, /* ipa unique id */ + edited_at timestamp, /* no timezone */ + visibility text, /* TODO: int key? or defined enum? I think SQL enums are difficult to work with in migrations */ + owners text[], + groups text[] +); + +CREATE TABLE clue ( + id UUID, + description text, + rule_id UUID, + rule_params jsonb, + origin_id UUID, /* originating cluesheet */ + created_by text, + created_at timestamp, + edited_by text, + edited_at timestamp, + tags text[] +); + +CREATE TABLE clue_relation ( + id UUID, + parent_id UUID, + child_id UUID +); + +CREATE TABLE user_progress ( + id UUID, + ipa_uid text, /* ipa unique id */ + clue_id UUID, + completions integer +); + +CREATE TABLE clue_rules ( + id UUID, + name text, + jq_expr text, + req_params text /* TODO */ +); + +CREATE TABLE user_participation ( + id UUID, + cluesheet_id UUID, + ipa_uid text, /* ipa unique id */ + hidden boolean +); diff --git a/backend/migrations/002_participation_constraint.down.sql b/backend/migrations/002_participation_constraint.down.sql new file mode 100644 index 0000000..e4572db --- /dev/null +++ b/backend/migrations/002_participation_constraint.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE user_participation +ADD COLUMN id UUID; + +ALTER TABLE user_participation +DROP CONSTRAINT IF EXISTS user_participation_pk; diff --git a/backend/migrations/002_participation_constraint.up.sql b/backend/migrations/002_participation_constraint.up.sql new file mode 100644 index 0000000..605d185 --- /dev/null +++ b/backend/migrations/002_participation_constraint.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE user_participation +DROP COLUMN IF EXISTS id; + +ALTER TABLE user_participation +ADD CONSTRAINT user_participation_pk PRIMARY KEY (cluesheet_id, ipa_uid); diff --git a/backend/model.go b/backend/model.go new file mode 100644 index 0000000..cdf8b60 --- /dev/null +++ b/backend/model.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "iter" + "maps" + "slices" + "sync" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/google/uuid" +) + +type Cluesheet struct { + Id uuid.UUID + Name string + Origin_id *uuid.UUID /* originating cluesheet */ + Created_by string /* ipa unique id */ + Created_at time.Time /* no timezone by default */ + Edited_by string /* ipa unique id */ + Edited_at time.Time /* no timezone */ + Visibility string /* TODO: int key? or defined enum? I think SQL enums are difficult to work with in migrations */ + Owners []string + Groups []string + Clues *[]Clue `json:",omitzero"` +} + +type Clue struct { + Id uuid.UUID + Description string + Rule_id *uuid.UUID + Rule_params *json.RawMessage + Origin_id uuid.UUID /* originating cluesheet */ + Created_by string + Created_at time.Time + Edited_by string + Edited_at time.Time + Tags []string + Children []*Clue `json:",omitzero"` +} + +type UserParticipation struct { + Cluesheet_id uuid.UUID + Ipa_uid string + Hidden bool +} + +type ClueRelation struct { + Id uuid.UUID + Parent_id uuid.UUID + Child_id uuid.UUID +} + +func GetClues(ctx context.Context, conn *pgxpool.Pool, cluesheet uuid.UUID) ([]Clue, error) { + rows, err := conn.Query(ctx, `select * from clue where origin_id = $1`, cluesheet) + if err != nil { + return nil, fmt.Errorf("failed to query clues: %w", err) + } + clues, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Clue]) + + var clueMap map[uuid.UUID]*Clue = make(map[uuid.UUID]*Clue, len(clues)) + var out map[uuid.UUID]*Clue = make(map[uuid.UUID]*Clue) + + for _, clue := range clues { + clueMap[clue.Id] = &clue + out[clue.Id] = &clue + } + var mex sync.Mutex + var relations []ClueRelation + b := &pgx.Batch{} + + for _, child := range clues { + qq := b.Queue(`select parent_id, child_id from clue_relation where child_id = $1`, child.Id) + qq.Query(func(rows pgx.Rows) error { + rel, err := pgx.CollectOneRow(rows, pgx.RowToStructByNameLax[ClueRelation]) + // Empty rows are fine + if errors.Is(err, pgx.ErrNoRows) { + return nil + } else if err != nil { + return err + } + + mex.Lock() + defer mex.Unlock() + relations = append(relations, rel) + return nil + }) + } + br := conn.SendBatch(ctx, b) + err = br.Close() + if err != nil { + return nil, fmt.Errorf("failed querying for clue relations: %w", err) + } + + for _, rel := range relations { + clueMap[rel.Parent_id].Children = append(clueMap[rel.Parent_id].Children, clueMap[rel.Child_id]) + delete(out, rel.Child_id) + } + + return slices.Collect(PointerToValue[iter.Seq[*Clue], Clue](maps.Values(out))), nil +} + +func PointerToValue[Iter iter.Seq[*V], V any](in Iter) iter.Seq[V] { + return func(yeild func(V) bool) { + for v := range in { + if !yeild(*v) { + return + } + } + } +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a7f42d1 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + db: + image: postgres:17 + container_name: cluesheet_postgres + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: test