Add depot, attendant, jetfire, optimus, ota services with kustomize overlays

This commit is contained in:
Chris Rai
2026-01-31 15:35:07 -05:00
parent a0ec642ca1
commit 9a5cb2f547
404 changed files with 38817 additions and 16 deletions

View File

@@ -0,0 +1,26 @@
ARG BASE_IMAGE=cloud_base_go
FROM ${BASE_IMAGE} as builder-go
WORKDIR /build/ota_update_go
COPY ./ota_update_go/go.mod ./ota_update_go/go.sum ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go install github.com/swaggo/swag/cmd/swag@v1.7.8 \
&& go mod download
COPY ./ota_update_go ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& swag init --parseDependency --parseInternal --parseDepth 5 -g main.go \
&& go build -tags musl
FROM alpine:3.17
RUN apk add --no-cache librdkafka --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
&& apk add --no-cache ca-certificates
COPY ./modules_go/logger/log_config .
COPY --from=builder-go /build/ota_update_go/otaupdate .
ENV LOG_CONFIG=log_config
EXPOSE 8077
CMD ./otaupdate

View File

@@ -0,0 +1,193 @@
package background
import (
"encoding/json"
"maps"
"otaupdate/services"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/carcommand"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
// Anywhere a comment says lock, its probably actually lock/unlock
// Have a local list we check and run against
// and have a redis cache list that we will modify
// keep track of time started, last updated time, number of times sent, and if its a lock or unlock
var (
immobilizerOnce sync.Once
immobilizer *Immobilizer
)
// Message will stay alive in redis for an hour before it is removed
const CAR_CHECK_INTERVAL time.Duration = time.Duration(5 * time.Minute) // Minutes between checking car status and sending command
const REDIS_UPDATE_INTERVAL time.Duration = time.Duration(time.Hour * 2) // Minutes between updating redis with the current cache list. // need to keep track of last updated incase our service goes down
const REDIS_EXPIRATION_DURATION time.Duration = time.Duration(24 * time.Hour) // how old a time should be before we pick it up for us to process
const CAR_LOCK_ATTEMPT_COUNT = 15 // how many times to send the lock command
type CarTrack struct {
TimesModified int `json:"times_modified"`
Immobilize bool `json:"immobilize"` // 0: unlock, 1: lock
}
type Immobilizer struct {
VINs map[string]*CarTrack // By having a pointer here, should be able to modify timesModified without write lock
sync.RWMutex // mutex for handling the reading and writing of the VIN's map
}
func GetImmobilizer() *Immobilizer {
immobilizerOnce.Do(func() {
if immobilizer != nil {
return
}
logger.Info().Msg("Init Immobilizer instance")
immobilizer = InitiateImmobilizer()
})
return immobilizer
}
func InitiateImmobilizer() *Immobilizer {
imm := &Immobilizer{}
imm.VINs = make(map[string]*CarTrack)
go time.AfterFunc(CAR_CHECK_INTERVAL, imm.CheckCars)
// go time.AfterFunc(REDIS_UPDATE_INTERVAL, imm.UpdateRedis)
// Have a random offset so multiple ota's starting at same time don't look and claim at the same time
// go time.AfterFunc(REDIS_EXPIRATION_DURATION+(time.Duration(rand.IntN(20))*time.Minute), imm.ClaimRedis)
return imm
}
func (imm *Immobilizer) GetVINList() (vinList []string) {
imm.RLock()
defer imm.RUnlock()
for vin := range imm.VINs {
vinList = append(vinList, vin)
}
return
}
func (imm *Immobilizer) CheckCars() {
clientPool := services.RedisClientPool()
twins, _ := cache.GetVINListDigitalTwin(imm.GetVINList(), clientPool)
parkedVINs := []string{} // parkedVINs are the cars we are going to send the lock command to
for vin, twin := range twins {
gear := twin.Gear
if gear == nil {
continue
}
if gear.InPark {
parkedVINs = append(parkedVINs, vin)
}
}
// Have a list of vins we can now modify
// Send the lock/unlock command, send wake up command
hopefulImmob := imm.sendRemoteCommands(parkedVINs)
// Not on hopeful immob, we do not check if the car ever wakes up from its sms, so its possible if parked deep in a garage it will not
// Possible fix: remove redis timeout for car lock commands
go imm.RemoveVINs(hopefulImmob)
time.AfterFunc(CAR_CHECK_INTERVAL, imm.CheckCars)
}
func (imm *Immobilizer) sendRemoteCommands(parkedVINs []string) (removableVins []string) {
// First send wake up message
// Send lock or unlock command
// for _, vin := range request.VINs {
// Action logger should get added to when the user adds car to the list
// go func() {
// actionLog := actionlogger.ActionLog{
// VIN: vin,
// Action: actionlogger.RemoteCommand,
// UserIdentifier: httphandlers.GetClientID(r),
// CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/vehicle_command.go",
// Description: string(description),
// }
// err = alDB.Insert(actionLog)
// if err != nil {
// logger.Err(err).Msg("failed to insert action log inside HandleVehicleCommand")
// }
// }()
// vehicle_command.go has an extremely convoluted way to get remote commands to car. It pushed it to a kafka queue to to be picked up
// then kafka does some delivery re-trying stuff in some way, wakes up the car and then puts the message into redis.
// we are going straight to the redis section
// remoteCommands := make([]string, 0, len(parkedVINs))
imm.RLock()
batch := redis.NewRedisBatchCommands()
smsClient := services.GetSMSClient()
wake := carcommand.NewCarWakeUp(services.GetDB().GetCars(), smsClient)
for _, vin := range parkedVINs {
temp := imm.VINs[vin]
temp.TimesModified -= 1
if temp.TimesModified < 0 {
removableVins = append(removableVins, vin)
continue
}
// probably faster to create the list of things to push and batch, but fine for now
msg := common.RemoteCommandSource{}
if temp.Immobilize {
msg.Command = "doors_lock"
} else {
msg.Command = "doors_unlock"
}
// SafeQueueMessage auto deletes after one hour, which I do not want with this lock command, rather it sat around indefinitely
data, _ := json.Marshal(common.Message{
Handler: "remote_command",
Data: msg,
})
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(vin)), data)
// try to wake up car
wake.WakeUp(vin, false)
}
imm.RUnlock()
redisClient := services.RedisClientPool().GetFromPool()
defer redisClient.Close()
_, err := redisClient.ExecuteBatch(batch)
if err != nil {
logger.Err(err).Msg("failed to push car immobilizer commands to redis")
}
return
}
func (imm *Immobilizer) RemoveVINs(vins []string) {
imm.Lock()
defer imm.Unlock()
for _, v := range vins {
delete(imm.VINs, v)
}
}
func (imm *Immobilizer) AddVINs(vins []string, immobilize bool) {
imm.Lock()
defer imm.Unlock()
for _, v := range vins {
imm.VINs[v] = &CarTrack{TimesModified: CAR_LOCK_ATTEMPT_COUNT, Immobilize: immobilize}
}
}
// Just returns the information about the vins we are currently tracking
// not sure about memory safety on this
func (imm *Immobilizer) GetVINInformation() (vinInfo map[string]*CarTrack) {
imm.RLock()
defer imm.RUnlock()
vinInfo = maps.Clone(imm.VINs)
return
}
func (imm *Immobilizer) UpdateRedis() {
}
func (imm *Immobilizer) ClaimRedis() {
}

View File

@@ -0,0 +1,8 @@
package controllers
import "time"
type CarCANSignal struct {
VIN string
Last time.Time
}

View File

@@ -0,0 +1,7 @@
package controllers
import "github.com/pkg/errors"
var ErrorUnableToConvert = errors.New("unable to convert struct")
var ErrorPKRequired = errors.New("primary key required")
var ErrorNotFound = errors.New("no object found")

View File

@@ -0,0 +1,38 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/go-pg/pg/v10/orm"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewCreate(helper CreateHelperInterface) *HandleCreate {
return &HandleCreate{Helper: helper}
}
type CreateHelperInterface interface {
ParseRequest(r *http.Request, model interface{}) error
QueryInsert(model interface{}) (orm.Result, error)
NewModel() interface{}
}
type HandleCreate struct {
Helper CreateHelperInterface
}
func (h *HandleCreate) Handle(w http.ResponseWriter, r *http.Request) {
model := h.Helper.NewModel()
err := h.Helper.ParseRequest(r, model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
_, err = h.Helper.QueryInsert(model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, model)
}

View File

@@ -0,0 +1,47 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/go-pg/pg/v10/orm"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewDelete(helper DeleteHelperInterface) *HandleDelete {
return &HandleDelete{Helper: helper}
}
type DeleteHelperInterface interface {
ParseDeleteQueryParams(r *http.Request) interface{}
QueryDelete(req interface{}) (orm.Result, error)
ValidatePK(model interface{}) error
}
type HandleDelete struct {
Helper DeleteHelperInterface
}
func (h *HandleDelete) Handle(w http.ResponseWriter, r *http.Request) {
filter := h.Helper.ParseDeleteQueryParams(r)
err := h.Helper.ValidatePK(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
result, err := h.Helper.QueryDelete(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if result != nil && result.RowsAffected() == 0 {
loggerdataresp.BadDataErrorResp(w, errors.New("Nothing deleted"), http.StatusNotFound)
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,61 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewGetList(helper GetListHelperInterface) *HandleGetList {
return &HandleGetList{Helper: helper}
}
type GetListHelperInterface interface {
ParseGetListQueryParams(r *http.Request) interface{}
QueryCount(filter interface{}) (int, error)
QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error)
HasPK(filter interface{}) bool
}
type HandleGetList struct {
Helper GetListHelperInterface
}
func (h *HandleGetList) Handle(w http.ResponseWriter, r *http.Request) {
var total int
filter := h.Helper.ParseGetListQueryParams(r)
err := validator.ValidateNonRequired(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := queries.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "created_at DESC"
}
items, err := h.Helper.QuerySelect(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if options.Offset == 0 && !h.Helper.HasPK(filter) {
total, err = h.Helper.QueryCount(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: items,
Total: total,
})
}

View File

@@ -0,0 +1,38 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewGetModel(helper GetModelHelperInterface) *HandleGetModel {
return &HandleGetModel{Helper: helper}
}
type GetModelHelperInterface interface {
ParseGetModelParams(r *http.Request) interface{}
QueryLoad(model interface{}) error
HasPK(model interface{}) bool
}
type HandleGetModel struct {
Helper GetModelHelperInterface
}
func (h *HandleGetModel) Handle(w http.ResponseWriter, r *http.Request) {
item := h.Helper.ParseGetModelParams(r)
hasPK := h.Helper.HasPK(item)
if !hasPK {
loggerdataresp.BadDataErrorResp(w, ErrorPKRequired, http.StatusBadRequest)
return
}
err := h.Helper.QueryLoad(item)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, item)
}

View File

@@ -0,0 +1,48 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/go-pg/pg/v10/orm"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewUpdate(helper UpdateHelperInterface) *HandleUpdate {
return &HandleUpdate{Helper: helper}
}
type UpdateHelperInterface interface {
ParseRequest(r *http.Request, model interface{}) error
QueryUpdate(model interface{}) (orm.Result, error)
NewModel() interface{}
ValidatePK(model interface{}) error
}
type HandleUpdate struct {
Helper UpdateHelperInterface
}
func (h *HandleUpdate) Handle(w http.ResponseWriter, r *http.Request) {
model := h.Helper.NewModel()
err := h.ParseRequest(r, model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
_, err = h.Helper.QueryUpdate(model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, model)
}
func (h *HandleUpdate) ParseRequest(r *http.Request, model interface{}) error {
err := h.Helper.ParseRequest(r, model)
if err != nil {
return err
}
return h.Helper.ValidatePK(model)
}

View File

@@ -0,0 +1,46 @@
package controllers
import (
"otaupdate/services"
"time"
"github.com/fiskerinc/cloud-services/pkg/health"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
func HealthCheck() {
redis := health.NewRedisHealth(services.RedisClientPool())
server := health.HealthCheckServer{}
err := server.Serve([]health.Config{
{
Name: "db",
Check: health.NewPostgresCheck(services.GetDB().GetDBClient().GetConn()),
Timeout: time.Second * 1,
},
{
Name: "redis",
Check: redis.Check,
Timeout: time.Second * 1,
Info: redis.RedisStatus,
},
{
Name: "mongodb",
Check: health.NewMongoDBCheck(getMongoClient),
Timeout: time.Second * 1,
},
})
if err != nil {
logger.Error().Err(err).Send()
}
}
func getMongoClient() (health.MongoConnCheckInterface, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
conn := client.(health.MongoConnCheckInterface)
return conn, nil
}

View File

@@ -0,0 +1,14 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
)
type HelperBase struct {
}
func (h *HelperBase) ParseRequest(r *http.Request, data interface{}) error {
return httphandlers.ParseRequest(r, data)
}

View File

@@ -0,0 +1,43 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewMongoCreate(helper MongoCreateHelperInterface) *MongoHandleCreate {
return &MongoHandleCreate{Helper: helper}
}
type MongoCreateHelperInterface interface {
QueryInsert(model interface{}) error
NewModel() interface{}
ValidatePK(model interface{}) error
}
type MongoHandleCreate struct {
Helper MongoCreateHelperInterface
}
func (h *MongoHandleCreate) Handle(w http.ResponseWriter, r *http.Request) {
model := h.Helper.NewModel()
err := httphandlers.ParseRequest(r, model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = h.Helper.ValidatePK(model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = h.Helper.QueryInsert(model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
utils.RespJSON(w, http.StatusOK, model)
}

View File

@@ -0,0 +1,41 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewMongoDelete(helper MongoDeleteHelperInterface) *MongoHandleDelete {
return &MongoHandleDelete{Helper: helper}
}
type MongoDeleteHelperInterface interface {
ParseDeleteURLParams(r *http.Request) interface{}
ValidateFields(model interface{}) error
QueryDelete(req interface{}) error
}
type MongoHandleDelete struct {
Helper MongoDeleteHelperInterface
}
func (h *MongoHandleDelete) Handle(w http.ResponseWriter, r *http.Request) {
filter := h.Helper.ParseDeleteURLParams(r)
err := h.Helper.ValidateFields(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = h.Helper.QueryDelete(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,60 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewMongoGetList(helper MongoGetListHelperInterface) *MongoHandleGetList {
return &MongoHandleGetList{Helper: helper}
}
type MongoGetListHelperInterface interface {
NewModel() interface{}
ParseGetListURLParams(r *http.Request, model interface{})
ParseGetListQueryParams(r *http.Request, model interface{})
ValidateStruct(model interface{}) error
QueryCount(filter interface{}) (int64, error)
QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error)
}
type MongoHandleGetList struct {
Helper MongoGetListHelperInterface
}
func (h *MongoHandleGetList) Handle(w http.ResponseWriter, r *http.Request) {
filter := h.Helper.NewModel()
h.Helper.ParseGetListURLParams(r, filter)
h.Helper.ParseGetListQueryParams(r, filter)
err := h.Helper.ValidateStruct(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := queries.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
items, err := h.Helper.QuerySelect(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var total int64
if options.Offset == 0 && options.Limit != 0 {
total, err = h.Helper.QueryCount(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: items,
Total: int(total),
})
}

View File

@@ -0,0 +1,43 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
func NewMongoGetModel(helper MongoGetModelHelperInterface) *MongoHandleGetModel {
return &MongoHandleGetModel{Helper: helper}
}
type MongoGetModelHelperInterface interface {
ParseGetURLParams(r *http.Request) interface{}
ValidatePK(model interface{}) error
Query(filter interface{}) (interface{}, error)
}
type MongoHandleGetModel struct {
Helper MongoGetModelHelperInterface
}
func (h *MongoHandleGetModel) Handle(w http.ResponseWriter, r *http.Request) {
filter := h.Helper.ParseGetURLParams(r)
err := h.Helper.ValidatePK(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
item, err := h.Helper.Query(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
} else if item == nil {
loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound)
return
}
logger.Info().Msgf("%+v", item)
utils.RespJSON(w, http.StatusOK, item)
}

View File

@@ -0,0 +1,53 @@
package controllers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
func NewMongoUpdate(helper MongoUpdateHelperInterface) *MongoHandleUpdate {
return &MongoHandleUpdate{Helper: helper}
}
type MongoUpdateHelperInterface interface {
ParseUpdateURLParams(r *http.Request) interface{}
ValidateFields(model interface{}) error
NewModel() interface{}
ParseRequestBody(r *http.Request, model interface{}) error
QueryUpdate(filter interface{}, model interface{}) error
}
type MongoHandleUpdate struct {
Helper MongoUpdateHelperInterface
}
func (h *MongoHandleUpdate) Handle(w http.ResponseWriter, r *http.Request) {
filter := h.Helper.ParseUpdateURLParams(r)
err := h.Helper.ValidateFields(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
model := h.Helper.NewModel()
err = h.Helper.ParseRequestBody(r, model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = h.Helper.ValidateFields(model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logger.Warn().Err(err).Send()
return
}
err = h.Helper.QueryUpdate(filter, model)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest, loggerdataresp.MongoUpdateErrorCheck) {
return
}
logger.Info().Msgf("%+v", model)
utils.RespJSON(w, http.StatusOK, model)
}

View File

@@ -0,0 +1,68 @@
// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import (
"bytes"
"encoding/json"
"strings"
"text/template"
"github.com/swaggo/swag"
)
var doc = `{}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
}
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
"escape": func(v interface{}) string {
// escape tabs
str := strings.Replace(v.(string), "\t", "\\t", -1)
// replace " with \", and if that results in \\", replace that with \\\"
str = strings.Replace(str, "\"", "\\\"", -1)
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register("swagger", &s{})
}

View File

@@ -0,0 +1,151 @@
module otaupdate
go 1.25
toolchain go1.25.0
require (
github.com/Azure/azure-storage-blob-go v0.15.0
github.com/ClickHouse/clickhouse-go/v2 v2.6.0
github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000
github.com/go-pg/pg/v10 v10.11.1
github.com/gomodule/redigo v1.8.9
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.2.0
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab
github.com/julienschmidt/httprouter v1.3.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0
github.com/swaggo/swag v1.8.8
go.mongodb.org/mongo-driver v1.14.0
google.golang.org/grpc v1.67.3
google.golang.org/protobuf v1.36.1
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1
)
require (
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.58.2 // indirect
github.com/DataDog/appsec-internal-go v1.4.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
github.com/DataDog/go-libddwaf/v2 v2.2.3 // indirect
github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect
github.com/DataDog/sketches-go v1.4.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 // indirect
github.com/apache/thrift v0.16.0 // indirect
github.com/aws/aws-sdk-go v1.44.327 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.2 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/fiskerinc/cloud-services/pkg/can-go v0.0.0-00010101000000-000000000000 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jeremywohl/flatten v1.0.1 // indirect
github.com/jinzhu/copier v0.3.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx v1.2.25 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.25.0 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
github.com/paulmach/orb v0.8.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/rs/zerolog v1.29.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
github.com/swaggo/http-swagger v1.3.3 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twmb/franz-go v1.20.6 // indirect
github.com/twmb/franz-go/pkg/kadm v1.17.2 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.12.0 // indirect
github.com/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xitongsys/parquet-go v1.6.2 // indirect
github.com/xitongsys/parquet-go-source v0.0.0-20220315005136-aec0fe3e777c // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
mellium.im/sasl v0.3.1 // indirect
)
replace (
github.com/fiskerinc/cloud-services/pkg => ../../pkg
github.com/fiskerinc/cloud-services/pkg/can-go => ../../pkg/can-go
)

View File

@@ -0,0 +1,912 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613EeLayJiRAJuKlBGy+m22qWG+WRg=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
github.com/ClickHouse/clickhouse-go/v2 v2.6.0 h1:NmnPY2Cg4hCqS2ZGBep9EWHfQPAco2Vkpwb02VXtWew=
github.com/ClickHouse/clickhouse-go/v2 v2.6.0/go.mod h1:SvXuWqDsiHJE3VAn2+3+nz9W9exOSigyskcs4DAcxJQ=
github.com/DataDog/appsec-internal-go v1.4.0 h1:KFI8ElxkJOgpw+cUm9TXK/jh5EZvRaWM07sXlxGg9Ck=
github.com/DataDog/appsec-internal-go v1.4.0/go.mod h1:ONW8aV6R7Thgb4g0bB9ZQCm+oRgyz5eWiW7XoQ19wIc=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/go-libddwaf/v2 v2.2.3 h1:LpKE8AYhVrEhlmlw6FGD41udtDf7zW/aMdLNbCXpegQ=
github.com/DataDog/go-libddwaf/v2 v2.2.3/go.mod h1:8nX0SYJMB62+fbwYmx5J7zuCGEjiC/RxAo3+AuYJuFE=
github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
github.com/DataDog/go-tuf v1.0.2-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/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o=
github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 h1:m3Ayfs5OcAlIMEdLIQKubBsVLGee4YMUr14+d1256WE=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7/go.mod h1:QIAMbrwsnQZ2ES3G26RubSrDB5SPyzsp9Hts5NJdTrI=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 h1:HGREIyk0QRPt70R69Gm1JFHDgoiyYpCyuGE8E9k/nf0=
github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.44.327 h1:ZS8oO4+7MOBLhkdwIhgtVeDzCeWOlTfKJS7EgggbIEY=
github.com/aws/aws-sdk-go v1.44.327/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.7.1/go.mod h1:L5LuPC1ZgDr2xQS7AmIec/Jlc7O/Y1u2KxJyNVab250=
github.com/aws/aws-sdk-go-v2/config v1.5.0/go.mod h1:RWlPOAW3E3tbtNAqTwvSW54Of/yP3oiZXMI0xfUdjyA=
github.com/aws/aws-sdk-go-v2/credentials v1.3.1/go.mod h1:r0n73xwsIVagq8RsxmZbGSRQFj9As3je72C2WzUIToc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.3.0/go.mod h1:2LAuqPx1I6jNfaGDucWfA2zqQCYCOMCDHiCOciALyNw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.3.2/go.mod h1:qaqQiHSrOUVOfKe6fhgQ6UzhxjwqVW8aHNegd6Ws4w4=
github.com/aws/aws-sdk-go-v2/internal/ini v1.1.1/go.mod h1:Zy8smImhTdOETZqfyn01iNOe0CNggVbPjCajyaz6Gvg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.1/go.mod h1:v33JQ57i2nekYTA70Mb+O18KeH4KqhdqxTJZNK1zdRE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.1/go.mod h1:zceowr5Z1Nh2WVP8bf/3ikB41IZW59E4yIYbg+pC6mw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.1/go.mod h1:6EQZIwNNvHpq/2/QSJnp4+ECvqIy55w95Ofs0ze+nGQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.11.1/go.mod h1:XLAGFrEjbvMCLvAtWLLP32yTv8GpBquCApZEycDLunI=
github.com/aws/aws-sdk-go-v2/service/sso v1.3.1/go.mod h1:J3A3RGUvuCZjvSuZEcOpHDnzZP/sKbhDWV2T1EOzFIM=
github.com/aws/aws-sdk-go-v2/service/sts v1.6.0/go.mod h1:q7o0j7d7HrJk/vr9uUt3BVRASvcU7gYZB9PUgPiByXg=
github.com/aws/smithy-go v1.6.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8=
github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/ebitengine/purego v0.5.2 h1:r2MQEtkGzZ4LRtFZVAg5bjYKnUbxxloaeuGxH0t7qfs=
github.com/ebitengine/purego v0.5.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo=
github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
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.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab h1:K7WJJ5AnrQV/6tEh0Qqs19KLzvsq5V15f9CifKii6aU=
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab/go.mod h1:xr9Svf97gkxlW+ZDxs47vReKp7m9EUzNhEGOLyBHR+8=
github.com/jcmturner/gofork v0.0.0-20180107083740-2aebee971930/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
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/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncw/swift v1.0.52/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.1.6 h1:XbhB8IfG/EsnhNvZtNdLB0GBw92GYEFvKlhaJk9jUgA=
github.com/opencontainers/runc v1.1.6/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
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/paulmach/orb v0.8.0 h1:W5XAt5yNPNnhaMNEf0xNSkBMJ1LzOzdk2MRlB6EN0Vs=
github.com/paulmach/orb v0.8.0/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
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.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.8.2/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/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8=
github.com/testcontainers/testcontainers-go v0.14.0/go.mod h1:hSRGJ1G8Q5Bw2gXgPulJOLlEBaYJHeBSOkQM5JLG+JQ=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twmb/franz-go v1.20.6 h1:TpQTt4QcixJ1cHEmQGPOERvTzo99s8jAutmS7rbSD6w=
github.com/twmb/franz-go v1.20.6/go.mod h1:u+FzH2sInp7b9HNVv2cZN8AxdXy6y/AQ1Bkptu4c0FM=
github.com/twmb/franz-go/pkg/kadm v1.17.2 h1:g5f1sAxnTkYC6G96pV5u715HWhxd66hWaDZUAQ8xHY8=
github.com/twmb/franz-go/pkg/kadm v1.17.2/go.mod h1:ST55zUB+sUS+0y+GcKY/Tf1XxgVilaFpB9I19UubLmU=
github.com/twmb/franz-go/pkg/kmsg v1.12.0 h1:CbatD7ers1KzDNgJqPbKOq0Bz/WLBdsTH75wgzeVaPc=
github.com/twmb/franz-go/pkg/kmsg v1.12.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
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/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xitongsys/parquet-go v1.5.1/go.mod h1:xUxwM8ELydxh4edHGegYq1pA8NnMKDx0K/GyB0o2bww=
github.com/xitongsys/parquet-go v1.6.2 h1:MhCaXii4eqceKPu9BwrjLqyK10oX9WF+xGhwvwbw7xM=
github.com/xitongsys/parquet-go v1.6.2/go.mod h1:IulAQyalCm0rPiZVNnCgm/PCL64X2tdSVGMQ/UeKqWA=
github.com/xitongsys/parquet-go-source v0.0.0-20190524061010-2b72cbee77d5/go.mod h1:xxCx7Wpym/3QCo6JhujJX51dzSXrwmb0oH6FQb39SEA=
github.com/xitongsys/parquet-go-source v0.0.0-20200817004010-026bad9b25d0/go.mod h1:HYhIKsdns7xz80OgkbgJYrtQY7FjHWHKH6cvN7+czGE=
github.com/xitongsys/parquet-go-source v0.0.0-20220315005136-aec0fe3e777c h1:UDtocVeACpnwauljUbeHD9UOjjcvF5kLUHruww7VT9A=
github.com/xitongsys/parquet-go-source v0.0.0-20220315005136-aec0fe3e777c/go.mod h1:qLb2Itmdcp7KPa5KZKvhE9U1q5bYSOmgeOckF/H2rQA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
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/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
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.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/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-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 h1:Sqkq62MxQW/RD+sgZsQuUdHWHyXI4JS5x0lxlxrv2Hk=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1/go.mod h1:6aArYrAHjnuaofJ3lKuSRQbhrBx1LcSpiEYCIScJE5Y=
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-20200227125254-8fa46927fb4f/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.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=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ=
honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -0,0 +1,46 @@
package handlers
import (
"context"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleGetCarsHMIKey godoc
// @Summary Returns HMI session ID from Redis
// @Description If you don't know what this is, don't call it. It's that simple
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} string
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not Found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/hmi_key [get]
func HandleGetCarsHMIKey(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
vin := queryParams.Get("vin")
ok := validator.ValidateVINSimple(vin)
if !ok {
loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
return
}
rd := services.GetRedisV2Client()
res := rd.Client.Get(context.Background(), redis.HMISessionKey(vin))
sessionOrSalt, err := res.Result()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed To Get SessionOrSalt"))
return
}
w.Write([]byte(sessionOrSalt))
}

View File

@@ -0,0 +1,92 @@
package handlers
import (
"net/http"
"net/url"
"otaupdate/services"
"time"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleAPICallsGet godoc
// @Summary Search API calls
// @Description Get API calls filtered by method, user, path and date.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param search query string false "Text search"
// @Param from query string false "Date before requests which client is looking for"
// @Param to query string false "Date after requests which client is looking for"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.APICall}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apicalls [get]
func HandleAPICallsGet(w http.ResponseWriter, r *http.Request) {
filter, err := parseAPICallsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "created_at DESC"
}
csDB := services.GetDB().GetAPICalls()
cs, total, err := csDB.Search(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: cs,
Total: total,
})
}
func parseAPICallsFilter(r *http.Request) (common.APICallsSearch, error) {
qs := r.URL.Query()
from, err := parseTimeFilter(qs, "from")
if err != nil {
return common.APICallsSearch{}, err
}
to, err := parseTimeFilter(qs, "to")
if err != nil {
return common.APICallsSearch{}, err
}
return common.APICallsSearch{
Search: qs.Get("search"),
From: from,
To: to,
}, nil
}
func parseTimeFilter(qs url.Values, pname string) (*time.Time, error) {
stime := qs.Get(pname)
if stime == "" {
return nil, nil
}
t, err := time.Parse(time.RFC3339, stime)
if err != nil {
return nil, errors.WithStack(err)
}
return &t, nil
}

View File

@@ -0,0 +1,108 @@
package handlers_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/stretchr/testify/assert"
)
func TestHandleAPICallsGet(t *testing.T) {
db := services.GetDB()
timeMock := time.Date(2022, 3, 11, 3, 16, 12, 0, time.UTC)
callsList := []common.APICall{
{
ClientID: "jkm@fisker.com",
AccessType: "jwt_token",
Method: "GET",
Endpoint: "/some/path",
CreatedAt: &timeMock,
},
{
ClientID: "3dec092e-d869-46e3-be85-258aed85b2fc",
AccessType: "api_token",
Method: "GET",
Endpoint: "/some/path",
CreatedAt: &timeMock,
},
}
tests := map[string]struct {
urlQ string
callsDB queries.APICallsInterface
expStatus int
expBody string
}{
"correct": {
urlQ: "?from=" + timeMock.Format(time.RFC3339) + "&to=" + timeMock.Format(time.RFC3339) + "&search=text",
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
assert.Equal(t, common.APICallsSearch{
Search: "text",
From: &timeMock,
To: &timeMock,
}, filter)
return callsList, 2, nil
},
},
expStatus: http.StatusOK,
expBody: `{"data":[{"client_id":"jkm@fisker.com","access_type":"jwt_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"},{"client_id":"3dec092e-d869-46e3-be85-258aed85b2fc","access_type":"api_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"}],"total":2}`,
},
"correct_no_params": {
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
return callsList, 2, nil
},
},
expStatus: http.StatusOK,
expBody: `{"data":[{"client_id":"jkm@fisker.com","access_type":"jwt_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"},{"client_id":"3dec092e-d869-46e3-be85-258aed85b2fc","access_type":"api_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"}],"total":2}`,
},
"bad_from": {
urlQ: "?from=kk&to=" + timeMock.Format(time.RFC3339) + "&search=text",
expStatus: http.StatusBadRequest,
expBody: `{"message":"parsing time \"kk\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"kk\" as \"2006\"","error":"Bad Request"}`,
},
"bad_to": {
urlQ: "?from=" + timeMock.Format(time.RFC3339) + "&to=kk&search=text",
expStatus: http.StatusBadRequest,
expBody: `{"message":"parsing time \"kk\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"kk\" as \"2006\"","error":"Bad Request"}`,
},
"bad_limit": {
urlQ: "?limit=-2",
expStatus: http.StatusBadRequest,
expBody: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
"bad_db": {
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
return nil, 0, someErr
},
},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"some err","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
r := th.MakeTestRequest(http.MethodPost, "http://example.com/apicalls"+tt.urlQ, nil)
w := httptest.NewRecorder()
db.SetAPICalls(tt.callsDB)
handlers.HandleAPICallsGet(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,37 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
)
// HandleAPITokenAdd godoc
// @Summary Add API token
// @Description Create API token. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.APIToken true "API token data"
// @Success 200 {object} common.APIToken
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [post]
func HandleAPITokenAdd(w http.ResponseWriter, r *http.Request) {
apiTokenAdd.Handle(w, r)
}
var apiTokenAdd = controllers.NewCreate(&apiTokenCreateHelper{})
type apiTokenCreateHelper struct {
APITokensHelper
}
func (h *apiTokenCreateHelper) QueryInsert(model interface{}) (orm.Result, error) {
return services.GetDB().GetAPITokens().Insert(*model.(*common.APIToken))
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenAdd(t *testing.T) {
mock := mo.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
validData := common.APIToken{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}
tests := []mo.DBHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required. Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", common.APIToken{
Token: "TESTTOKEN",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}`,
},
{
Name: "DB error",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mo.RunDBTests(t, tests, handlers.HandleAPITokenAdd, &mock)
}

View File

@@ -0,0 +1,53 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
"github.com/gorilla/schema"
)
// APITokenDelete godoc
// @Summary Delete API token
// @Description Delete API token. Requires delete permissions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param token query string true "API token"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [delete]
func HandleAPITokenDelete(w http.ResponseWriter, r *http.Request) {
apiTokenDelete.Handle(w, r)
}
var apiTokenDelete = controllers.NewDelete(&apiTokenDeleteHelper{})
type apiTokenDeleteHelper struct {
APITokensHelper
}
func (h *apiTokenDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} {
req := common.APIToken{}
decoder := schema.NewDecoder()
decoder.SetAliasTag("json")
decoder.Decode(&req, r.URL.Query())
return &req
}
func (h *apiTokenDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) {
result := model.(*common.APIToken)
return services.GetDB().GetAPITokens().Delete(result.Token)
}
type APITokenDeleteRequest struct {
Token string `json:"token"`
}

View File

@@ -0,0 +1,64 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenDelete(t *testing.T) {
results := mocks.MockORMResults{
ReturnedRows: 1,
AffectedRows: 1,
}
mock := mocks.MockAPITokens{
DBMockHelper: mocks.DBMockHelper{
ORMResponse: &results,
},
}
services.GetDB().SetAPITokens(&mock)
tests := []mocks.DBHttpTest{
{
Name: "No id",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken", common.APIToken{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?roles=TESTTOKEN", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Good id",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
DBTestCase: mocks.DBTestCase{
ExpectedFilter: &common.APIToken{
Token: "TESTTOKEN",
},
},
},
{
Name: "DB error",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mocks.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mocks.RunDBTests(t, tests, handlers.HandleAPITokenDelete, &mock)
}

View File

@@ -0,0 +1,38 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
)
// APITokenUpdate godoc
// @Summary Update API token
// @Description Update API token. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.APIToken true "API token data"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [put]
func HandleAPITokenUpdate(w http.ResponseWriter, r *http.Request) {
apiTokenUpdate.Handle(w, r)
}
var apiTokenUpdate = controllers.NewUpdate(&apiTokenUpdateHelper{})
type apiTokenUpdateHelper struct {
APITokensHelper
}
func (h *apiTokenUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) {
return services.GetDB().GetAPITokens().Update(model.(*common.APIToken))
}

View File

@@ -0,0 +1,59 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenUpdate(t *testing.T) {
mock := mocks.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
validData := common.APIToken{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}
tests := []mocks.DBHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required. Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Missing PK",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", common.APIToken{
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required","error":"Bad Request"}`,
},
{
Name: "Good data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mocks.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mocks.RunDBTests(t, tests, handlers.HandleAPITokenUpdate, &mock)
}

View File

@@ -0,0 +1,81 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/gorilla/schema"
)
// HandleAPITokensGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param token query string false "API token"
// @Param role query string false "Role"
// @Param description query string false "Description"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.APIToken}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitokens [get]
func HandleAPITokensGetList(w http.ResponseWriter, r *http.Request) {
apiTokensGetList.Handle(w, r)
}
var apiTokensGetList = controllers.NewGetList(&apiTokensGetListHelper{})
type apiTokensGetListHelper struct {
APITokensHelper
}
func (h *apiTokensGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} {
schema := schema.NewDecoder()
filter := common.APIToken{}
schema.SetAliasTag("json")
schema.Decode(&filter, r.URL.Query())
return &filter
}
func (h *apiTokensGetListHelper) QueryCount(filter interface{}) (int, error) {
return services.GetDB().GetAPITokens().Count(filter.(*common.APIToken))
}
func (h *apiTokensGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
return services.GetDB().GetAPITokens().Select(filter.(*common.APIToken), options)
}
type APITokensHelper struct {
controllers.HelperBase
}
func (h *APITokensHelper) NewModel() interface{} {
return &common.APIToken{}
}
func (h *APITokensHelper) HasPK(filter interface{}) bool {
result := filter.(*common.APIToken)
return result.Token != ""
}
func (h *APITokensHelper) ValidatePK(model interface{}) error {
result := model.(*common.APIToken)
err := validator.ValidateField(result.Token, "required")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}

View File

@@ -0,0 +1,125 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokensGetList(t *testing.T) {
mock := mo.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
listData := []common.APIToken{
{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
},
}
expectedResp := `{"data":[{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}],"total":1}`
expectedRespNoTotal := `{"data":[{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}]}`
defaultOrder := "created_at DESC"
tests := []mo.DBHttpTest{
{
Name: "No parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Token parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{
Token: "TESTTOKEN",
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "ECU parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?roles=TEST", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{
Roles: "TEST",
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Paging parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?offset=10&limit=5", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 5,
Offset: 10,
},
MockListResponse: listData,
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 100,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
}
mo.RunDBTests(t, tests, handlers.HandleAPITokensGetList, &mock)
}

View File

@@ -0,0 +1,44 @@
package handlers
import (
"context"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCanSignalListGet godoc
// @Summary Lists of Can Signals used in Feature Table
// @Description Returns a list of can signals Requires API token permission.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CANSignalNameList} "list of cars"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals_list [get]
func HandleCanSignalListGet(w http.ResponseWriter, r *http.Request) {
var canlist []common.CANSignalNameList
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
chCtx := ch.Context(context.Background())
err = conn.Select(chCtx, &canlist, "select Signal_Name from ml_var_list_table where Is_Feature=True")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: canlist})
}

View File

@@ -0,0 +1,48 @@
package handlers_test
import (
"context"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/stretchr/testify/assert"
)
func TestHandleCanSignalListGetList(t *testing.T) {
tests := map[string]struct {
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
conn: &clickhouse.MockConn{ExpectedResult: []common.CANSignalNameList{{"A"}, {"B"}},},
expStatus: http.StatusOK,
expBody: `{"data":[{"signal_name":"A"},{"signal_name":"B"}]}`,
},
"failed_query": {
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.CANSignalNameList","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
ctx := context.Background()
r := httptest.NewRequest(http.MethodGet, "http://example.com/can_signals_list", nil).
WithContext(ctx)
handlers.HandleCanSignalListGet(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,200 @@
package handlers
import (
"context"
"fmt"
"net/http"
"otaupdate/services"
"reflect"
"strconv"
"strings"
"time"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
const (
limit = 100000
)
// HandleCanSignalVINGet godoc
// @Summary Export CAN signals for a specific VIN
// @Description Exports CAN signals for a specific VIN based on specified time range and CAN signals. Requires API token permission.
// @Accept json
// @Produce octet-stream
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param select_all query boolean false "Select All CAN Signals"
// @Param can_signals query []string false "CAN Signals"
// @Param timestamp_start query float64 true "Start time must be included"
// @Param timestamp_end query float64 true "End time must be included"
// @Param vin query string true "VIN must be included in the query"
// @Success 200 {file} CSV file with the specified CAN signals data
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals_export [get]
func HandleCanSignalVINGet(w http.ResponseWriter, r *http.Request) {
filter, err := parseCANSignalFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
filter.Limit = limit
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
if filter.SelectAll {
allCanSignals, err := getListOfAllCanSignals(conn)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
filter.CanSignals = []string{allCanSignals}
}
w.Header().Set("Content-Type", "text/csv")
res := 0
for res == filter.Limit || res == 0 {
res, err = getCanSignalVin(conn, filter, w)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if res == 0 {
return
}
filter.Offset += res
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
func parseCANSignalFilter(r *http.Request) (common.CANSignalQuery, error) {
sch := schema.NewDecoder()
filter := common.CANSignalQuery{}
sch.SetAliasTag("json")
//parse, err := r.URL.Parse(r.URL.String())
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.CANSignalQuery{}, errors.WithStack(err)
}
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.CANSignalQuery{}, errors.WithStack(err)
}
if len(filter.CanSignals) == 0 && !filter.SelectAll {
return common.CANSignalQuery{}, errors.New("either Select All of a list of CAN Signals required")
}
return filter, nil
}
func getCanSignalVin(conn clickhouse.ConnInterface, filter common.CANSignalQuery, w http.ResponseWriter) (int, error) {
chCtx := ch.Context(context.Background())
query := fmt.Sprintf(`SELECT * from (SELECT VIN, Timestamp, %s FROM feature_table WHERE VIN = '%s' AND Timestamp BETWEEN %f AND %f LIMIT %d, %d) ORDER BY Timestamp DESC`,
strings.Join(filter.CanSignals, ", "),
filter.VIN,
filter.TimestampStart,
filter.TimestampEnd,
filter.Offset,
filter.Limit)
rows, err := conn.Query(chCtx, query)
if err != nil {
return 0, errors.WithStack(err)
}
if filter.Offset == 0 {
headerLine := "VIN,Timestamp"
for _, signal := range filter.CanSignals {
headerLine += "," + signal
}
headerLine += "\n"
_, err = w.Write([]byte(headerLine))
if err != nil {
return 0, errors.WithStack(err)
}
}
columnTypes := rows.ColumnTypes()
row := make([]interface{}, len(columnTypes))
for i, cType := range columnTypes {
kind := cType.ScanType().Kind()
scanName := cType.ScanType().Name()
if kind == reflect.String || strings.Contains(scanName, "string") {
row[i] = new(string)
} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
row[i] = new(float64)
} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
row[i] = new(int64)
} else if strings.Contains(scanName, "Time") {
row[i] = new(time.Time)
} else {
row[i] = new(string)
}
}
var canSignals []string
if len(filter.CanSignals) == 1 {
canSignals = strings.Split(filter.CanSignals[0], ",")
}
rowCounter := 0
defer rows.Close()
for rows.Next() {
rowCounter++
err := rows.Scan(row...)
if err != nil {
return 0, errors.WithStack(err)
}
vin := *row[0].(*string)
timestamp := *row[1].(*time.Time)
dataline := vin + "," + timestamp.Format("2006-01-02 15:04:05.000000")
for i := 2; i < len(canSignals)+2; i++ {
val := "0"
if v, ok := row[i].(*float64); ok {
val = strconv.FormatFloat(*v, 'f', -1, 64)
}
dataline += "," + val
}
w.Write([]byte(dataline + "\n"))
}
w.(http.Flusher).Flush()
return rowCounter, nil
}
func getListOfAllCanSignals(conn clickhouse.ConnInterface) (string, error) {
var allCanSignals []string
var canlist []common.CANSignalNameList
chCtx := ch.Context(context.Background())
err := conn.Select(chCtx, &canlist, "select Signal_Name from ml_var_list_table where Is_Feature=True")
if err != nil {
return "", err
}
for _, signalName := range canlist {
allCanSignals = append(allCanSignals, signalName.Signal_Name)
}
return strings.Join(allCanSignals, ","), nil
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,83 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
)
var errInvalidVIN = errors.New("invalid VIN entered")
// CarConfigurationUpdate godoc
// @Summary Send VOD and CDS to car
// @Description Get all sap codes for a car, transform them to VOD and CDS, then send it to the car
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN to send configuration update"
// @Param forced query bool false "Force configuration update"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car_config/{vin} [post]
func CarConfigurationUpdate(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
queryParams := r.URL.Query()
forced, _ := strconv.ParseBool(queryParams.Get("forced"))
rds := services.RedisClientPool().GetFromPool()
defer rds.Close()
cs := services.GetVehicleConfig()
db := services.GetDB()
sms := services.GetSMSClient()
alDB := services.GetDB().GetActionLog()
actionLog := actionlogger.ActionLog{
VIN: vin,
Action: actionlogger.CarConfigurationUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/car_configuration_update.go",
}
go func() {
err := alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside CarConfigurationUpdate")
}
}()
username := httphandlers.GetClientID(r)
manifestSender := manifestsender.NewTBOXManifestSender(rds, cs, db, sms, nil)
input := manifestsender.ProcessConfigUpdateStruct{
VIN: vin,
Username: username,
SendToCar: true,
DontCreateDatabaseEntry: false,
Forced: forced,
}
// Process and send as this is a new manifest that needs a car update
_, err := manifestSender.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData())
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Sent",
})
}

View File

@@ -0,0 +1,77 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
)
// HandleCarSoftwareInformation godoc
// @Summary Get overall software version from a car and its ecu version information
// @Description Get overall software version from a car and its ecu version information
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} CarSoftwareInformationResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car/software_information [get]
func HandleCarSoftwareInformation(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
vin := qs.Get("vin")
information, err := carSoftwareInformation(vin)
if loggerdataresp.BadDataError(err) {
return
}
utils.RespJSON(w, http.StatusOK, information)
}
func carSoftwareInformation(vin string) (info CarSoftwareInformationResponse, err error) {
info.VIN = vin
var ecus []common.CarECU
ecus, err = services.GetDB().GetCars().GetCarECUs(common.CarECUFilter{VIN: vin, Unique: true}, nil)
if err != nil {
return
}
info.ECUVersionInformation = convertCommonCarECUToECUVersionInformationArray(ecus)
var carVersion common.CarPKCOSVersion
carVersion, err = services.GetDB().GetCars().GetSoftwareVersion(vin)
info.OSVersion = carVersion.OSVersion
info.SUMsVersion = carVersion.SumsVersion
return
}
type CarSoftwareInformationResponse struct {
VIN string `json:"vin"`
SUMsVersion string `json:"sum_version"`
OSVersion string `json:"os_version"`
ECUVersionInformation []ECUVersionInformation `json:"ecu_version_information"`
}
type ECUVersionInformation struct {
ECU string `json:"ecu"`
SoftwareVersion string `json:"software_version"`
HardwareVersion string `json:"hardware_version"`
}
func convertCommonCarECUToECUVersionInformationArray(input []common.CarECU) (output []ECUVersionInformation) {
output = make([]ECUVersionInformation, len(input))
for index, ecuInfo := range input {
output[index] = convertCommonCarECUToECUVersionInformation(ecuInfo)
}
// Probably want to sort the ECU's alphabetically
return output
}
func convertCommonCarECUToECUVersionInformation(input common.CarECU) (output ECUVersionInformation) {
output.ECU = input.ECU
output.SoftwareVersion = input.Version
output.HardwareVersion = input.HWVersion
return output
}

View File

@@ -0,0 +1,79 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
)
// HandleCarSoftwareInformationV2 godoc
// @Summary Get overall software version from a car and its ecu version information
// @Description Get overall software version from a car and its ecu version information
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} CarSoftwareInformationResponseV2
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car/software_information/v2 [get]
func HandleCarSoftwareInformationV2(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
vin := qs.Get("vin")
information, err := carSoftwareInformationV2(vin)
if loggerdataresp.BadDataError(err) {
return
}
utils.RespJSON(w, http.StatusOK, information)
}
func carSoftwareInformationV2(vin string) (info CarSoftwareInformationResponseV2, err error) {
info.VIN = vin
var ecus []common.CarECU
ecus, err = services.GetDB().GetCars().GetCarECUs(common.CarECUFilter{VIN: vin, Unique: true, ECUs: OVLoopECUList}, nil)
if err != nil {
return
}
info.ECUVersionInformation = convertCommonCarECUToECUVersionInformationArrayV2(ecus)
var carVersion common.CarPKCOSVersion
carVersion, err = services.GetDB().GetCars().GetSoftwareVersion(vin)
info.OSVersion = carVersion.OSVersion
info.SUMsVersion = carVersion.SumsVersion
return
}
type CarSoftwareInformationResponseV2 struct {
VIN string `json:"vin"`
SUMsVersion string `json:"sum_version"`
OSVersion string `json:"os_version"`
ECUVersionInformation []ECUVersionInformationV2 `json:"ecu_version_information"`
}
type ECUVersionInformationV2 struct {
ECU string `json:"ecu"`
SupplierSWVersion string `json:"supplier_sw_version,omitempty" validate:"max=1024"`
BootLoaderVersion string `json:"boot_loader_version,omitempty" validate:"max=1024"`
}
func convertCommonCarECUToECUVersionInformationArrayV2(input []common.CarECU) (output []ECUVersionInformationV2) {
output = make([]ECUVersionInformationV2, len(input))
for index, ecuInfo := range input {
output[index] = convertCommonCarECUToECUVersionInformationV2(ecuInfo)
}
// Probably want to sort the ECU's alphabetically
return output
}
func convertCommonCarECUToECUVersionInformationV2(input common.CarECU) (output ECUVersionInformationV2) {
output.ECU = input.ECU
output.SupplierSWVersion = input.SupplierSWVersion
output.BootLoaderVersion = input.BootLoaderVersion
return output
}
var OVLoopECUList = []string{"ACU","ADAS","AMP","BCM","BMS","CIM","CMRR_FL","CMRR_FR","CMRR_RL","CMRR_RR","DSMC","ECC","EPS1","EPS2","ESP","FCM","GW","iBooster","ICC","MCU","MCU_F","MCU_R","MRR","OBC","OHC","PKC","PLGM","PSM","PVIU","PWC","PWC_R","TRM","VCU","VSP"}

View File

@@ -0,0 +1,153 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"slices"
"strings"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
)
// FIX BUG: current system allows for vins to be tmobile activated,
// but because we don't have them, we don't get them in response list
// HandleCarsAllowedAccess godoc
// @Summary Fetch the list of VINs that can connect to fisker cloud
// @Description Note: when a VIN is added or removed, changes will take time to propagate to the network filters
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Success 200 {object} HandleCarsAllowedAccessResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/allowed_access [get]
func HandleCarsAllowedAccess(w http.ResponseWriter, r *http.Request) {
var err error
res := HandleCarsAllowedAccessResponse{}
res.AllowedVINs, err = fetchVINList()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
res.AllowAll = len(res.AllowedVINs) == 0
err = json.NewEncoder(w).Encode(res)
loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError)
}
type HandleCarsAllowedAccessResponse struct {
AllowAll bool `json:"allow_all"`
AllowedVINs []string `json:"allowed_vins"`
}
func fetchVINList() (vins []string, err error) {
vins, err = services.GetDB().GetCars().GetWhiteListCars()
return
}
// HandleCarsAllowedAccess godoc
// @Summary Check if a single VIN has cloud and t-mobile access
// @Description
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param VINList body HandleAccessCheckInput true "List of VINs to check"
// @Success 200 {object} HandleAccessCheckResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/allowed_access [post]
func HandleCarAllowedAccess(w http.ResponseWriter, r *http.Request) {
vinput := HandleAccessCheckInput{}
err := json.NewDecoder(r.Body).Decode(&vinput)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
results, err := CarAllowedAccess(vinput.VINs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
res := HandleAccessCheckResponse{
VINStatuses: results,
}
err = json.NewEncoder(w).Encode(res)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
}
func CarAllowedAccess(vins []string) (results []AllowedStruct, err error) {
var allowedVINs []string
allowedVINs, err = fetchVINList()
if err != nil {
return
}
slices.Sort(allowedVINs)
carDB := services.GetDB().GetCars()
tmobileClient, err := tmobile.NewTMobileMiniClient()
if err != nil {
return
}
results = make([]AllowedStruct, 0, len(vins))
for _, v := range vins {
temp := AllowedStruct{}
temp.VIN = v
temp.CloudAccess = slices.Contains(allowedVINs, v)
// Slow but I dont give a damn
var car *common.Car
car, err = carDB.SelectByVIN(v)
if err != nil {
logger.Err(err).Msg("failed to select car by vin")
} else if car.ICCID != "" && car.ICCID != "N/A" {
car.ICCID = strings.TrimSuffix(car.ICCID, "F")
var details *tmobile.DeviceDetailsResponse
input := tmobile.DeviceDetailsRequest{
ICCID: car.ICCID,
}
details, err = tmobileClient.DeviceDetails(context.Background(), &input)
if err != nil {
break
}
temp.TMobileStatus = string(details.Status)
} else {
temp.TMobileStatus = "INVALID ICCID"
}
results = append(results, temp)
}
return
}
type HandleAccessCheckInput struct {
VINs []string `json:"vins"`
}
type HandleAccessCheckResponse struct {
VINStatuses []AllowedStruct `json:"vin_statuses"`
}
type AllowedStruct struct {
VIN string `json:"vin"`
CloudAccess bool `json:"cloud_access"`
TMobileStatus string `json:"tmobile_access"`
}

View File

@@ -0,0 +1,71 @@
package handlers
import (
"net/http"
"strconv"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleGetCarsByManifest godoc
// @Summary Get cars by manifest
// @Description Returns list of cars selected by manifest id
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Param manifest_id path int true "Manifest ID"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.Car} "list of cars"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /manifests/{manifest_id}/vehicles [get]
func HandleGetCarsByManifest(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
idS := params.ByName("manifest_id")
id, err := strconv.Atoi(idS)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := queries.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
manDB := services.GetDB().GetUpdateManifests()
man := common.UpdateManifest{ID: int64(id)}
err = manDB.Load(&man)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
carsDB := services.GetDB().GetCars()
cars, err := carsDB.CarsByManifest(man, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := carsDB.CountCarsByManifest(man)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if cars == nil {
cars = []common.Car{}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: cars,
Total: total,
})
}

View File

@@ -0,0 +1,116 @@
package handlers_test
import (
"context"
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
m "github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/julienschmidt/httprouter"
)
func TestHandleGetCarsByManifest(t *testing.T) {
mock := mo.MockCars{}
mockMan := mo.MockUpdateManifests{
LoadResponse: &m.UpdateManifest{},
}
services.GetDB().SetCars(&mock)
services.GetDB().SetUpdateManifests(&mockMan)
expectedCar := `{"vin":"1G1FP87S3GN100062","country":"US","year":2022,"model":"Ocean","trim":"Base","powertrain":"MD23","restraint":"None","body_type":"truck"}`
expectedResp := fmt.Sprintf(`{"data":[%s],"total":1}`, expectedCar)
expectedEmptyResp := `{"data":[]}`
listData := []m.Car{
{
VIN: "1G1FP87S3GN100062",
Model: "Ocean",
Year: 2022,
Trim: "Base",
Country: "US",
Powertrain: "MD23",
Restraint: "None",
BodyType: "truck",
},
}
p := httprouter.Params{
{"manifest_id", "8"},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
invalidP := httprouter.Params{
{"manifest_id", "o"},
}
invalidCtx := context.WithValue(context.Background(), httprouter.ParamsKey, invalidP)
tests := []mo.DBHttpTest{
{
Name: "Success",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Empty",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedEmptyResp,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: nil,
},
},
{
Name: "Invalid param",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(invalidCtx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.Atoi: parsing \"o\": invalid syntax","error":"Bad Request"}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles?limit=-100", nil).
WithContext(ctx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles?limit=1000", nil).
WithContext(ctx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
mo.RunDBTests(t, tests, handlers.HandleGetCarsByManifest, &mock)
}

View File

@@ -0,0 +1,104 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
"github.com/fiskerinc/cloud-services/pkg/vindecoder"
"github.com/pkg/errors"
)
// HandleCarChangeAccess godoc
// @Summary change access for a vin to connect to fisker cloud
// @Description Note: when a VIN is added or removed, changes will take time to propagate to the network filters
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body CarChangeAccessInput true "car change access data"
// @Success 200 {object} HandleCarsAllowedAccessResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/change_access [post]
func HandleCarChangeAccess(w http.ResponseWriter, r *http.Request) {
ccai := CarChangeAccessInput{}
err := json.NewDecoder(r.Body).Decode(&ccai)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
failedVINs := []string{}
for _, vin := range ccai.VINs {
ok := vindecoder.ValidateVINSimple(vin)
if !ok {
failedVINs = append(failedVINs, vin)
}
}
if len(failedVINs) > 0 {
errors.New(fmt.Sprintf("VINS Invalid, no changes made: %+v", failedVINs))
loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest)
return
}
err = CarChangeAccess(ccai)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
}
func CarChangeAccess(ccai CarChangeAccessInput) (err error) {
// Add/Remove from the database
if ccai.AllowCloudAccess != nil {
if *ccai.AllowCloudAccess {
err = services.GetDB().GetCars().WhitelistCars(ccai.VINs, ccai.Source)
if err != nil {
return err
}
} else {
err = services.GetDB().GetCars().BlacklistCars(ccai.VINs)
if err != nil {
return err
}
}
}
if ccai.AllowTMobileAccess != nil {
var miniClient *tmobile.TMobileMiniClient
miniClient, err = tmobile.NewTMobileMiniClient()
if err != nil {
return errors.WithMessage(err, "Failed to Create TMOible Client")
}
var iccids []string
iccids, err = services.GetDB().GetCars().GetICCIDs(ccai.VINs)
if err != nil {
return errors.WithMessage(err, "Failed to get ICCIDs")
}
tmobileInput := tmobile.ChangeDeviceActivation{
ICCIDs: iccids,
Enabled: *ccai.AllowTMobileAccess,
}
// Add/Remove from TMobile
// TODO when T-Mobile gets back about access
err = miniClient.ChangeDeviceStatus(context.Background(), tmobileInput)
if err != nil {
return
}
}
return
}
type CarChangeAccessInput struct {
VINs []string `json:"vins"` // List of VINs that will be changed
AllowCloudAccess *bool `json:"allow_cloud_access,omitempty"` // Cars can connect through gateway
AllowTMobileAccess *bool `json:"allow_tmobile_access,omitempty"` // Cars get tmobile access
Source string `json:"source,omitempty"`
}

View File

@@ -0,0 +1,102 @@
package handlers
import (
"fmt"
"io"
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/logger"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesAdd godoc
// @Summary Add car updates
// @Description Create car updates assigning manifest package to cars, and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body usecase_helpers.JSONCarUpdatesRequest true "Update manifest or package id and, car ids"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CarUpdate} "Created car updates result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate [post]
func HandleCarUpdatesAdd(w http.ResponseWriter, r *http.Request) {
var req uhelpers.JSONCarUpdatesRequest
var ups []common.CarUpdate
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
username := httphandlers.GetClientID(r)
manifest, err := getManifest(req.UpdateManifestID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
d := services.GetDB().GetCarUpdates()
k := services.GetKafkaProducer()
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: "",
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/car_update_add.go",
Description: fmt.Sprintf("UpdateManifest: %d", req.UpdateManifestID),
}
for _, vin := range req.VINs {
actionLog.VIN = vin
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdatesAdd")
}
}
}()
notifier := uhelpers.NewUpdateNotifier(d, k)
ups, err = notifier.Send(req.VINs, manifest, username)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
for _, vin := range req.VINs {
// Notify car user of in progress update through FOA API
fs := services.GetFoaService()
foaResp, err := fs.OtaUpdateStatus(vin, &common.CarUpdate{UpdateManifestID: req.UpdateManifestID}, &common.CarUpdateProgress{Status: carupdatestatus.Pending})
if err != nil || (foaResp != nil && foaResp.StatusCode != http.StatusOK) {
bodyBytes, _ := io.ReadAll(foaResp.Body)
bodyString := string(bodyBytes)
logger.Err(err).Msgf("notify FOA for update manifest %d pending state %s for %s failed with http status %d and message %s", req.UpdateManifestID, carupdatestatus.Pending, vin, foaResp.StatusCode, bodyString)
err = nil
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: ups,
})
}
func getManifest(manifestID int64) (common.UpdateManifest, error) {
um := services.GetDB().GetUpdateManifests()
manifest := common.UpdateManifest{ID: manifestID}
err := um.Load(&manifest)
return manifest, err
}

View File

@@ -0,0 +1,317 @@
package handlers_test
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
dbm "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/kafka"
km "github.com/fiskerinc/cloud-services/pkg/kafka/mock"
"github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils/whereami"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/go-pg/pg/v10"
"google.golang.org/protobuf/proto"
)
func TestCarUpdateAdd(t *testing.T) {
whereami.SetService(whereami.OTA)
redis.MockRedisConnection()
mockDB := dbm.MockCarUpdates{}
mockKafka := km.KafkaMock{}
mockRedis := rm.MockRedis{}
vin := "1G1FP87S3GN100062"
mockFoa := FoaServiceMock{}
services.SetFoaService(&mockFoa)
services.GetDB().SetCarUpdates(&mockDB)
services.SetKafkaProducer(&mockKafka)
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
otaUpdateKey := common.Service.Key(vin)
attendentTopic := kafka.AttendantServiceGRPCKafka
updateMsg := &kafka_grpc.GRPC_AttendantPayload_UpdateManifest{
UpdateManifest: &kafka_grpc.UpdateManifest{
CarUpdateId: 1,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: updateMsg,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
validMessage := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(binaryPayload))
standardManifest := common.UpdateManifest{
ID: 100,
Name: "TEST",
Version: "10000",
Type: "standard",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ICC",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "ICC",
SelfDownload: true,
Files: []*common.UpdateManifestFile{
{
FileID: "AAAAAAA",
URL: "http://download.com/file1.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
},
},
},
{
ECU: "ECU",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "D",
Files: []*common.UpdateManifestFile{
{
FileID: "BBBBBBB",
URL: "http://download.com/file2.bin",
FileSize: 2000,
Checksum: "BBBBBBB",
WriteRegionID: 300,
WriteRegion: common.MemoryRegion{
Offset: 301,
Length: 302,
},
EraseRegionID: 400,
EraseRegion: &common.MemoryRegion{
Offset: 401,
Length: 402,
},
},
},
},
},
}
forcedManifest := standardManifest
forcedManifest.Type = "forced"
tests := []testrunner.TestCase{
{
Name: "Bad car ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VINs required","error":"Bad Request"}`,
},
},
{
Name: "No data",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. VINs required","error":"Bad Request"}`,
},
},
{
Name: "Bad package ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
VINs: []string{"NONEXISTENT", "NONEXISTENT2"},
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. VINs[0] 'NONEXISTENT' invalid. VINs[1] 'NONEXISTENT2' invalid","error":"Bad Request"}`,
},
},
{
Name: "Good data standard manifest id",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"status":"pending","username":"testusername","UpdateSource":"OTA"}]}`,
},
DBTestCase: &dbm.DBTestCase{
SetupMockResponse: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: &standardManifest,
})
},
},
RedisTestCase: &rm.RedisTestCase{},
KafkaTestCase: &km.KafkaTestCase{
ExpectedProduceMessages: map[string]map[string]interface{}{
attendentTopic: {
otaUpdateKey: validMessage,
},
},
},
},
{
Name: "Case where manifest id does not exist",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
Setup: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: nil,
DBMockHelper: dbm.DBMockHelper{
Error: pg.ErrNoRows,
},
})
},
},
},
{
Name: "Good data forced manifest id",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"status":"pending","username":"testusername","UpdateSource":"OTA"}]}`,
},
DBTestCase: &dbm.DBTestCase{
SetupMockResponse: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: &forcedManifest,
})
},
},
RedisTestCase: &rm.RedisTestCase{},
KafkaTestCase: &km.KafkaTestCase{
ExpectedProduceMessages: map[string]map[string]interface{}{
attendentTopic: {
otaUpdateKey: validMessage,
},
},
},
},
{
Name: "Error",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
},
DBTestCase: &dbm.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
for _, test := range tests {
mockRedis.Reset()
mockKafka.Reset()
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Setup(&mockKafka)
}
if test.HttpTestCase != nil {
if test.HttpTestCase.Setup != nil {
test.HttpTestCase.Setup()
}
ctx := context.WithValue(test.HttpTestCase.Request.Context(), httphandlers.ClientIDContextKey, "testusername")
test.HttpTestCase.Request = test.HttpTestCase.Request.WithContext(ctx)
w := test.HttpTestCase.Test(handlers.HandleCarUpdatesAdd)
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Validate(t, test.Name, &mockKafka)
}
}
}
func TestJSONCarUpdatesRequestValidation(t *testing.T) {
request := uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{},
}
err := validator.ValidateStruct(request)
if err == nil {
t.Errorf(th.TestErrorTemplate, "Validate VINs count", nil, err)
} else {
_, msg := validator.GetValidationErrorMsg(err)
expected := "VINs less than 1"
if msg != expected {
t.Errorf(th.TestErrorTemplate, "Validate VINs count", expected, msg)
}
}
request.VINs = []string{"1G1FP87S3GN100062"}
err = validator.ValidateStruct(request)
if err != nil {
t.Errorf(th.TestErrorTemplate, "Validate Good VIN", nil, err)
}
request.VINs = []string{"1G1FP87S3GN10006I"}
err = validator.ValidateStruct(request)
if err == nil {
t.Errorf(th.TestErrorTemplate, "Validate Bad VIN", nil, err)
} else {
_, msg := validator.GetValidationErrorMsg(err)
expected := "VINs[0] '1G1FP87S3GN10006I' invalid"
if msg != expected {
t.Errorf(th.TestErrorTemplate, "Validate Bad VIN", expected, msg)
}
}
}
type FoaServiceMock struct{}
func (f *FoaServiceMock) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
}

View File

@@ -0,0 +1,142 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateCancel godoc
// @Summary Cancel car update
// @Description Cancels car update and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to cancel"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/cancel [post]
func HandleCarUpdateCancel(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
carupdates := services.GetDB().GetCarUpdates()
cu, err := carupdates.SelectByID(req.CarUpdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: cu.VIN,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_cancel.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateCancel")
}
}()
// If this update was from aftersales, then we just mark as canceled
if cu.UpdateSource == common.UPDATE_SOURCE_AFTERSALES {
err = cancelUpdateAftersales(req, cu, carupdates)
} else {
err = cancelUpdateOTA(req, cu, carupdates)
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func cancelUpdateAftersales(req common.CarUpdateRequest, cu *common.CarUpdate, carupdates queries.CarUpdatesInterface) (err error) {
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.ManifestCanceled,
}
_, err = carupdates.UpdateStatus(&current)
if err != nil {
return
}
return
}
func cancelUpdateOTA(req common.CarUpdateRequest, cu *common.CarUpdate, carupdates queries.CarUpdatesInterface) (err error) {
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
}
_, err = carupdates.UpdateStatus(&current)
if err != nil {
return
}
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestCancel,
Data: req,
})
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,138 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
r "github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateCancel(t *testing.T) {
r.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "invalid update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/xxxx/cancel", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "good request",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateCancel, "/carupdate/:id/cancel")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,41 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/urlhelper"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdateDelete godoc
// @Summary Delete car update
// @Description Delete car update. Requires delete permissions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id query int true "Car update id"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate [delete]
func HandleCarUpdateDelete(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
carupdate := common.CarUpdate{
ID: urlhelper.GetQueryInt64(qs, "id"),
}
_, err := services.GetDB().GetCarUpdates().Delete(&carupdate)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,38 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCarUpdateDelete(t *testing.T) {
services.GetDB().SetCarUpdates(&mocks.MockCarUpdates{})
tests := []th.BasicHttpTest{
{
Name: "No id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id required","error":"Bad Request"}`,
},
{
Name: "Zero id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=0", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id required","error":"Bad Request"}`,
},
{
Name: "Good id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdateDelete)
}

View File

@@ -0,0 +1,135 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateDeploy godoc
// @Summary Deploy car update
// @Description Deploys car update and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to deploy"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/deploy [post]
func HandleCarUpdateDeploy(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
carupdates := services.GetDB().GetCarUpdates()
cu, err := carupdates.SelectByID(req.CarUpdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: cu.VIN,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_deploy.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateDeploy")
}
}()
if !isRedeployAvailable(cu.Status) {
utils.RespJSON(w, http.StatusUnprocessableEntity, common.JSONMessage{
Message: fmt.Sprintf("Unable to redeploy, CarUpdate is currently %s", cu.Status),
})
return
}
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.Pending,
}
_, err = carupdates.UpdateStatus(&current)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestInstall,
Data: req,
})
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.Pending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func isRedeployAvailable(status string) bool {
switch status {
case carupdatestatus.ManifestSucceeded, carupdatestatus.ManifestCanceled,
carupdatestatus.ManifestError, carupdatestatus.ManifestCancelPending,
carupdatestatus.RollbackSucceeded, carupdatestatus.ManifestRejected,
carupdatestatus.RollbackFailed, carupdatestatus.CleanupSucceeded:
return true
default:
return false
}
}

View File

@@ -0,0 +1,156 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateDeploy(t *testing.T) {
redis.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
Status: carupdatestatus.ManifestSucceeded,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "invalid update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/xxxx/deploy", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "invalid status",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusUnprocessableEntity,
ExpectedResponse: `{"message":"Unable to redeploy, CarUpdate is currently pending"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
Status: carupdatestatus.Pending,
},
},
},
{
Name: "good request",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_install","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_install","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateDeploy, "/carupdate/:id/deploy")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,133 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateVehicleCancel godoc
// @Summary Cancel car update on vehicle
// @Description Cancel a rogue car update on a vehicle that's not found on cloud.
// A car update may not be found in cloud following physical vehicle maintenance.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to cancel"
// @Param vin query string true "VIN of vehicle to send to"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/vehicle-cancel [post]
func HandleCarUpdateVehicleCancel(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
vin := r.URL.Query().Get("vin")
ok := validator.ValidateVINSimple(vin)
if !ok {
loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: vin,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_vehicle_cancel.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateVehicleCancel")
}
}()
cu := &common.CarUpdate{
ID: id,
VIN: vin,
}
carupdates := services.GetDB().GetCarUpdates()
_, err = carupdates.SelectByID(req.CarUpdateID)
if err == nil {
err = fmt.Errorf("car update was found in cloud, use /carupdate/%d/cancel", req.CarUpdateID)
} else {
err = nil
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
err = sendCancelUpdate(req, cu)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func sendCancelUpdate(req common.CarUpdateRequest, cu *common.CarUpdate) (err error) {
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestCancel,
Data: req,
})
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,151 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
r "github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateVehicleCancel(t *testing.T) {
r.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "car update known to cloud",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"car update was found in cloud, use /carupdate/1000/cancel","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/xxxx/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "car update unknown to cloud",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateVehicleCancel, "/carupdate/:id/vehicle-cancel")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/urlhelper"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesGet godoc
// @Summary Search car updates
// @Description Get car updates filtered by id, car id, and update package id
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id query int false "CarUpdate id"
// @Param vin query string false "Car VIN"
// @Param manifest_id query int false "Update manifest id"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CarUpdate}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdates [get]
func HandleCarUpdatesGet(w http.ResponseWriter, r *http.Request) {
var total int
cu := services.GetDB().GetCarUpdates()
filter, err := parseCarsUpdateFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "id DESC"
}
ups, err := cu.Select(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if options.Offset == 0 && filter.ID == 0 {
total, err = cu.Count(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: ups,
Total: total,
})
}
func parseCarsUpdateFilter(r *http.Request) (*common.CarUpdate, error) {
qs := r.URL.Query()
filter := common.CarUpdate{
ID: urlhelper.GetQueryInt64(qs, "id"),
VIN: qs.Get("vin"),
UpdateManifestID: urlhelper.GetQueryInt64(qs, "manifest_id"),
}
err := validator.ValidateNonRequired(filter)
return &filter, err
}

View File

@@ -0,0 +1,143 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
m "github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCarUpdatesGet(t *testing.T) {
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
expectedCarUpdate := `{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"updatemanifest":{"name":"TEST","version":"1.1","description":"description","release_notes":"http://releasenotes.com","rollback":false,"type":"forced","country":"US","powertrain":"MD23","restraint":"None","model":"Ocean","trim":"Sport","year":2022,"body_type":"truck"},"UpdateSource":"OTA"}`
expectedResp := fmt.Sprintf(`{"data":[%s],"total":1}`, expectedCarUpdate)
expectedRespNoTotal := fmt.Sprintf(`{"data":[%s]}`, expectedCarUpdate)
defaultOrder := "id DESC"
listData := []m.CarUpdate{
{
ID: 1,
VIN: "1G1FP87S3GN100062",
UpdateManifestID: 1,
UpdateManifest: &m.UpdateManifest{
Name: "TEST",
Description: "description",
Version: "1.1",
ReleaseNotes: "http://releasenotes.com",
RollbackEnabled: false,
Type: "forced",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
},
UpdateSource: "OTA",
},
}
tests := []mo.DBHttpTest{
{
Name: "No parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Id parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{
ID: 1,
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "VIN and UpdateManifestID parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?vin=1G1FP87S3GN100062&manifest_id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{
VIN: "1G1FP87S3GN100062",
UpdateManifestID: 1,
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Paging parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?offset=10&limit=5", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 5,
Offset: 10,
},
MockListResponse: listData,
},
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
mo.RunDBTests(t, tests, handlers.HandleCarUpdatesGet, &mock)
}

View File

@@ -0,0 +1,76 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesLog godoc
// @Summary Gets log of car update statuses
// @Description Returns array of car update statuses
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param carupdateid query int true "car update id"
// @Success 200 {object} CarUpdateStatuses "Car update statuses"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdateslog [get]
func HandleCarUpdatesLog(w http.ResponseWriter, r *http.Request) {
var total int
carupdateID, err := validateCarUpdatesLog(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "id DESC"
}
cu := services.GetDB().GetCarUpdates()
statuses, err := cu.GetUpdateStatuses(carupdateID, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if options.Offset == 0 {
total, err = cu.CountUpdateStatuses(carupdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: statuses,
Total: total,
})
}
func validateCarUpdatesLog(r *http.Request) (int64, error) {
qs := r.URL.Query()
qsID := qs.Get("carupdateid")
if qsID == "" {
return 0, fmt.Errorf("car update id required")
}
id, err := strconv.ParseInt(qsID, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid id %s", qsID)
}
return id, nil
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"net/http"
"testing"
"time"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleCarUpdatesLog(t *testing.T) {
date := time.Date(2021, time.November, 10, 23, 0, 0, 0, time.UTC)
mock := mo.MockCarUpdates{
SelectCarUpdateStatusesResponse: []common.CarUpdateStatus{
{
ID: 1000,
CarUpdateID: 100,
Status: "pending",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &date,
UpdatedAt: &date,
},
},
},
}
services.GetDB().SetCarUpdates(&mock)
//year int, month Month, day, hour, min, sec, nsec int, loc *Location
tests := []th.BasicHttpTest{
{
Name: "Missing query",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdateslog", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"car update id required","error":"Bad Request"}`,
},
{
Name: "Bad car update ids",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdateslog?carupdateid=XXXXXXXXX", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"invalid id XXXXXXXXX","error":"Bad Request"}`,
},
{
Name: "Good request",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdateslog?carupdateid=100", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1000,"carupdate_id":100,"status":"pending","error_code":0,"created":"2021-11-10T23:00:00Z","updated":"2021-11-10T23:00:00Z"}],"total":1}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdatesLog)
}

View File

@@ -0,0 +1,109 @@
package handlers
import (
"errors"
"fmt"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
"github.com/go-pg/pg/v10"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesStatuses godoc
// @Summary Gets statuses for car update by car update ids
// @Description Returns array of car update statuses
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param carupdateids query string true "Comma delimited list of car update ids"
// @Success 200 {object} CarUpdateStatuses "Car update statuses"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdatesstatuses [get]
func HandleCarUpdatesStatuses(w http.ResponseWriter, r *http.Request) {
data, err := validateCarUpdatesStatuses(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
keys := make([]string, len(data))
// These should be of type common.CarUpdateProgress
statuses := make([]interface{}, len(data))
for i, carupdateID := range data {
keys[i] = redis.CarUpdateStatusHashKey(carupdateID)
statuses[i] = &common.CarUpdateProgress{}
}
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
err = conn.GetObjectsMulti(keys, statuses)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, CarUpdateStatuses{
Statuses: statuses,
})
}
// I am not a fan of doing this, going from Redis and then checking the database
func filterCarUpdatedStatuses(statuses []interface{}) (filtered []interface{}) {
filtered = make([]interface{}, 0)
for x := range statuses {
status, ok := statuses[x].(common.CarUpdateProgress)
if !ok {
continue
}
// LOAD does not search on CarUpdateID
manifest := &common.CarUpdate{ID: status.CarUpdateID, UpdateManifest: &common.UpdateManifest{ManifestType: common.MagnaManifestUpdateType}}
um := services.GetDB().GetCarUpdates()
err := um.Load(manifest)
if err != nil {
if errors.Is(err, pg.ErrNoRows) {
continue
}
logger.Warn().Err(err).Send()
}
if manifest.ID > 0 {
filtered = append(filtered, statuses[x])
}
}
return filtered
}
func validateCarUpdatesStatuses(r *http.Request) ([]int64, error) {
qs := r.URL.Query()
qsIDs := qs.Get("carupdateids")
if qsIDs == "" {
return nil, fmt.Errorf("car update ids required")
}
if len(qsIDs) > 6000 {
return nil, fmt.Errorf("carupdateids too long")
}
carupdateIDs, err := querystring.SplitIntArray(qsIDs)
if err != nil {
return nil, err
}
return carupdateIDs, nil
}
type CarUpdateStatuses struct {
Statuses []interface{} `json:"statuses"`
}

View File

@@ -0,0 +1,40 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleCarUpdatesStatuses(t *testing.T) {
redis.MockRedisConnection()
services.SetRedisClientPool(tester.NewMockClientPool())
tests := []th.BasicHttpTest{
{
Name: "Missing query",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdatesstatuses", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"car update ids required","error":"Bad Request"}`,
},
{
Name: "Bad car update ids",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdatesstatuses?carupdateids=XXXXXXXXX", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"invalid id XXXXXXXXX","error":"Bad Request"}`,
},
{
Name: "Good request",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdatesstatuses?carupdateids=100,101,102", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"statuses":[{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0},{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0},{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0}]}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdatesStatuses)
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCustomerOtaEmails godoc
// @Summary Sends customer emails by list of vins
// @Description Sends OTA notification emails to all emails associated with vins in request body
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CustomerOtaEmailsRequest true "Customer OTA Emails Request"
// @Success 200 {object} map[string]bool "Customer Ota Emails"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /customer_ota_emails [put]
func HandleCustomerOtaEmails(w http.ResponseWriter, r *http.Request) {
var request common.CustomerOtaEmailsRequest
err := httphandlers.ParseRequest(r, &request)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var fromEmail = "fastservice@ovloop.com"
var toEmails = []string{}
var subject = request.EmailSubject
var body = request.EmailBody
driverEmails, err := services.GetDB().GetDriverEmails().SelectByVINs(request.VINs)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
for _, driverEmail := range driverEmails {
toEmails = append(toEmails, driverEmail.Email)
}
err = services.GetSMTP().Send(fromEmail, toEmails, subject, body)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}

View File

@@ -0,0 +1,77 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/smtpclient"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCustomerOtaEmails(t *testing.T) {
mock := MockDriverEmails{
SelectResponse: &[]common.DriverEmail{
{
Vin: "VCF1ZBU28PG002114",
DriverId: "446b4d69-8768-4e6b-bcf8-0507ea74c952",
Email: "ivan.delgadillo@indigotech.com",
GivenName: "Ivan",
FamilyName: "Delgadillo",
},
{
Vin: "VCF1ZBU25PG003608",
DriverId: "7d9c8fed-51b3-4df3-9603-5dfe0e8979f2",
Email: "junsub.lee@indigotech.com",
GivenName: "Junsub",
FamilyName: "Lee",
},
{
Vin: "VCF1EBU2XPG011442",
DriverId: "9855e14c-22f6-438d-84ee-47a7be675773",
Email: "csimpson@ovloop.com",
GivenName: "Clea",
FamilyName: "Simpson",
},
},
}
services.GetDB().SetDriverEmails(&mock)
mocksmtp := smtpclient.MockSMTP{}
services.SetSMTP(&mocksmtp)
tests := []mo.DBHttpTest{
{
Name: "Good data",
Request: th.MakeTestRequest(
http.MethodGet,
"/customer_ota_emails",
common.CustomerOtaEmailsRequest{
VINs: []string{"VCF1ZBU28PG002114", "VCF1ZBU25PG003608", "VCF1EBU2XPG011442"},
EmailSubject: "Test Email Subject",
EmailBody: `Dear Ocean Owner:
Test Email Body
Sincerely,
Me`,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: ``,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleCustomerOtaEmails, "/customer_ota_emails", &mock)
}
type MockDriverEmails struct {
SelectResponse *[]common.DriverEmail
Error error
mo.DBMockHelper
}
func (d *MockDriverEmails) SelectByVINs(vins []string) ([]common.DriverEmail, error) {
return *d.SelectResponse, d.Error
}

View File

@@ -0,0 +1,59 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleDBCSignalsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param dbc path string true "DBC hash"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.SignalDescWithECU}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals/{dbc} [get]
func HandleDBCSignalsGetList(w http.ResponseWriter, r *http.Request) {
options, err := clickhouse.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
params := httprouter.ParamsFromContext(r.Context())
dbc := params.ByName("dbc")
err = validator.GetValidator().Var(dbc, "required")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
cl, err := services.GetClickhouseClient()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
signals, count, err := cl.SelectDBCSignals(dbc, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: signals, Total: count})
}

View File

@@ -0,0 +1,115 @@
package handlers_test
import (
"context"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleDBCSignalsGetList(t *testing.T) {
validQuery := "?limit=5&offset=2"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{
ExpectedResult: []common.SignalDescWithECU{
{
ECUName: "ADAS",
SignalDesc: common.SignalDesc{
DBCHash: "hash",
MessageID: 2,
Name: "All_Signals_Sum_Check0",
Start: 0,
Length: 8,
IsBigEndian: true,
IsSigned: true,
IsMultiplexer: true,
IsMultiplexed: false,
MultiplexerValue: 5,
Offset: 0,
Scale: 3,
Min: 0,
Max: 20,
Unit: "sm",
Description: "desc",
ValueDescriptions: []string{"lt", "lg", "lk"},
ReceiverNodes: []string{"GW", "OO"},
DefaultValue: 0,
},
}, {
ECUName: "ICC",
SignalDesc: common.SignalDesc{
DBCHash: "hash",
MessageID: 20,
Name: "All_Signals_Sum_Check1",
Start: 8,
Length: 12,
IsBigEndian: false,
IsSigned: false,
IsMultiplexer: false,
IsMultiplexed: true,
MultiplexerValue: 7,
Offset: 8,
Scale: 31,
Min: 5,
Max: 12,
Unit: "kg",
Description: "desc 1",
ValueDescriptions: nil,
ReceiverNodes: nil,
DefaultValue: 10,
},
},
},
QueryRowtMock: func(ctx context.Context, query string, args ...interface{}) driver.Row {
return clickhouse.RowMock{RowResult: 5}
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"dbc_hash":"hash","message_id":2,"name":"All_Signals_Sum_Check0","start":0,"length":8,"big_endian":true,"signed":true,"multiplexer":true,"multiplexed":false,"multiplexer_value":5,"offset":0,"scale":3,"min":0,"max":20,"unit":"sm","description":"desc","value_descriptions":["lt","lg","lk"],"receiver_nodes":["GW","OO"],"default_value":0,"ECUName":"","ecu_name":"ADAS"},{"dbc_hash":"hash","message_id":20,"name":"All_Signals_Sum_Check1","start":8,"length":12,"big_endian":false,"signed":false,"multiplexer":false,"multiplexed":true,"multiplexer_value":7,"offset":8,"scale":31,"min":5,"max":12,"unit":"kg","description":"desc 1","value_descriptions":null,"receiver_nodes":null,"default_value":10,"ECUName":"","ecu_name":"ICC"}]}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.SignalDescWithECU","error":"Service Unavailable"}`,
},
"wrong limit": {
q: "?limit=-2",
expStatus: http.StatusBadRequest,
expBody: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
p := httprouter.Params{
{
Key: "dbc",
Value: "hash",
},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
r := httptest.NewRequest(http.MethodGet, "http://example.com/can_signals/dbc"+tt.q, nil).
WithContext(ctx)
handlers.HandleDBCSignalsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,55 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/utils"
// "github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleDigitalTwinSignal godoc
// @Summary List signals with updated timestamp
// @Description Returns list of state with last updated timestamp.
// @Accept json
// @Produce json
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []interface{}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ditto/carstate [get]
func HandleDigitalTwinSignal(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit < 1 || limit > 1000 {
limit = 1000
}
if offset < 1 {
offset = 0
}
// vin := r.URL.Query().Get("vin")
// if vin != "" {
// vins := strings.Split(vin, ",")
// for _, v := range vins {
// ok, err := validator.ValidateVINSimple(v)
// if !ok || err != nil {
// loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
// return
// }
// }
// }
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
data := cache.NewDigitalTwinTimestampState(conn).GetDigitalTwinSignals(offset, limit)
utils.RespJSON(w, http.StatusOK, data)
}

View File

@@ -0,0 +1,21 @@
package handlers
import (
"strings"
"otaupdate/docs"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
func InitSwaggerDoc() {
schemes := envtool.GetEnv("SWAGGER_SCHEMES", "https")
docs.SwaggerInfo.Title = "Fisker Inc OTA API"
docs.SwaggerInfo.Description = "Fisker Inc OTA portals APIs"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Host = ""
docs.SwaggerInfo.BasePath = httphandlers.ServiceBaseURL
docs.SwaggerInfo.Schemes = strings.Split(schemes, ",")
}

View File

@@ -0,0 +1,100 @@
package handlers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"otaupdate/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
const headerContentType = "Content-Type"
var swaggerHandler http.HandlerFunc
func init() {
handlers.InitSwaggerDoc()
swaggerHandler = httphandlers.GetSwaggerHandler()
}
func TestHandleSwaggerRedirect(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/api", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
validateSwaggerRedirect(t, recorder)
}
func TestHandleSwagger(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/api/", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
validateSwaggerRedirect(t, recorder)
}
func TestHandleSwaggerJSON(t *testing.T) {
contentType := "application/json; charset=utf-8"
req, _ := http.NewRequest("GET", "http://example.com/api/doc.json", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
headers := recorder.Result().Header
if headers.Get(headerContentType) != contentType {
t.Errorf(testhelper.TestErrorTemplate, headerContentType, contentType, headers.Get(headerContentType))
}
var data map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &data)
if err != nil {
t.Error(err)
}
}
func TestHandleSwaggerHTML(t *testing.T) {
contentType := "text/html; charset=utf-8"
htmlTitle := "<title>Swagger UI</title>"
req, _ := http.NewRequest("GET", "http://example.com/api/index.html", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
headers := recorder.Result().Header
if headers.Get(headerContentType) != contentType {
t.Errorf(testhelper.TestErrorTemplate, headerContentType, contentType, headers.Get(headerContentType))
}
if !strings.Contains(recorder.Body.String(), htmlTitle) {
t.Errorf(testhelper.TestErrorTemplate, "HTML", htmlTitle, recorder.Body.String())
}
}
func validateSwaggerRedirect(t *testing.T, recorder *httptest.ResponseRecorder) {
if recorder.Code != http.StatusMovedPermanently {
t.Errorf(testhelper.TestErrorTemplate, "Status code", http.StatusMovedPermanently, recorder.Code)
}
u, err := url.Parse(recorder.Header().Get("location"))
if err != nil {
t.Error(err)
}
if u.Path != "/api/index.html" {
t.Errorf(testhelper.TestErrorTemplate, "Path", "/api/index.html", u.Path)
}
}

View File

@@ -0,0 +1,194 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleECUDTCGet godoc
// @Summary Get ECU DTCs for a specific vehicle
// @Description Get ECU diagnostic trouble codes (DTCs) for a specific vehicle within a given time range
// @Tags ECU
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Param ecu query string false "ECU"
// @Param trouble_code query string false "Trouble Code"
// @Param start_time query string false "Start time (RFC3339 format)"
// @Param end_time query string false "End time (RFC3339 format)"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Param decode query bool false "Return decoded dtc information"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.DTC_ECU} "List of DTC ECU data"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /dtcs/{vin} [get]
func HandleECUDTCGet(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
queryParams := r.URL.Query()
ecu := queryParams.Get("ecu")
troubleCode := queryParams.Get("trouble_code")
startStr := queryParams.Get("start_time")
endStr := queryParams.Get("end_time")
decode, _ := strconv.ParseBool(queryParams.Get("decode"))
filter := bson.M{
"vin": vin}
if ecu != "" {
filter["ecu"] = ecu
}
if troubleCode != "" {
troubleCodeInt, err := strconv.ParseInt(troubleCode, 10, 64)
if err != nil {
http.Error(w, "Invalid trouble_code format, use int64", http.StatusBadRequest)
return
}
filter["dtc"] = troubleCodeInt
}
err := validator.GetValidator().Var(vin, "vin|vinsuffix")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var start time.Time
if startStr != "" {
start, err = time.Parse(time.RFC3339, startStr)
if err != nil {
http.Error(w, "Invalid start_time format, use RFC3339 format", http.StatusBadRequest)
return
}
}
var end time.Time
if endStr != "" {
end, err = time.Parse(time.RFC3339, endStr)
if err != nil {
http.Error(w, "Invalid end_time format, use RFC3339 format", http.StatusBadRequest)
return
}
}
if !start.IsZero() && !end.IsZero() {
filter["created_at"] = bson.M{
"$gte": start,
"$lte": end,
}
} else if !start.IsZero() {
filter["created_at"] = bson.M{
"$gte": start,
}
} else if !end.IsZero() {
filter["created_at"] = bson.M{
"$lte": end,
}
}
mongoOpts := options.Find()
query_params, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
mongoOpts.SetLimit(int64(query_params.Limit))
if query_params.Order != "" {
mongoOpts.SetSort(bson.D{
{"created_at", -1}}) // Descending order for 'created_at'.
}
if query_params.Offset != 0 {
mongoOpts.SetSkip(int64(query_params.Offset))
}
mongo, err := services.GetMongoClient()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var total int64
if query_params.Offset == 0 {
total, err = mongo.Collection("dtcs").CountDocuments(ctx, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
}
cursor, err := mongo.Collection("dtcs").Find(ctx, filter, mongoOpts)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var dtcs []common.DTC_ECU
for cursor.Next(ctx) {
var result common.DTC_ECU
err := cursor.Decode(&result)
if err != nil {
logger.Warn().Msg(err.Error())
continue
}
if decode {
fetchDTCDataFromMongo(&result)
}
dtcs = append(dtcs, result)
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: dtcs,
Total: int(total),
})
}
func fetchDTCDataFromMongo(dtc *common.DTC_ECU) (err error) {
client, err := mongo.GetPDXMongoClient()
if err != nil {
err = errors.WithStack(err)
return
}
// Handle the dtc string, need to drop the first byte as its the status code
troubleCodeHex := fmt.Sprintf("%X", dtc.TroubleCode)
troubleCodeHex = strings.ToUpper(troubleCodeHex)
info, err := client.GetDTCDefinitionByHexString(troubleCodeHex, dtc.ECU)
if err != nil {
return
}
if info == nil {
logger.Warn().Msgf("Failed to find dtc code from ecu: %s troubleCodeHex: %s", dtc.ECU, troubleCodeHex)
}
dtc.Information = info
dtc.StatusByteDecode = dtc.DTCStatusByteMeaning()
return
}

View File

@@ -0,0 +1,83 @@
package handlers_test
import (
"testing"
)
func TestHandlers_HandleECUDTCGet(t *testing.T) {
/*
db := services.GetDB()
dtcs := []common.DTC_ECU{
{
ID: 196,
VIN: "1GNGC26RXXJ407648",
ECU: "AMP",
DTC: []byte{9, 81, 118, 19},
Epoch_usec: 1624485598,
},
{
ID: 197,
VIN: "1GNGC26RXXJ407648",
ECU: "AMP",
TroubleCode: 12,
DTC: []byte{9, 81, 118, 19},
Epoch_usec: 1624485598,
},
}
tests := map[string]struct {
vin string
ecu string
start string
end string
dtcs q.ECUInterface
expStatus int
expBody string
}{
"success": {
vin: "1GNGC26RXXJ407648",
ecu: "AMP",
start: "",
end: "",
dtcs: &mocks.MockEcuDtc{
SelectDTCECUResponse: dtcs,
},
expStatus: http.StatusOK,
expBody: `{"data":[{"id":196,"vin":"1GNGC26RXXJ407648","ecu_name":"AMP","dtc":"CVF2Ew==","trouble_code":0,"status_byte":0,"epoch_usec":1624485598},{"id":197,"vin":"1GNGC26RXXJ407648","ecu_name":"AMP","dtc":"CVF2Ew==","trouble_code":12,"status_byte":0,"epoch_usec":1624485598}]}`,
},
"invalid_vin": {
vin: "INVALID_VIN",
ecu: "AMP",
start: "",
end: "",
dtcs: &mocks.MockEcuDtc{},
expStatus: http.StatusBadRequest,
expBody: `{"message":"vin|vinsuffix vin|vinsuffix ","error":"Bad Request"}`,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
w := httptest.NewRecorder()
db.SetDTCECU(tt.dtcs)
p := httprouter.Params{
{"vin", tt.vin},
{"ecu", tt.ecu},
{"trouble_code", "12"},
{"start_time", tt.start},
{"end_time", tt.end},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
request := httptest.NewRequest(http.MethodGet, "http://example.com/dtcs/"+tt.vin, nil).
WithContext(ctx)
handlers.HandleECUDTCGet(w, request)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
*/
}

View File

@@ -0,0 +1,155 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleECUStatsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param ecus query []string true "ECU names"
// @Param dbcs query []string true "DBC hashes"
// @Param vins query []string true "Array of VINs"
// @Param hours query int true "Past hours that must be included into the request"
// @Param min_zero_pct query float32 true "Minimum zero values percent"
// @Param min_out_of_range_pct query int true "Minimum out of range percent"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.ECUStat}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ecu_stats [get]
func HandleECUStatsGetList(w http.ResponseWriter, r *http.Request) {
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
filter, err := parseStatsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
stats, err := getEcusStats(conn, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: stats})
}
func parseStatsFilter(r *http.Request) (common.StatsFilter, error) {
sch := schema.NewDecoder()
filter := common.StatsFilter{}
sch.SetAliasTag("json")
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.StatsFilter{}, errors.WithStack(err)
}
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.StatsFilter{}, errors.WithStack(err)
}
return filter, nil
}
func getEcusStats(conn clickhouse.ConnInterface, filter common.StatsFilter) ([]common.ECUStat, error) {
var result []common.ECUStat
chCtx := ch.Context(context.Background(), ch.WithParameters(ch.Parameters{
"minOutOfRangePct": fmt.Sprint(filter.MinOutOfRangePct),
"minZeroPct": fmt.Sprint(filter.MinZeroPct),
"hours": fmt.Sprint(filter.Hours),
"vins": "['" + strings.Join(filter.VINs, "','") + "']",
"dbcs": "['" + strings.Join(filter.DBCs, "','") + "']",
"ecus": "['" + strings.Join(filter.ECUs, "','") + "']",
}))
if err := conn.Select(chCtx, &result, `select ecu_name,
sum(case when value_out_range_pct> {minOutOfRangePct:Float32} then 1 else 0 end) as signals_w_incorrect_values,
sum(case when zero_pct> {minZeroPct:Float32} then 1 else 0 end ) as signals_all_zero,
count(*) as number_of_ecu_signals,
sum(tot_cnt) as total_signal_records,
(signals_w_incorrect_values/number_of_ecu_signals) incorrect_val_signal_pct,
signals_all_zero/number_of_ecu_signals as zero_signals_pct
from ( select
case when aa.Name<>'' then aa.Name
when bb.signal_name<>'' then bb.signal_name
else null end as signal_name,
case when aa.ID<>'0' then aa.ID
when bb.message_id<>'0' then bb.message_id
else null end as can_id,
case when aa.ecu_name<>'' then aa.ecu_name
when dbcm.ecu_name<>'' then dbcm.ecu_name
else null end as ecu_name,
zero_count,value_out_range_cnt, tot_cnt,
case when tot_cnt <> 0 then value_out_range_pct else 0 end as value_out_range_pct,
case when tot_cnt <> 0 then zero_pct else 0 end as zero_pct
from
(/* check if signals are within dbc value range and if 0 on joined CAN signals and dbc*/
select Name, ID, signal_name, ecu_name,cycle_time_ns,sender_node,
sum(case when Value=0 then 1 else 0 end) as zero_count,
sum(case when a.Value<b.min or a.Value>b.max then 1 else 0 end) as value_out_range_cnt,
count(*) as tot_cnt,
value_out_range_cnt/tot_cnt as value_out_range_pct,
zero_count/tot_cnt as zero_pct
from vehicle_signal as a
inner join
(/* select dbc_messages and dbc_signals */
select b1.*, b2.message_id, b2.ecu_name, b2.cycle_time_ns, b2.sender_node
from dbc_signals as b1
inner join dbc_messages as b2
on b1.dbc_hash=b2.dbc_hash
and b1.message_id=b2.message_id
where b1.dbc_hash in {dbcs:Array(String)}
) as b
on a.Name=b.signal_name
where
a.Timestamp> (select max(Timestamp) from vehicle_signal where VIN in {vins:Array(String)}) - toIntervalHour({hours:UInt64})
and
a.VIN in {vins:Array(String)}
group by 1,2,3,4,5,6
) as aa
full outer join dbc_signals as bb
on aa.Name=bb.signal_name
and aa.ID=bb.message_id
inner join dbc_messages as dbcm
on bb.message_id=dbcm.message_id
where bb.dbc_hash in {dbcs:Array(String)} and dbcm.dbc_hash in {dbcs:Array(String)}
and ecu_name in {ecus:Array(String)})
group by ecu_name
order by zero_signals_pct desc, incorrect_val_signal_pct desc`); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,71 @@
package handlers_test
import (
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleECUStatsGetList(t *testing.T) {
validQuery := "?min_out_of_range_pct=10&min_zero_pct=0.009&hours=160&vins=TREXTEST7TUR9NXGC&vins=TREXTEST5T61T1BR2&dbcs=73583d63735b404f5209a71107c3d2174b0ab1ba40bd826b8cb69668598b0395&ecus=ADAS&ecus=ICC&ecus=TREX"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: []common.ECUStat{
{
ECUName: "ADAS",
IncorrectValues: 1,
AllZero: 1,
ECUSignalsTotal: 231,
SignalsTotal: 2221,
IncorrectPercent: 0.04,
ZeroPercent: 0.003,
},
{
ECUName: "TREX",
IncorrectValues: 0,
AllZero: 0,
ECUSignalsTotal: 77,
SignalsTotal: 9789,
IncorrectPercent: 0.55,
ZeroPercent: 0.36,
},
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"ecu_name":"ADAS","signals_w_incorrect_values":1,"signals_all_zero":1,"number_of_ecu_signals":231,"total_signal_records":2221,"incorrect_val_signal_pct":0.04,"zero_signals_pct":0.003},{"ecu_name":"TREX","signals_w_incorrect_values":0,"signals_all_zero":0,"number_of_ecu_signals":77,"total_signal_records":9789,"incorrect_val_signal_pct":0.55,"zero_signals_pct":0.36}]}`,
},
"failed_filter": {
q: "",
expStatus: http.StatusBadRequest,
expBody: `{"message":"MinOutOfRangePct required. MinZeroPct required. Hours required. VINs required. DBCs required. ECUs required","error":"Bad Request"}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.ECUStat","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "http://example.com/ecu_stats"+tt.q, nil)
handlers.HandleECUStatsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,165 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleVINECUStatsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param ecus query []string true "ECU names"
// @Param hours query int true "Past hours that must be included into the request"
// @Param min_zero_pct query float32 true "Minimum zero values percent"
// @Param min_out_of_range_pct query int true "Minimum out of range percent"
// @Param dbc path string true "DBC hash"
// @Param vin path string true "VIN"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.ECUStat}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ecu_stats/{vin}/{dbc} [get]
func HandleVINECUStatsGetList(w http.ResponseWriter, r *http.Request) {
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
filter, err := parseVINStatsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
stats, err := getEcusVINStats(conn, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: stats})
}
func parseVINStatsFilter(r *http.Request) (common.VINStatsFilter, error) {
sch := schema.NewDecoder()
filter := common.VINStatsFilter{}
sch.SetAliasTag("json")
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.VINStatsFilter{}, errors.WithStack(err)
}
params := httprouter.ParamsFromContext(r.Context())
filter.VIN = params.ByName("vin")
filter.DBC = params.ByName("dbc")
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.VINStatsFilter{}, errors.WithStack(err)
}
return filter, nil
}
func getEcusVINStats(conn clickhouse.ConnInterface, filter common.VINStatsFilter) ([]common.ECUStat, error) {
var result []common.ECUStat
chCtx := ch.Context(context.Background(), ch.WithParameters(ch.Parameters{
"minOutOfRangePct": fmt.Sprint(filter.MinOutOfRangePct),
"minZeroPct": fmt.Sprint(filter.MinZeroPct),
"hours": fmt.Sprint(filter.Hours),
"vin": filter.VIN,
"dbc": filter.DBC,
"ecus": "['" + strings.Join(filter.ECUs, "','") + "']",
}))
if err := conn.Select(chCtx, &result, `
select ecu_name,
sum(case when value_out_range_pct>{minOutOfRangePct:Float32} then 1 else 0 end) as signals_w_incorrect_values,
sum(case when zero_pct>{minZeroPct:Float32} then 1 else 0 end ) as signals_all_zero,
count(*) as number_of_ecu_signals,
sum(tot_cnt) as total_signal_records,
(signals_w_incorrect_values/number_of_ecu_signals) incorrect_val_signal_pct,
signals_all_zero/number_of_ecu_signals as zero_signals_pct
from
(/* add missing signals and ecus from dbc as full outer join and generate per a CAN signal stats */
select
case when aa.Name<>'' then aa.Name
when bb.signal_name<>'' then bb.signal_name
else null end as signal_name,
case when aa.ID<>'0' then aa.ID
when bb.message_id<>'0' then bb.message_id
else null end as can_id,
case when aa.ecu_name<>'' then aa.ecu_name
when dbcm.ecu_name<>'' then dbcm.ecu_name
else null end as ecu_name,
zero_count,value_out_range_cnt, tot_cnt,
case when tot_cnt <> 0 then value_out_range_pct else 0 end as value_out_range_pct,
case when tot_cnt <> 0 then zero_pct else 0 end as zero_pct
from
(/* check if signals are within dbc value range and if 0 on joined CAN signals and dbc*/
select Name, ID, signal_name, ecu_name,cycle_time_ns,sender_node,
sum(case when Value=0 then 1 else 0 end) as zero_count,
sum(case when a.Value<b.min or a.Value>b.max then 1 else 0 end) as value_out_range_cnt,
count(*) as tot_cnt,
value_out_range_cnt/tot_cnt as value_out_range_pct,
zero_count/tot_cnt as zero_pct
from vehicle_signal as a
inner join
(/* select dbc_messages and dbc_signals */
select b1.*, b2.message_id, b2.ecu_name, b2.cycle_time_ns, b2.sender_node
from dbc_signals as b1
inner join dbc_messages as b2
on b1.dbc_hash=b2.dbc_hash
and b1.message_id=b2.message_id
where b1.dbc_hash = {dbc:String}
) as b
on a.Name=b.signal_name
where
a.Timestamp> (select max(Timestamp) from vehicle_signal where VIN = {vin:String}) - toIntervalHour({hours:UInt64})
and
a.VIN = {vin:String}
group by 1,2,3,4,5,6
) as aa
full outer join dbc_signals as bb
on aa.Name=bb.signal_name
and aa.ID=bb.message_id
inner join dbc_messages as dbcm
on bb.message_id=dbcm.message_id
where bb.dbc_hash = {dbc:String} and dbcm.dbc_hash = {dbc:String}
and ecu_name in {ecus:Array(String)}
)
group by ecu_name
order by zero_signals_pct desc, incorrect_val_signal_pct desc
`); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,82 @@
package handlers_test
import (
"context"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleVINECUStatsGetList(t *testing.T) {
validQuery := "?min_out_of_range_pct=10&min_zero_pct=0.009&hours=160&ecus=ADAS&ecus=ICC&ecus=TREX"
validVin := "TREXTEST7TUR9NXGC"
validDBC := "73583d63735b404f5209a71107c3d2174b0ab1ba40bd826b8cb69668598b0395"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: []common.ECUStat{
{
ECUName: "ADAS",
IncorrectValues: 1,
AllZero: 1,
ECUSignalsTotal: 231,
SignalsTotal: 2221,
IncorrectPercent: 0.04,
ZeroPercent: 0.003,
},
{
ECUName: "TREX",
IncorrectValues: 0,
AllZero: 0,
ECUSignalsTotal: 77,
SignalsTotal: 9789,
IncorrectPercent: 0.55,
ZeroPercent: 0.36,
},
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"ecu_name":"ADAS","signals_w_incorrect_values":1,"signals_all_zero":1,"number_of_ecu_signals":231,"total_signal_records":2221,"incorrect_val_signal_pct":0.04,"zero_signals_pct":0.003},{"ecu_name":"TREX","signals_w_incorrect_values":0,"signals_all_zero":0,"number_of_ecu_signals":77,"total_signal_records":9789,"incorrect_val_signal_pct":0.55,"zero_signals_pct":0.36}]}`,
},
"failed_filter": {
q: "",
expStatus: http.StatusBadRequest,
expBody: `{"message":"MinOutOfRangePct required. MinZeroPct required. Hours required. ECUs required","error":"Bad Request"}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.ECUStat","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
p := httprouter.Params{
{Key: "vin", Value: validVin},
{Key: "dbc", Value: validDBC},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
r := httptest.NewRequest(http.MethodGet, "http://example.com/ecu_stats/vin/dbc"+tt.q, nil).
WithContext(ctx)
handlers.HandleVINECUStatsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,12 @@
package handlers
import (
"github.com/pkg/errors"
)
var ErrInvalidVIN = errors.New("invalid VIN")
var ErrMissingVIN = errors.New("missing VIN")
var ErrInvalidType = errors.New("invalid object type")
var ErrInvalidURLParams = errors.New("missing URL parameters")

View File

@@ -0,0 +1,46 @@
package handlers
import (
"fmt"
"net/http"
)
// HandleExperiment godoc
// @Summary Testing msg preview
// @Description Blank
// @Accept json
// @Produce json
// @Param id query string true "ID of request"
// @Router /experiment [get]
func HandleExperiment(w http.ResponseWriter, r *http.Request) {
// Get the "id" query parameter
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id parameter", http.StatusBadRequest)
return
}
// Build Open Graph tags dynamically
title := fmt.Sprintf("Page for ID %s", id)
url := fmt.Sprintf("https://dev-gw.cloud.fiskerinc.com/ota_update/expirment?id=%s", id)
image := "https://www.google.com/url?sa=i&url=https%3A%2F%2Fulife.vpul.upenn.edu%2Fcareerservices%2Fblog%2F2010%2F11%2F12%2Fprofessionalism-and-the-pre-health-student-beyond-please-and-thank-you%2Ffunny-cat-green-avacado%2F&psig=AOvVaw3bK13MXk_hL91SyLrmdrMS&ust=1755886127191000&source=images&cd=vfe&opi=89978449&ved=0CBYQjRxqFwoTCODu-du_nI8DFQAAAAAdAAAAABAE"
// Write headers
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>%s</title>
<meta property="og:title" content="%s" />
<meta property="og:type" content="website" />
<meta property="og:url" content="%s" />
<meta property="og:image" content="%s" />
<meta property="og:description" content="This is the Open Graph preview for ID %s" />
</head>
<body>
<h1>Open Graph Page for %s</h1>
<p>Preview metadata has been set in the HTML headers.</p>
</body>
</html>`, title, title, url, image, id, id)
}

View File

@@ -0,0 +1,472 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"strings"
"time"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/security"
"github.com/fiskerinc/cloud-services/pkg/smtpclient"
"github.com/pkg/errors"
)
type logCollector struct {
messages []string
startTime time.Time
}
func newLogCollector() *logCollector {
return &logCollector{
messages: make([]string, 0),
startTime: time.Now(),
}
}
func (lc *logCollector) add(message string) {
timestamp := time.Now().Format("15:04:05.000")
lc.messages = append(lc.messages, fmt.Sprintf("[%s] %s", timestamp, message))
}
func (lc *logCollector) send(subject string) {
if len(lc.messages) == 0 {
return
}
duration := time.Since(lc.startTime)
body := fmt.Sprintf("Request Duration: %v\n\nLogs:\n%s", duration, strings.Join(lc.messages, "\n"))
smtp := smtpclient.NewSMTP("email-smtp.us-west-2.amazonaws.com", 587)
smtp.Auth("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
to := []string{"marner@ovloop.com", "padamsen@ovloop.com"}
err := smtp.Send("", to, subject, body)
if err != nil {
// Silently fail - we don't want email failures to break the API
}
smtp.Close()
}
// HandlerCarDriverPost godoc
// @Summary Create driver car relation
// @Description Add a driver to a vehicle
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/add_external [post]
func HandleVehicleExternalDriverAdd(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Add Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request received\nEndpoint: /drivers/add_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vdai := VehicleDriverAddInput{}
err := json.NewDecoder(r.Body).Decode(&vdai)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request data decoded successfully\nUserID: %s\nSource: %s\nVIN: %s\nFirstName: %s\nLastName: %s\nCallbackURL: %s",
vdai.UserID, vdai.Source, vdai.PairingInfo.VIN, vdai.Person.FirstName, vdai.Person.LastName, vdai.CallbackURL))
// If there is an error, than we did not succesfuly beign pairng
err = VehicleExternalDriverAdd(vdai, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: VehicleExternalDriverAdd failed\nError: %v\nStack Trace: %s\nUserID: %s\nVIN: %s",
err, string(debug.Stack()), vdai.UserID, vdai.PairingInfo.VIN))
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request completed successfully\nUserID: %s\nVIN: %s", vdai.UserID, vdai.PairingInfo.VIN))
}
func VehicleExternalDriverAdd(vdai VehicleDriverAddInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Starting driver addition process\nUserID: %s\nSource: %s\nVIN: %s", vdai.UserID, vdai.Source, vdai.PairingInfo.VIN))
// TODO: CHECK CAR IS ON
// TODO: Check that the salt or session matches
// Check that the QR code is valid and from the car
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Validating connection info\nVIN: %s\nSalt: %s\nSessionID: %s",
vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
err = ValidateConnectionInfo(vdai.PairingInfo, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation successful\nVIN: %s", vdai.PairingInfo.VIN))
// Try to Create an account for this user. If they already have an account, that is fine as well
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Adding new driver to database\nUserID: %s\nSource: %s", vdai.UserID, vdai.Source))
userID, err := addNewDriverDatabase(vdai.UserID, vdai.Source, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to add new driver to database\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vdai.UserID, vdai.Source))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver added to database successfully\nUserID: %s\nSource: %s\nFiskerUserID: %s", vdai.UserID, vdai.Source, userID))
// So we now have a user, we can now begin the car pairing
// Create car to driver entry
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Creating car to driver relationship\nVIN: %s\nFiskerUserID: %s", vdai.PairingInfo.VIN, userID))
cars := services.GetDB().GetCars()
relation, err := cars.AddDriver(&common.Car{VIN: vdai.PairingInfo.VIN}, &common.Driver{ID: userID}, "OWNER") // Don't know if there is any other role
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to create car to driver relationship\nError: %v\nStack Trace: %s\nVIN: %s\nFiskerUserID: %s\nRole: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, userID, "OWNER"))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Car to driver relationship created successfully\nVIN: %s\nDriverID: %s\nDriverRole: %s", relation.VIN, relation.DriverID, relation.DriverRole))
// Send HMI command
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Getting Redis connection from pool\nVIN: %s\nDriverID: %s", relation.VIN, relation.DriverID))
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Preparing HMI message\nVIN: %s\nDriverID: %s\nDriverRole: %s\nFirstName: %s\nLastName: %s",
relation.VIN, relation.DriverID, relation.DriverRole, vdai.Person.FirstName, vdai.Person.LastName))
// TODO: Add settings HERE
err = conn.SafePublishMessage(
common.HMI.Key(relation.VIN),
common.Message{
Handler: "profile_new",
Data: common.JSONHMIProfile{
DriverID: relation.DriverID,
DriverRole: relation.DriverRole,
User: common.UserProfile{
FirstName: vdai.Person.FirstName,
LastName: vdai.Person.LastName,
},
Settings: make([]common.CarSetting, 0),
Subscriptions: make([]common.Subscription, 0),
},
},
)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to publish HMI message\nError: %v\nStack Trace: %s\nVIN: %s\nDriverID: %s\nHMIKey: %s",
err, string(debug.Stack()), relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: HMI message published successfully\nVIN: %s\nDriverID: %s\nHMIKey: %s", relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver addition process completed successfully\nUserID: %s\nVIN: %s\nFiskerUserID: %s", vdai.UserID, vdai.PairingInfo.VIN, userID))
return
}
func ValidateConnectionInfo(pi PairingInfo, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Starting validation\nVIN: %s\nSalt: %s\nSessionID: %s", pi.VIN, pi.Salt, pi.SessionID))
salter, err := security.NewSalter(pi.VIN)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Failed to create salter\nError: %v\nStack Trace: %s\nVIN: %s", err, string(debug.Stack()), pi.VIN))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salter created successfully\nVIN: %s", pi.VIN))
clientPool := services.GetRedisV2Client()
switch {
case pi.SessionID != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using session ID validation\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = salter.ValidateSessionID(pi.SessionID)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation successful\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = checkSession(clientPool, pi.VIN, pi.SessionID, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session validation completed successfully\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
case pi.Salt != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using salt validation\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
err = checkSession(clientPool, pi.VIN, pi.Salt, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s",
err, string(debug.Stack()), pi.VIN, pi.Salt))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt validation completed successfully\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
//sessionID = salter.GenerateSessionID(pi.VIN, pi.Salt)
default:
logs.add(fmt.Sprintf("ValidateConnectionInfo: Missing both salt and session ID\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
ErrMissingSaltAndSessionID, string(debug.Stack()), pi.VIN, pi.Salt, pi.SessionID))
err = ErrMissingSaltAndSessionID
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Connection info validation completed successfully\nVIN: %s", pi.VIN))
return
}
func addNewDriverDatabase(externalID, source string, logs *logCollector) (userID string, err error) {
logs.add(fmt.Sprintf("addNewDriverDatabase: Starting database operation\nExternalID: %s\nSource: %s", externalID, source))
// This complicated query does the following things
// Checks to see if the external user already exists. If so we return their fisker_id
// If they do not exist, we insert a new fisker_id into the drivers table, and then insert the user into the external user table
query := `WITH existing_user AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
), new_driver AS (
INSERT INTO drivers (id)
SELECT uuid_generate_v4()
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
RETURNING id
), inserted_user AS (
INSERT INTO drivers_external (fisker_id, external_id, source)
SELECT id, ?, ? FROM new_driver
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
)
SELECT fisker_id AS id FROM existing_user
UNION ALL
SELECT id FROM new_driver;`
// UNION ALL can probably be just union, just trying to make sure we get a row back
// Don't need to worry about someone being in external drivers and not fisker drivers, as there is a foreign key dependency
type Result struct {
ID string
}
var result Result
db := services.GetDB().GetDBClient()
logs.add(fmt.Sprintf("addNewDriverDatabase: Executing database query\nExternalID: %s\nSource: %s", externalID, source))
_, err = db.GetConn().QueryOne(&result, query, externalID, source, externalID, source)
if err != nil {
logs.add(fmt.Sprintf("addNewDriverDatabase: Database query failed\nError: %v\nStack Trace: %s\nExternalID: %s\nSource: %s",
err, string(debug.Stack()), externalID, source))
return
}
logs.add(fmt.Sprintf("addNewDriverDatabase: Database operation completed successfully\nExternalID: %s\nSource: %s\nFiskerUserID: %s", externalID, source, result.ID))
return result.ID, err
}
// TODO: Add validation to struct
type VehicleDriverAddInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
PairingInfo PairingInfo `json:"pairing_info"`
Person UserInfo `json:"user_info"`
CallbackURL string `json:"callback_url"` // Where to send the BLE key when pairing is done
}
type PairingInfo struct {
VIN string `json:"vin"`
Salt string `json:"salt"` // either salt or session is required
SessionID string `json:"session_id"`
}
type UserInfo struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
type VehicleDriverAddResponse struct {
AccessAllowed bool `json:"access_allowed"` // True if the user provided the correct QR code data to connect with the car
Error error `json:"error,omitempty"`
}
// HandleExternalDriverDelete godoc
// @Summary Remove driver from DB
// @Description Remove a drivers profile completely
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/remove_external [delete]
func HandleExternalDriverDelete(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Delete Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request received\nEndpoint: /drivers/remove_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vrddi := ExternalDriverDeleteInput{}
err := json.NewDecoder(r.Body).Decode(&vrddi)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request data decoded successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
err = ExternalDriverDelete(vrddi, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: ExternalDriverDelete failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vrddi.UserID, vrddi.Source))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request completed successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
}
// Delete an external driver from the database
func ExternalDriverDelete(eddi ExternalDriverDeleteInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ExternalDriverDelete: Starting driver deletion process\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
query := `WITH to_delete AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
)
DELETE FROM drivers
WHERE id IN (SELECT fisker_id FROM to_delete)`
logs.add(fmt.Sprintf("ExternalDriverDelete: Executing database deletion query\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
db := services.GetDB().GetDBClient()
_, err = db.GetConn().Exec(query, eddi.UserID, eddi.Source)
if err != nil {
logs.add(fmt.Sprintf("ExternalDriverDelete: Database deletion failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), eddi.UserID, eddi.Source))
return
}
logs.add(fmt.Sprintf("ExternalDriverDelete: Driver deletion completed successfully\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
return
}
type ExternalDriverDeleteInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
}
// Go here, and add function to remove a car driver relationship for an external driver
// // HandleVehicleExternalDriverDelete godoc
// // @Summary Remove driver from DB
// // @Description Remove a drivers profile completely
// // @Accept json
// // @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// // @Param Api-Key header string false "<API token>"
// // @Param data body VehicleDriverAddInput true "User INFO"
// // @Router /drivers/remove_external [delete]
// func HandleVehicleExternalDriverVehicleRemove(w http.ResponseWriter, r *http.Request) {
// vrddi := VehicleExternalDriverDeleteInput{}
// err := json.NewDecoder(r.Body).Decode(&vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
// return
// }
// err = VehicleExternalDriverRemove(vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
// return
// }
// }
// func VehicleExternalDriverRemove(vrddi VehicleExternalDriverDeleteInput) (err error) {
// query := ``
// return
// }
// type VehicleExternalDriverDeleteInput struct{
// UserID string `json:"user_id"` // However the user wants to be placed in
// Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
// }
func checkSession(redisClient *redisv2.Connection, vin string, sessionID string, logs *logCollector) error {
logs.add(fmt.Sprintf("checkSession: Starting session validation\nVIN: %s\nSessionID: %s", vin, sessionID))
if sessionID == "" {
logs.add(fmt.Sprintf("checkSession: Session ID is empty\nError: %v\nStack Trace: %s\nVIN: %s", ErrMissingSaltAndSessionID, string(debug.Stack()), vin))
return ErrMissingSaltAndSessionID
}
logs.add(fmt.Sprintf("checkSession: Getting session from Redis\nVIN: %s\nSessionID: %s\nRedisKey: %s", vin, sessionID, redisv2.HMISessionKey(vin)))
redisResponse := redisClient.Client.Get(context.Background(), redisv2.HMISessionKey(vin))
session, err := redisResponse.Result()
if err != nil {
logs.add(fmt.Sprintf("checkSession: Failed to get session from Redis\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisKey: %s",
err, string(debug.Stack()), vin, sessionID, redisv2.HMISessionKey(vin)))
return err
}
logs.add(fmt.Sprintf("checkSession: Retrieved session from Redis\nVIN: %s\nSessionID: %s\nRedisSession: %s", vin, sessionID, session))
if session != sessionID {
logs.add(fmt.Sprintf("checkSession: Session mismatch detected\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisSession: %s",
ErrSessionMismatch, string(debug.Stack()), vin, sessionID, session))
return ErrSessionMismatch
}
logs.add(fmt.Sprintf("checkSession: Session validation completed successfully\nVIN: %s\nSessionID: %s", vin, sessionID))
return nil
}
var ErrSessionMismatch = errors.New("sessions do not match")
var ErrMissingSaltAndSessionID = errors.New("request missing salt and sessionID")

View File

@@ -0,0 +1,141 @@
package handlers
import (
"errors"
"net/http"
"otaupdate/services"
"sort"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
var apiCreateToken string = envtool.GetEnv("MIGRATE_CREATE_TOKEN", "")
// HandleFlashpackVersionAdd godoc
// @Summary Add a flashpack version
// @Description Add a flashpack version
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CarFlashpackVersionAddRequest true "Mappings between ECU versions and a flashpack number"
// @Success 200 {object} common.JSONDBQueryResult "Created flashpack ecu mapping result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version [post]
func HandleFlashpackVersionAdd(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionAddRequest
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if len(req.ECUVersions) < 1 {
loggerdataresp.BadDataErrorResp(w, errors.New("CarECUName and CarECUVersion required"), http.StatusBadRequest)
return
}
// Include previous flashpack mappings
previousMappings, err := services.GetDB().GetCars().GetCarFlashpackVersionMappingsByModelTrim(req.CarModel, req.CarTrim, nil)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// the flashpacks are stored in the database as strings, so we have to sort them numerically
// in descending order by year and flashpack
sort.Slice(previousMappings, func(i, j int) bool {
iFp, _ := strconv.ParseFloat(previousMappings[i].Flashpack, 64) // guaranteed numeric
jFp, _ := strconv.ParseFloat(previousMappings[j].Flashpack, 64) // guaranteed numeric
iYear := previousMappings[i].CarYear
jYear := previousMappings[j].CarYear
if iYear == jYear {
return iFp > jFp
} else {
return iYear > jYear
}
})
// Put all the mappings into one array, still in descending order by flashpack number
// only include the ones that are less than or equal to the new flashpack number being added
mappings := []common.CarFlashpackVersion{}
for _, v := range req.ECUVersions {
mappings = append(mappings, common.CarFlashpackVersion{
CarECUName: v.CarECUName,
CarECUVersion: v.CarECUVersion,
Flashpack: req.Flashpack,
CarModel: req.CarModel,
CarTrim: req.CarTrim,
CarYear: req.CarYear,
})
}
for _, m := range previousMappings {
reqFp, _ := strconv.ParseFloat(req.Flashpack, 64) // already validated as numeric
mFp, _ := strconv.ParseFloat(m.Flashpack, 64) // already validated as numeric
if (m.CarYear < req.CarYear) ||
(m.CarYear == req.CarYear && mFp <= reqFp) {
mappings = append(mappings, m)
}
}
// Put the mappings in a map by ecu name
// There can be more than one ECU version for an ECU for a flashpack
var newMappings = make(map[string][]common.CarFlashpackVersion)
for _, m := range mappings {
// Only include the mapping if it is one of the latest
latestVersionMappings, ok := newMappings[m.CarECUName]
// Include multiple versions for the same ecu and flashpack number
if (ok && m.Flashpack == latestVersionMappings[0].Flashpack) || !ok {
newMappings[m.CarECUName] = append(newMappings[m.CarECUName], m)
}
}
// Flatten the map into an array
var newMappingsArray []common.CarFlashpackVersion
for _, m := range newMappings {
newMappingsArray = append(newMappingsArray, m...)
}
// Apply the new flashpack number to all the mappings to be inserted
for i := range newMappingsArray {
newMappingsArray[i].Flashpack = req.Flashpack
newMappingsArray[i].CarYear = req.CarYear
}
err = services.GetDB().GetCars().AddCarFlashpackVersionMappings(newMappingsArray)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Also add to other environments, as required
for _, targetURL := range targetURLS {
if !validator.ValidateURL(targetURL) || apiCreateToken == "" {
break // No URL in MANIFEST_MIGRATE_URLS
}
otaService := services.NewOtaService(targetURL, apiCreateToken)
resp, err := otaService.FlashpackVersionAdd(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
utils.ForwardResponse(w, resp)
}
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Created",
})
}

View File

@@ -0,0 +1,74 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"testing"
m "github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionAdd(t *testing.T) {
// mock := mo.MockCars{}
// services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Bad data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{
{
CarECUName: "ADAS",
CarECUVersion: "ADASVersion",
},
{
CarECUName: "RV",
CarECUVersion: "RVVersion",
},
},
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarYear required","error":"Bad Request"}`,
},
{
Name: "Bad data no ECUs",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{},
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarECUName and CarECUVersion required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{
{
CarECUName: "ADAS",
CarECUVersion: "ADASVersion5",
},
{
CarECUName: "RV",
CarECUVersion: "RVVersion",
},
},
Flashpack: "11.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2025,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Created"}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionAdd, "/flashpack_version", nil)
}

View File

@@ -0,0 +1,66 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
var apiDeleteToken string = envtool.GetEnv("MIGRATE_DELETE_TOKEN", "")
// HandleFlashpackVersionDelete godoc
// @Summary Delete a flashpack version
// @Description Delete a flashpack version
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CarFlashpackVersionRequest true "Flashpack version"
// @Success 200 {object} common.JSONDBQueryResult "Deleted flashpack ecu mapping result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version [delete]
func HandleFlashpackVersionDelete(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionRequest
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = services.GetDB().GetCars().DeleteFlashpackVersion(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Also delete in other environments, as required
for _, targetURL := range targetURLS {
if !validator.ValidateURL(targetURL) || apiDeleteToken == "" {
break // No URL in MANIFEST_MIGRATE_URLS
}
otaService := services.NewOtaService(targetURL, apiDeleteToken)
resp, err := otaService.FlashpackVersionDelete(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
utils.ForwardResponse(w, resp)
}
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,43 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
m "github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionDelete(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Bad data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/flashpack_version", m.CarFlashpackVersionRequest{
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarYear required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/flashpack_version", m.CarFlashpackVersionRequest{
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionDelete, "/flashpack_version", &mock)
}

View File

@@ -0,0 +1,70 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpackVersionECUMappingsGet godoc
// @Summary Get mappings between a flashpack and ecu versions
// @Description Get mappings between a flashpack and ecu versions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param model path string true "Model"
// @Param trim path string true "Trim"
// @Param year path int true "Year"
// @Param flashpack path string true "Flashpack"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpack ecu mappings result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version_ecu_mappings/{model}/{trim}/{year}/{flashpack} [get]
func HandleFlashpackVersionECUMappingsGet(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionRequest
var err error
params := httprouter.ParamsFromContext(r.Context())
req.Flashpack = params.ByName("flashpack")
req.CarModel = params.ByName("model")
req.CarTrim = params.ByName("trim")
req.CarYear, err = strconv.Atoi(params.ByName("year"))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "created_at DESC"
}
cars := services.GetDB().GetCars()
flashpackMappings, err := cars.GetCarFlashpackVersionMappingsByModelTrimYearFlashpack(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := cars.GetCarFlashpackVersionMappingsByModelTrimYearFlashpackCount(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: flashpackMappings,
Total: total,
})
}

View File

@@ -0,0 +1,27 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionECUMappingsGet(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Good data",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_version_ecu_mappings/Ocean/2023/41.14", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"flashpack":"44.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion1"},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion"},{"flashpack":"11.0","car_model":"Ocean","car_trim":"Base","car_year":2024,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion4"},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"BCM","car_ecu_version":"BCMVersion"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion0"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion0"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"PDI","car_ecu_version":"PDIVersion"}],"total":8}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionECUMappingsGet, "/flashpack_version_ecu_mappings/:model/:year/:flashpack", &mock)
}

View File

@@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"otaupdate/services"
"sort"
"github.com/fiskerinc/cloud-services/pkg/common"
fv "github.com/fiskerinc/cloud-services/pkg/flashpackversion"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpackVersionGetInfo godoc
// @Summary Get flashpack version info for a car
// @Description Get flashpack version info (version number, ECUs to be updated for next version) for a car
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpack version info result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version_info/{vin} [get]
func HandleFlashpackVersionInfoGet(w http.ResponseWriter, r *http.Request) {
vin := httprouter.ParamsFromContext(r.Context()).ByName("vin")
err := validator.ValidateField(vin, "vin")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
cars := services.GetDB().GetCars()
car, err := cars.SelectByVIN(vin)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
nextFlashpackVersion, err := cars.GetNextFlashpackVersion(car.Model, car.Trim, car.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if nextFlashpackVersion != nil {
ecusNeededForNextFlashpack, err := fv.FindCarECUsToUpdateForNextFlashpackNumber(cars, *car, nextFlashpackVersion.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Sort by ECU name in alphabetical order
sort.Slice(ecusNeededForNextFlashpack, func(i, j int) bool {
return ecusNeededForNextFlashpack[i].CarECUName < ecusNeededForNextFlashpack[j].CarECUName
})
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: common.CarFlashpackVersionInfoResponse{
Flashpack: car.Flashpack,
NextFlashpack: nextFlashpackVersion.Flashpack,
ECUVersions: ecusNeededForNextFlashpack,
},
})
} else {
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: common.CarFlashpackVersionInfoResponse{
Flashpack: car.Flashpack,
},
})
}
}

View File

@@ -0,0 +1,218 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionInfoGet(t *testing.T) {
mock := setupMockCars()
services.GetDB().SetCars(setupMockCars())
tests := []mo.DBHttpTest{
{
Name: "Get info",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_version_info/11111111111111111", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":{"flashpack":"39.14","next_flashpack":"41.14","ecu_versions":[{"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion"}]}}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionInfoGet, "/flashpack_version_info/:vin", mock)
}
func setupMockCars() *mo.MockCars {
return &mo.MockCars{
SelectResponse: &common.Car{VIN: "11111111111111111", ICCID: "1111111111111111111F", Flashpack: "39.14", Model: "Ocean", Trim: "Base"},
SelectCarSettings: []common.CarSetting{},
SelectCarFlashpackVersions: []common.CarFlashpackVersion{
// 46.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion2",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionA",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionB",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 44.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion1",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionA",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionB",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 41.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 39.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion0",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersion0",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 37.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "37.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
},
SelectCarECUs: []common.CarECU{
{
VIN: "11111111111111111",
ECU: "ADAS",
SupplierSWVersion: "ADASVersion1",
},
{
VIN: "11111111111111111",
ECU: "ACUN",
SupplierSWVersion: "ACUNVersion0",
},
{
VIN: "11111111111111111",
ECU: "BCM",
SupplierSWVersion: "BCMVersion",
},
{
VIN: "11111111111111111",
ECU: "PDI",
SupplierSWVersion: "PDIVersion",
},
},
}
}

View File

@@ -0,0 +1,65 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpacksGetAll godoc
// @Summary Get all flashpacks
// @Description Get all flashpacks
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param model path string true "Model"
// @Param trim path string true "Trim"
// @Param year path int true "Year"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpacks result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_versions/{model}/{trim}/{year} [get]
func HandleFlashpackVersionsGetAll(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
model := params.ByName("model")
trim := params.ByName("trim")
year, err := strconv.Atoi(params.ByName("year"))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "flashpack DESC"
}
cars := services.GetDB().GetCars()
flashpacks, err := cars.GetFlashpackVersions(model, trim, year, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := cars.GetFlashpackVersionsCount(model, trim, year)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: flashpacks,
Total: total,
})
}

View File

@@ -0,0 +1,27 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionsGetAll(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Get all",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_versions/Ocean/Base/2023", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"flashpack":"43.19","car_model":"Ocean","car_trim":"Base","car_year":2023},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023}],"total":2}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionsGetAll, "/flashpack_versions/:model/:trim/:year", &mock)
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleFleetAdd godoc
// @Summary Add a fleet
// @Description Add a fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param config body FleetRequest true "Fleet data"
// @Success 200 {object} FleetRequest
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet [post]
func HandleFleetAdd(w http.ResponseWriter, r *http.Request) {
fleetCreate.Handle(w, r)
}
var fleetCreate = controllers.NewMongoCreate(&fleetCreateHelper{})
type fleetCreateHelper struct {
fleetHelper
}
func (h *fleetCreateHelper) QueryInsert(model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
fleet, ok := model.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
}
return client.GetFleets().AddFleet(fleet)
}
type FleetRequest struct {
Name string `json:"name"`
LogLevel common.LogLevel `json:"log_level" bson:"log_level"`
CANBus common.CANBus `json:"canbus" bson:"canbus"`
IDPSEnabled bool `json:"idps_enabled" bson:"idps_enabled"`
}
type fleetHelper struct{}
func (h *fleetHelper) NewModel() interface{} {
return &mongo.Fleet{}
}
func (h *fleetHelper) HasPK(filter interface{}) bool {
return filter.(*mongo.Fleet).Name != ""
}
func (h *fleetHelper) ValidatePK(model interface{}) error {
result := model.(*mongo.Fleet)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,45 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"required required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", mongo.Fleet{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"required required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", mongo.Fleet{Name: "TEST-FLEET"}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"TEST-FLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetAdd, "/fleet")
}

View File

@@ -0,0 +1,68 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetDelete godoc
// @Summary Delete fleet
// @Description Delete fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [delete]
func HandleFleetDelete(w http.ResponseWriter, r *http.Request) {
fleetDelete.Handle(w, r)
}
var fleetDelete = controllers.NewMongoDelete(&fleetDeleteHelper{})
type fleetDeleteHelper struct {
fleetHelper
}
func (h *fleetDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} {
var req = &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetDeleteHelper) ValidateFields(model interface{}) error {
p := model.(*mongo.Fleet)
err := validator.ValidateField(p.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetDeleteHelper) QueryDelete(model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
return client.GetFleets().DeleteFleet(model.(*mongo.Fleet))
}
type FleetDeleteRequest struct {
Name string `validate:"required,fleet"`
}

View File

@@ -0,0 +1,33 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFilterDelete(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewVehiclesCollection(&mongo.MockCollection{})
client.SetVehicles(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/fleet/TESTFLEET", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetDelete, "/fleet/:name")
}

View File

@@ -0,0 +1,128 @@
package handlers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterAdd godoc
// @Summary Add CAN filter for fleet
// @Description Add CAN filter for fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param config body common.CANFilter true "CAN filter"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter [post]
func HandleFleetFilterAdd(w http.ResponseWriter, r *http.Request) {
fleetFilterAdd.Handle(w, r)
}
var fleetFilterAdd = controllers.NewMongoUpdate(&fleetFilterAddHelper{})
type fleetFilterAddHelper struct{}
func (h *fleetFilterAddHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetFilterAddHelper) ValidateFields(model interface{}) error {
result, ok := model.(*mongo.Fleet)
if !ok {
return nil
}
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterAddHelper) NewModel() interface{} {
return &common.CANFilter{}
}
func (h *fleetFilterAddHelper) ParseRequestBody(r *http.Request, model interface{}) error {
if err := httphandlers.ParseRequest(r, model); err != nil {
return errors.WithMessage(err, "failed to parse request body")
}
p := model.(*common.CANFilter)
if p.EdgeMask == nil && p.Interval == nil {
return &validator.FieldError{
ErrorMsg: "At least one of edge_mask or interval is required",
}
}
if p.EdgeMask != nil && p.Interval != nil {
if (*p.EdgeMask).String() == "" && *p.Interval == 0 ||
(*p.EdgeMask).String() != "" && *p.Interval != 0 {
return &validator.FieldError{
ErrorMsg: "Only one of edge_mask or interval can be specified",
}
}
}
return nil
}
func (h *fleetFilterAddHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
if err = client.GetFleets().AddFilterToFleet(filter.(*mongo.Fleet).Name, model.(*common.CANFilter)); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(filter.(*mongo.Fleet).Name)
}
func ResetFleetVehiclesConfigCache(fleetName string) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
vehicles, err := client.GetFleets().GetVehiclesForFleet(fleetName, "", &queries.PageQueryOptions{})
if err != nil {
return err
}
r := services.RedisClientPool().GetFromPool()
defer r.Close()
if err = cache.RemoveCacheConfigForVehicles(r, vehicles); err != nil {
logger.Warn().Msgf("failed to remove cache config for vehicles: %v", err)
}
return nil
}

View File

@@ -0,0 +1,68 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid fleet",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/$TEST/filter", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid vin parameter",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", common.CANFilter{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Invalid data with can id",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", common.CANFilter{CANID: "123"}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"At least one of edge_mask or interval is required","error":"Bad Request"}`,
},
{
Name: "Invalid data with all fields",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter",
common.CANFilter{CANID: "123", EdgeMask: elptr.ElPtr(common.BinaryHex("123")), Interval: elptr.ElPtr(1)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Only one of edge_mask or interval can be specified","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(
http.MethodPost, "http://example.com/fleet/US-TEST/filter",
common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"can_id":"123","interval":100}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterAdd, "/fleet/:name/filter")
}

View File

@@ -0,0 +1,74 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterDelete godoc
// @Summary Delete filter from fleet
// @Description Delete filter from fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param id path string true "CAN ID"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter/{id} [delete]
func HandleFleetFilterDelete(w http.ResponseWriter, r *http.Request) {
fleetFilterDelete.Handle(w, r)
}
var fleetFilterDelete = controllers.NewMongoDelete(&fleetFilterDeleteHelper{})
type fleetFilterDeleteHelper struct{}
func (h *fleetFilterDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} {
req := &FleetFilterDeleteParams{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
req.CANID = params.ByName("id")
return req
}
func (h *fleetFilterDeleteHelper) ValidateFields(model interface{}) error {
result := model.(*FleetFilterDeleteParams)
err := validator.ValidateStruct(result)
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterDeleteHelper) QueryDelete(filter interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
f := filter.(*FleetFilterDeleteParams)
if err = client.GetFleets().DeleteFilterFromFleet(f.Name, f.CANID); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(f.Name)
}
type FleetFilterDeleteParams struct {
Name string `validate:"fleet"`
CANID string `validate:"can_id"`
}

View File

@@ -0,0 +1,33 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetFilterDelete(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/fleet/US-TEST/filter/123", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterDelete, "/fleet/:name/filter/:id")
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterGetList godoc
// @Summary Get filters for fleet
// @Description Get filters for fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filters [get]
func HandleFleetFilterGetList(w http.ResponseWriter, r *http.Request) {
fleetFilterGetList.Handle(w, r)
}
var fleetFilterGetList = controllers.NewMongoGetList(&fleetFilterGetListHelper{})
type fleetFilterGetListHelper struct{}
func (h *fleetFilterGetListHelper) NewModel() interface{} {
return &mongo.Fleet{}
}
func (h *fleetFilterGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) {
filter := model.(*mongo.Fleet)
params := httprouter.ParamsFromContext(r.Context())
filter.Name = params.ByName("name")
}
func (h *fleetFilterGetListHelper) ValidateStruct(model interface{}) error {
result := model.(*mongo.Fleet)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) {
// does not utilize URL queries so leave this function empty
}
func (h *fleetFilterGetListHelper) QueryCount(filter interface{}) (int64, error) {
client, err := services.GetMongoClient()
if err != nil {
return 0, err
}
return client.GetFleets().GetFiltersForFleetCount(filter.(*mongo.Fleet).Name)
}
func (h *fleetFilterGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
return client.GetFleets().GetFiltersForFleet(filter.(*mongo.Fleet).Name, options)
}

View File

@@ -0,0 +1,75 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterGetList(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "US-TEST",
CANBus: common.CANBus{
Filters: []common.CANFilter{
{
CANID: "123",
Interval: elptr.ElPtr(100),
},
},
},
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid name parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/$TEST/filters", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"can_id":"123","interval":100}]}`,
},
{
Name: "Valid data limit 50",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=50", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"can_id":"123","interval":100}]}`,
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterGetList, "/fleet/:name/filters")
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
)
// HandleFleetFilterUpdate godoc
// @Summary Update a fleet filter
// @Description Update a fleet filter
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param id path string true "CAN ID"
// @Param config body common.CANFilter true "Fleet filter data"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter/{id} [put]
func HandleFleetFilterUpdate(w http.ResponseWriter, r *http.Request) {
fleetFilterUpdate.Handle(w, r)
}
var fleetFilterUpdate = controllers.NewMongoUpdate(&fleetFilterUpdateHelper{})
type fleetFilterUpdateHelper struct{}
func (h *fleetFilterUpdateHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &FleetFilterUpdateParams{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
req.CANID = params.ByName("id")
return req
}
func (h *fleetFilterUpdateHelper) ValidateFields(model interface{}) error {
result, ok := model.(*FleetFilterUpdateParams)
if !ok {
return nil
}
err := validator.ValidateStruct(result)
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterUpdateHelper) NewModel() interface{} {
return &common.CANFilter{}
}
func (h *fleetFilterUpdateHelper) ParseRequestBody(r *http.Request, model interface{}) error {
if err := httphandlers.ParseRequest(r, model); err != nil {
return errors.WithMessage(err, "failed to parse request body")
}
p := model.(*common.CANFilter)
if p.EdgeMask == nil && p.Interval == nil {
return &validator.FieldError{
ErrorMsg: "At least one of edge_mask or interval is required",
}
}
if p.EdgeMask != nil && p.Interval != nil {
if (*p.EdgeMask).String() == "" && *p.Interval == 0 ||
(*p.EdgeMask).String() != "" && *p.Interval != 0 {
return &validator.FieldError{
ErrorMsg: "Only one of edge_mask or interval can be specified",
}
}
}
return nil
}
func (h *fleetFilterUpdateHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
f := filter.(*FleetFilterUpdateParams)
if err = client.GetFleets().UpdateFilterForFleet(f.Name, f.CANID, model.(*common.CANFilter)); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(f.Name)
}
type FleetFilterUpdateParams struct {
Name string `validate:"fleet"`
CANID string `validate:"can_id"`
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterUpdate(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{Interval: elptr.ElPtr(0)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"can_id":"123","interval":100}`,
},
{
Name: "Invalid data with can id",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter/123", common.CANFilter{CANID: "123"}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"At least one of edge_mask or interval is required","error":"Bad Request"}`,
},
{
Name: "Invalid data with all fields",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{CANID: "123", EdgeMask: elptr.ElPtr(common.BinaryHex("123")), Interval: elptr.ElPtr(1)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Only one of edge_mask or interval can be specified","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterUpdate, "/fleet/:name/filter/:id")
}

View File

@@ -0,0 +1,63 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
// HandleFleetGet godoc
// @Summary Get fleet
// @Description Get fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Success 200 {object} mongo.Fleet
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [get]
func HandleFleetGet(w http.ResponseWriter, r *http.Request) {
fleetGet.Handle(w, r)
}
var fleetGet = controllers.NewMongoGetModel(&fleetGetModelHelper{})
type fleetGetModelHelper struct {
fleetHelper
}
func (h *fleetGetModelHelper) ParseGetURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetGetModelHelper) Query(filter interface{}) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
fleet, ok := filter.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
}
return client.GetFleets().FindFleet(filter.(*mongo.Fleet))
}
type FleetFilterParams struct {
Name string `json:"name" validate:"required,fleet"`
}

View File

@@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
)
// HandleFleetGetList godoc
// @Summary Get list of fleets
// @Description Get list of fleets
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param tags query string false "Tags associated with fleet"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult{data=[]mongo.Fleet}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleets [get]
func HandleFleetGetList(w http.ResponseWriter, r *http.Request) {
fleetGetList.Handle(w, r)
}
var fleetGetList = controllers.NewMongoGetList(&fleetGetListHelper{})
var fleetListSort = map[string]string{
"canbus_": "canbus.",
}
type fleetGetListHelper struct {
fleetHelper
}
func (h *fleetGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) {
// does not utilize URL params so leave this function empty
}
func (h *fleetGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) {
filter := model.(*mongo.Fleet)
filter.SetSearchQuery(r.URL.Query().Get("search"))
}
func (h *fleetGetListHelper) ValidateStruct(model interface{}) error { return nil }
func (h *fleetGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
if options != nil {
options.Order = mongo.AdaptOrder(options.Order, fleetListSort)
}
return client.GetFleets().SelectFleets(filter.(*mongo.Fleet), options)
}
func (h *fleetGetListHelper) QueryCount(filter interface{}) (int64, error) {
client, err := services.GetMongoClient()
if err != nil {
return 0, err
}
return client.GetFleets().GetFleetCount(filter.(*mongo.Fleet))
}

View File

@@ -0,0 +1,59 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetGetList(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
FindObject: []mongo.Fleet{
{
Name: "TESTFLEET",
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"name":"TESTFLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}]}`,
},
{
Name: "Valid data with max limit",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=100", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"name":"TESTFLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}]}`,
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetGetList, "/fleets")
}

View File

@@ -0,0 +1,50 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetGet(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "TESTFLEET",
CANBus: common.CANBus{DTCEnabled: elptr.ElPtr(true)},
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/INVALIDFLEET$", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"fleet fleet ","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/TESTFLEET", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":null},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetGet, "/fleet/:name")
}

View File

@@ -0,0 +1,149 @@
package handlers
import (
"errors"
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
e "github.com/fiskerinc/cloud-services/pkg/mongo/error"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetUpdate godoc
// @Summary Update fleet
// @Description Update fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param config body mongo.Fleet true "Fleet data"
// @Success 200 {object} mongo.Fleet
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [put]
func HandleFleetUpdate(w http.ResponseWriter, r *http.Request) {
fleetUpdate.Handle(w, r)
}
var fleetUpdate = controllers.NewMongoUpdate(&fleetUpdateHelper{})
type fleetUpdateHelper struct {
fleetHelper
}
func (h *fleetUpdateHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetUpdateHelper) ValidateFields(model interface{}) error {
p := model.(*mongo.Fleet)
err := validator.ValidateField(p.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetUpdateHelper) ParseRequestBody(r *http.Request, model interface{}) error {
err := httphandlers.ParseRequest(r, model)
if err != nil {
return err
}
fleet, ok := model.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
model = fleet
}
}
return nil
}
func (h *fleetUpdateHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
flt := model.(*mongo.Fleet)
if flt.CANBus.DTCEnabled == nil {
flt.CANBus.DTCEnabled = elptr.ElPtr(true)
}
err = client.GetFleets().UpdateFleet(filter.(*mongo.Fleet), flt)
if err != nil {
return err
}
fleetVINs, err := client.GetFleets().GetVehiclesForFleet(flt.Name, "", &queries.PageQueryOptions{})
if err != nil {
return err
}
batch := redis.NewRedisBatchCommands()
for _, fleetVIN := range fleetVINs {
v := &mongo.Vehicle{VIN: fleetVIN, LogLevel: flt.LogLevel, CANBus: flt.CANBus, DebugMask: flt.DebugMask, IDPSEnabled: flt.IDPSEnabled}
err = client.GetVehicles().UpdateVehicle(v)
if err != nil && errors.Is(err, e.ErrInvalidNumberOfDocs) {
logger.At(logger.Warn(), fleetVIN, "mongodb").Err(err).Send()
continue
} else if err != nil {
return err
}
batch.Add("DEL", redis.CarConfigKey(fleetVIN))
if flt.CANBus.DTCEnabled != nil {
data := common.TRexConfigResponse{
LogLevel: flt.LogLevel,
CANBus: flt.CANBus,
}
if cache.ENABLE_DEBUG_MASK {
data.DebugMask = flt.DebugMask
}
data.IDPSEnabled = flt.IDPSEnabled
err = batch.AddPublish(common.TRex.Key(fleetVIN), common.Message{
Handler: "config",
Data: data,
})
if err != nil {
return err
}
}
}
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
_, err = conn.ExecuteBatch(batch)
if err != nil {
return err
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More