diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c8669e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 +# Shared Dockerfile for all Go services +# Usage: docker build --build-arg SERVICE=gateway -t gateway . + +ARG SERVICE=gateway + +# Build stage +FROM golang:1.25-alpine AS builder + +ARG SERVICE + +RUN apk add --no-cache git ca-certificates tzdata + +WORKDIR /app + +# Copy module files for dependency caching (don't use go.work in container) +COPY pkg/go.mod pkg/go.sum ./pkg/ +COPY pkg/can-go/go.mod pkg/can-go/go.sum ./pkg/can-go/ +COPY services/${SERVICE}/go.mod services/${SERVICE}/go.sum ./services/${SERVICE}/ + +# Download dependencies (cached layer) +WORKDIR /app/services/${SERVICE} +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download -x + +# Copy source +WORKDIR /app +COPY pkg/ ./pkg/ +COPY services/${SERVICE}/ ./services/${SERVICE}/ + +# Build static binary +WORKDIR /app/services/${SERVICE} +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux \ + go build -ldflags="-s -w" -trimpath -o /app-binary . + +# Runtime stage - distroless for minimal attack surface +FROM gcr.io/distroless/static-debian12:nonroot + +ARG SERVICE + +COPY --from=builder /app-binary /app +COPY --from=builder /app/pkg/logger/log_config /log_config +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo + +# Copy docs if they exist (optional) +COPY --from=builder /app/services/${SERVICE}/docs* /docs/ + +ENV LOG_CONFIG=/log_config +ENV TZ=UTC + +ENTRYPOINT ["/app"] diff --git a/deploy/overlays/development/kustomization.yaml b/deploy/overlays/development/kustomization.yaml index df20d1d..992dd8d 100644 --- a/deploy/overlays/development/kustomization.yaml +++ b/deploy/overlays/development/kustomization.yaml @@ -7,6 +7,11 @@ resources: - ../../base - secrets.yaml - services/gateway/ + - services/depot/ + - services/attendant/ + - services/jetfire/ + - services/optimus/ + - services/ota/ labels: - pairs: diff --git a/deploy/overlays/development/services/attendant/deployment.yaml b/deploy/overlays/development/services/attendant/deployment.yaml new file mode 100644 index 0000000..0949f00 --- /dev/null +++ b/deploy/overlays/development/services/attendant/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: attendant + namespace: cloud-services + labels: + app: attendant + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: attendant + template: + metadata: + labels: + app: attendant + spec: + containers: + - name: attendant + image: localhost:32000/attendant:v1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8077 + name: http + - containerPort: 11011 + name: health + envFrom: + - configMapRef: + name: cloud-common-config + - secretRef: + name: cloud-db-credentials + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + livenessProbe: + httpGet: + path: /liveness + port: 11011 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readiness + port: 11011 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/deploy/overlays/development/services/attendant/kustomization.yaml b/deploy/overlays/development/services/attendant/kustomization.yaml new file mode 100644 index 0000000..8e21f7f --- /dev/null +++ b/deploy/overlays/development/services/attendant/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cloud-services + +resources: + - deployment.yaml diff --git a/deploy/overlays/development/services/depot/deployment.yaml b/deploy/overlays/development/services/depot/deployment.yaml new file mode 100644 index 0000000..a068bd0 --- /dev/null +++ b/deploy/overlays/development/services/depot/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: depot + namespace: cloud-services + labels: + app: depot + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: depot + template: + metadata: + labels: + app: depot + spec: + containers: + - name: depot + image: localhost:32000/depot:v1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8077 + name: http + - containerPort: 11011 + name: health + envFrom: + - configMapRef: + name: cloud-common-config + - secretRef: + name: cloud-db-credentials + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + livenessProbe: + httpGet: + path: /liveness + port: 11011 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readiness + port: 11011 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/deploy/overlays/development/services/depot/kustomization.yaml b/deploy/overlays/development/services/depot/kustomization.yaml new file mode 100644 index 0000000..8e21f7f --- /dev/null +++ b/deploy/overlays/development/services/depot/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cloud-services + +resources: + - deployment.yaml diff --git a/deploy/overlays/development/services/jetfire/deployment.yaml b/deploy/overlays/development/services/jetfire/deployment.yaml new file mode 100644 index 0000000..ed56e24 --- /dev/null +++ b/deploy/overlays/development/services/jetfire/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jetfire + namespace: cloud-services + labels: + app: jetfire + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: jetfire + template: + metadata: + labels: + app: jetfire + spec: + containers: + - name: jetfire + image: localhost:32000/jetfire:v1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8077 + name: http + - containerPort: 11011 + name: health + env: + - name: CLICKHOUSE_HOST + value: clickhouse.clickhouse.svc.cluster.local + - name: CLICKHOUSE_PORT + value: "9000" + - name: CLICKHOUSE_USER + value: default + - name: CLICKHOUSE_FEATURE_TABLE + value: feature_table + - name: CLICKHOUSE_VEHICLE_SIGNAL_TABLE + value: vehicle_signal + envFrom: + - configMapRef: + name: cloud-common-config + - secretRef: + name: cloud-db-credentials + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi + livenessProbe: + httpGet: + path: /liveness + port: 11011 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readiness + port: 11011 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/deploy/overlays/development/services/jetfire/kustomization.yaml b/deploy/overlays/development/services/jetfire/kustomization.yaml new file mode 100644 index 0000000..8e21f7f --- /dev/null +++ b/deploy/overlays/development/services/jetfire/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cloud-services + +resources: + - deployment.yaml diff --git a/deploy/overlays/development/services/optimus/deployment.yaml b/deploy/overlays/development/services/optimus/deployment.yaml new file mode 100644 index 0000000..383818b --- /dev/null +++ b/deploy/overlays/development/services/optimus/deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: optimus + namespace: cloud-services + labels: + app: optimus + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: optimus + template: + metadata: + labels: + app: optimus + spec: + containers: + - name: optimus + image: localhost:32000/optimus:v1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8077 + name: http + - containerPort: 11011 + name: health + env: + - name: CLICKHOUSE_HOST + value: clickhouse.clickhouse.svc.cluster.local + - name: CLICKHOUSE_PORT + value: "9000" + - name: CLICKHOUSE_USER + value: default + envFrom: + - configMapRef: + name: cloud-common-config + - secretRef: + name: cloud-db-credentials + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi + livenessProbe: + httpGet: + path: /liveness + port: 11011 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readiness + port: 11011 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/deploy/overlays/development/services/optimus/kustomization.yaml b/deploy/overlays/development/services/optimus/kustomization.yaml new file mode 100644 index 0000000..8e21f7f --- /dev/null +++ b/deploy/overlays/development/services/optimus/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cloud-services + +resources: + - deployment.yaml diff --git a/deploy/overlays/development/services/ota/deployment.yaml b/deploy/overlays/development/services/ota/deployment.yaml new file mode 100644 index 0000000..a2b00a9 --- /dev/null +++ b/deploy/overlays/development/services/ota/deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ota + namespace: cloud-services + labels: + app: ota + annotations: + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: ota + template: + metadata: + labels: + app: ota + spec: + containers: + - name: ota + image: localhost:32000/ota_update_go:v1 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8077 + name: http + - containerPort: 11011 + name: health + env: + - name: SERVICE_BASE_URL + value: /ota_update + envFrom: + - configMapRef: + name: cloud-common-config + - secretRef: + name: cloud-db-credentials + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 512Mi + livenessProbe: + httpGet: + path: /liveness + port: 11011 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readiness + port: 11011 + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: ota + namespace: cloud-services +spec: + selector: + app: ota + ports: + - port: 8077 + targetPort: 8077 + name: http diff --git a/deploy/overlays/development/services/ota/ingress.yaml b/deploy/overlays/development/services/ota/ingress.yaml new file mode 100644 index 0000000..4415186 --- /dev/null +++ b/deploy/overlays/development/services/ota/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ota + namespace: cloud-services + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + tls: + - hosts: + - gw.mini.cloud.fiskerinc.com + secretName: cloud-services-tls + rules: + - host: gw.mini.cloud.fiskerinc.com + http: + paths: + - path: /ota_update + pathType: Prefix + backend: + service: + name: ota + port: + number: 8077 diff --git a/deploy/overlays/development/services/ota/kustomization.yaml b/deploy/overlays/development/services/ota/kustomization.yaml new file mode 100644 index 0000000..c80c291 --- /dev/null +++ b/deploy/overlays/development/services/ota/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cloud-services + +resources: + - deployment.yaml + - ingress.yaml diff --git a/go.work b/go.work index a4bae62..4d5e4bc 100644 --- a/go.work +++ b/go.work @@ -3,5 +3,10 @@ go 1.25 use ( ./pkg ./pkg/can-go + ./services/attendant + ./services/depot ./services/gateway + ./services/jetfire + ./services/optimus + ./services/ota_update_go ) diff --git a/pkg/kafka/consumer.go b/pkg/kafka/consumer.go index 25a4f60..add6a61 100644 --- a/pkg/kafka/consumer.go +++ b/pkg/kafka/consumer.go @@ -52,6 +52,8 @@ type ConsumerInterface interface { ConsumeToChannel(topics []string, events chan *Message) error ConsumeToChannelJson(topics []string, events chan common.EventRawJSON) error ConsumePartitionsToChannel(partitions []TopicPartition, events chan *Message) error + ConsumeOrRebalancedCatch(topics []string, events chan *Message, rebalance chan struct{}) error + Subscribe(topics []string) GetMetadata(topic string) (*Metadata, error) Check(ctx context.Context) error Stop() @@ -275,6 +277,48 @@ func (c *Consumer) Stop() { } } +// Subscribe adds topics to consume (for compatibility with old API) +func (c *Consumer) Subscribe(topics []string) { + c.client.AddConsumeTopics(topics...) +} + +// ConsumeOrRebalancedCatch consumes messages and notifies on rebalance events +func (c *Consumer) ConsumeOrRebalancedCatch(topics []string, events chan *Message, rebalance chan struct{}) error { + c.client.AddConsumeTopics(topics...) + c.setConnected(true) + defer c.setConnected(false) + + c.running = true + for c.running { + fetches := c.client.PollFetches(c.ctx) + if errs := fetches.Errors(); len(errs) > 0 { + for _, e := range errs { + if e.Err == context.Canceled { + return nil + } + // Check for rebalance-related errors + if e.Err.Error() == "REBALANCE_IN_PROGRESS" || e.Err.Error() == "NOT_COORDINATOR" { + select { + case rebalance <- struct{}{}: + default: + } + return e.Err + } + logger.Error().Err(e.Err).Msgf("fetch error on %s", e.Topic) + c.setConnected(false) + } + continue + } + + c.setConnected(true) + fetches.EachRecord(func(r *kgo.Record) { + events <- recordToMessage(r) + }) + } + + return nil +} + // Check verifies consumer connectivity func (c *Consumer) Check(ctx context.Context) error { if !c.isConnected() { diff --git a/pkg/loggerdataresp/data_error_resp.go b/pkg/loggerdataresp/data_error_resp.go index bbeff20..dd23abd 100644 --- a/pkg/loggerdataresp/data_error_resp.go +++ b/pkg/loggerdataresp/data_error_resp.go @@ -14,7 +14,6 @@ import ( "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils" - cKafka "github.com/confluentinc/confluent-kafka-go/v2/kafka" "github.com/go-pg/pg/v10" "github.com/pkg/errors" ) @@ -106,10 +105,6 @@ func badDataErrorResp(w http.ResponseWriter, err error, defaultStatus int, errFu return true } - if handleKafkaErrors(err, errFunc, w) { - return true - } - logError(err, err.Error(), defaultStatus, errFunc, w) return true @@ -169,17 +164,6 @@ func handleSAPErrors(err error, errFunc respErr, w http.ResponseWriter) bool { return false } -func handleKafkaErrors(err error, errFunc respErr, w http.ResponseWriter) bool { - if kafkaErr, ok := err.(cKafka.Error); ok { - if kafkaErr.IsFatal() { - logError(kafkaErr, kafkaErr.Error(), http.StatusServiceUnavailable, errFunc, w) - return true - } - } - - return false -} - func logError(err error, errMessage string, status int, errFunc respErr, w http.ResponseWriter) { switch status { case http.StatusServiceUnavailable, http.StatusInternalServerError: diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..235fb17 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Build and optionally deploy a service +# Usage: ./scripts/build.sh [--deploy] [version] +# Example: ./scripts/build.sh depot --deploy v1 + +set -e + +SERVICE=${1:-gateway} +DEPLOY=${2:-} +VERSION=${3:-v1} +REGISTRY="localhost:32000" +TAG="$VERSION" + +echo "Building $SERVICE..." +docker build --platform linux/arm64 \ + --build-arg SERVICE=$SERVICE \ + -t $REGISTRY/$SERVICE:$TAG \ + -f Dockerfile . + +if [ "$DEPLOY" == "--deploy" ]; then + echo "Saving image..." + docker save $REGISTRY/$SERVICE:$TAG -o /tmp/$SERVICE.tar + + echo "Transferring to cluster..." + scp /tmp/$SERVICE.tar admin@control-plane.local:/tmp/ + + echo "Importing to microk8s..." + ssh admin@control-plane.local "/usr/local/bin/multipass transfer /tmp/$SERVICE.tar microk8s-vm:/tmp/" + ssh admin@control-plane.local "/usr/local/bin/multipass exec microk8s-vm -- microk8s ctr images rm $REGISTRY/$SERVICE:$TAG 2>/dev/null || true" + ssh admin@control-plane.local "/usr/local/bin/multipass exec microk8s-vm -- microk8s ctr images import /tmp/$SERVICE.tar" + + echo "Restarting deployment..." + ssh admin@control-plane.local "/usr/local/bin/multipass exec microk8s-vm -- microk8s kubectl rollout restart deployment/$SERVICE -n cloud-services" + + echo "Done!" +fi diff --git a/services/attendant/Dockerfile b/services/attendant/Dockerfile new file mode 100644 index 0000000..c9e52bf --- /dev/null +++ b/services/attendant/Dockerfile @@ -0,0 +1,25 @@ +ARG BASE_IMAGE=cloud_base_go +FROM ${BASE_IMAGE} as builder-go + +WORKDIR /build/attendant +COPY ./attendant/go.mod ./attendant/go.sum ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go mod download + +COPY ./attendant ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && 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/attendant/attendant . + +ENV LOG_CONFIG=log_config +EXPOSE 8077 + +CMD ./attendant diff --git a/services/attendant/controllers/car_update_progress.go b/services/attendant/controllers/car_update_progress.go new file mode 100644 index 0000000..bcb0dc1 --- /dev/null +++ b/services/attendant/controllers/car_update_progress.go @@ -0,0 +1,518 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/common" + s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/manifestsender" + "github.com/fiskerinc/cloud-services/pkg/redis" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" + + "github.com/fiskerinc/cloud-services/pkg/hwversion" + "github.com/go-pg/pg/v10" + r "github.com/gomodule/redigo/redis" + "github.com/pkg/errors" +) + +const redisObjectExpire = 3600 + +const ( + PackageDownloadStart = "package_download_start" + PackageDownloadComplete = "package_download_complete" + PackageInstallStart = "package_install_start" + PackageInstallComplete = "package_install_complete" + InstallError = "install_error" +) + +var RepeatedStatus = errors.New("RepeatedStatus") + +// CarUpdateProgress takes in a car update message and saves it to our database +// This includes setting the status of a car update, and telling the car and SAP that the update is done +func NewCarUpdateProgress(clientPool redis.ClientPoolInterface, ka *services.KeepAwake, db *services.DB, device common.Device) CarUpdateProgressInterface { + if device == common.TRex { + return &CarUpdateProgress{ + RedisClientPool: clientPool, + DB: db, + ka: ka, + } + } + + if device == common.HMI { + return &HMICarUpdateProgress{ + conf: services.GetVehicleConfig(), + sms: services.GetSMSClient(), + ka: ka, + CarUpdateProgress: CarUpdateProgress{ + RedisClientPool: clientPool, + DB: db, + ka: ka, + }, + } + } + + return nil +} + +type CarUpdateProgressInterface interface { + Process(vin string, data []byte) error + ProcessStatus(vin string, status common.CarUpdateProgress) error + Dispose() +} + +type CarUpdateProgress struct { + RedisClientPool redis.ClientPoolInterface + DB *services.DB + ka *services.KeepAwake +} + +func (cu *CarUpdateProgress) Process(vin string, data []byte) error { + var status common.CarUpdateProgress + + err := json.Unmarshal(data, &status) + if err != nil { + return err + } + + return cu.ProcessStatus(vin, status) +} + +func (cu *CarUpdateProgress) ProcessStatus(vin string, status common.CarUpdateProgress) (err error) { + if cu.transformDBCarUpdateProgress(&status) { + err = cu.logStatusDB(status) + // If the error is the repeated, we can just exit early + if err != nil { + if errors.Is(err, queries.RepeatedStatus) { + err = nil + } + return + } + } + + cu.cancelTheCANAwake(vin, status) + + batch := redis.NewRedisBatchCommands() + + cu.transformRedisCarUpdateProgress(&status) + cu.BatchCacheRedis(batch, redis.CarUpdateStatusHashKey(status.CarUpdateID), &status) + + client := cu.RedisClientPool.GetFromPool() + defer client.Close() + _, err = client.ExecuteBatch(batch) + if err != nil { + return err + } + + // do not send car update status for internal cloud statuses + if cu.isInternalStatus(status) { + return nil + } + + msg := cu.getMessage(&status) + err = cu.publishStatusHMI(vin, &msg) + if err != nil { + return err + } + + msgMobile := cu.getMessageForMobile(&status, vin) + err = cu.publishStatusMobile(vin, &msgMobile) + if err != nil { + return err + } + + err = cu.onUpdateManifestComplete(&status, vin) + + return err +} + +// We will try and cancel sending CAN status stuff +func (cu *CarUpdateProgress) cancelTheCANAwake(vin string, status common.CarUpdateProgress) { + switch status.Status { + case s.DownloadFailed, s.InstallFailed, s.ManifestCancelAccepted, s.ManifestCancelRejected, + s.ManifestError, s.ManifestRejected, s.ManifestValidationFailed, s.RequirementsFailed, s.ManifestCanceled: + logger.Info().Msgf("canceling CAN Awake for %s because %s", vin, status.Status) + cu.ka.RemoveKeepAwakeMessage(vin) + } +} + +func (cu *CarUpdateProgress) isInternalStatus(status common.CarUpdateProgress) bool { + return status.Status == s.Sent || status.Status == s.Pending +} + +func (cu *CarUpdateProgress) transformDBCarUpdateProgress(status *common.CarUpdateProgress) bool { + switch status.Status { + case s.DownloadStarted: + if status.PackageCurrent == 0 { + status.Status = PackageDownloadStart + } + return true + case s.DownloadCompleted: + if status.PackageCurrent == status.PackageTotal { + status.Status = PackageDownloadComplete + } + return true + case s.InstallStarted: + if status.InstalledFiles == 0 && status.TotalFiles > 0 { + status.Status = PackageInstallStart + } + return true + case s.InstallSucceeded: + if status.InstalledFiles == status.TotalFiles && status.TotalFiles > 0 { + status.Status = PackageInstallComplete + } + return true + case InstallError: + status.Status = s.InstallFailed + return true + case s.Installing: + return false + case s.Downloading: + // these status updates do not need to be saved in the database + return false + } + + return true +} + +func (cu *CarUpdateProgress) transformRedisCarUpdateProgress(status *common.CarUpdateProgress) { + switch status.Status { + case s.DownloadStarted, s.DownloadCompleted, PackageDownloadStart: + status.Status = s.Downloading + case s.InstallStarted, s.InstallSucceeded, PackageInstallStart: + status.Status = s.Installing + } +} + +func (cu *CarUpdateProgress) logStatusDB(status common.CarUpdateProgress) (err error) { + // If we are one of these status's we want to ignore, then we need to do some extra database steps, otherwise insert normally + carUpdate := common.CarUpdate{ + ID: status.CarUpdateID, + Status: status.Status, + ErrorCode: status.ErrorCode, + Info: strings.TrimSpace(fmt.Sprintf("%s %s", status.ECU, status.Info)), + } + + if _, ok := s.NoRepeatUpdateStatus[status.Status]; ok { + _, err = cu.DB.GetCarUpdates().UpdateStatusIfNotRepeat(&carUpdate) + return + } + _, err = cu.DB.GetCarUpdates().UpdateStatus(&carUpdate) + return err +} + +func (cu *CarUpdateProgress) GetCache(key string) (*common.CarUpdateProgress, error) { + client := cu.RedisClientPool.GetFromPool() + defer client.Close() + status := common.CarUpdateProgress{} + err := client.GetObject(key, &status) + return &status, err +} + +func (cu *CarUpdateProgress) BatchCacheRedis(batch *redis.RedisBatchCommands, key string, status *common.CarUpdateProgress) { + batch.Add(r.Args{}.Add("HSET").Add(key).AddFlat(status)...) + batch.Add("EXPIRE", key, redisObjectExpire) +} + +func (cu *CarUpdateProgress) getMessage(status *common.CarUpdateProgress) common.Message { + return common.Message{ + Handler: "car_update_status", + Data: status, + } +} + +func (cu *CarUpdateProgress) getMessageForMobile(status *common.CarUpdateProgress, vin string) common.Message { + type mobileData struct { + VIN string `json:"vin"` + *common.CarUpdateProgress + } + + return common.Message{ + Handler: "car_update_status", + Data: mobileData{vin, status}, + } +} + +func (cu *CarUpdateProgress) publishStatusHMI(vin string, msg *common.Message) error { + client := cu.RedisClientPool.GetFromPool() + defer client.Close() + // redis publish to HMI + hmiKey := common.HMI.Key(vin) + // Add VIN + err := client.SafePublishMessage(hmiKey, msg) + return err +} + +func (cu *CarUpdateProgress) publishStatusMobile(vin string, msg *common.Message) error { + drivers := cache.NewDriversCache(cu.RedisClientPool, cu.DB.GetCars()) + + // redis publish to mobile devices + driverIDs, err := drivers.RetrieveDriverIDs(vin) + if err != nil { + return err + } + + // Change thos for loop to isntead create a batch and execute it all at once + client := cu.RedisClientPool.GetFromPool() + defer client.Close() + for _, d := range driverIDs { + mobileKey := common.Mobile.Key(d) + err = client.SafePublishMessage(mobileKey, msg) + if err != nil { + return err + } + } + + return nil +} + +func (cu *CarUpdateProgress) Dispose() { + cu.DB = nil +} + +type HMICarUpdateProgress struct { + conf vconfig.ConfigServiceInterface + sms sms.SMSServiceClient + ka *services.KeepAwake + CarUpdateProgress +} + +func (h *HMICarUpdateProgress) Process(vin string, data []byte) error { + var status common.CarUpdateProgress + + err := json.Unmarshal(data, &status) + if err != nil { + return err + } + + if h.downloadComplete(&status) { + // stop calling the sendKeepAwakeMessage + h.ka.RemoveKeepAwakeMessage(vin) + _, err = h.sendManifestToTRex(vin, &status) + if err != nil { + return err + } + h.logStatusDB(common.CarUpdateProgress{ + CarUpdateID: status.CarUpdateID, + Status: s.Sent, + Info: "TBOX", + }) + } + + return h.ProcessStatus(vin, status) +} + +func (h *HMICarUpdateProgress) downloadComplete(status *common.CarUpdateProgress) bool { + return status.Status == s.DownloadCompleted +} + +func (h *HMICarUpdateProgress) getManifest(status *common.CarUpdateProgress) (*common.UpdateManifest, error) { + update := common.CarUpdate{ID: status.CarUpdateID} + err := h.DB.GetCarUpdates().Load(&update) + if err != nil { + return nil, err + } + + update.UpdateManifest.CarUpdateID = status.CarUpdateID + + return update.UpdateManifest, nil +} + +func (h *HMICarUpdateProgress) sendManifestToTRex(vin string, status *common.CarUpdateProgress) (msgID string, err error) { + logger.Info().Msgf("HMICarUpdateProgress sendManifestToTRex car_update_id %d", status.CarUpdateID) + manifest, err := h.getManifest(status) + if err != nil { + return + } + + if !manifest.HasSelfDownload() { + logger.Error().Msgf("%s download_completed for non-self-download manifest", vin) + return + } + + err = hwversion.SetHWVersion(manifest, vin, services.GetDB().GetCars()) + if err != nil { + // An error here is very unexpected. The hw versioning should have been confirmed earlier before ICC was updated + err = errors.WithStack(err) + logger.Err(err).Str("VIN", vin).Int64("UpdateID", status.CarUpdateID).Msg("failed to set hw versions for a manifest after ICC complete update") + err = nil + } + manifest.SortECUs() + manifest.FilterCompatibleECUs(vin) + + // This code is going to be removed by mny other PR so not going to mess with it for now + client := h.RedisClientPool.GetFromPool() + defer client.Close() + trex := manifestsender.NewTBOXManifestSender(client, h.conf, h.DB, h.sms, nil) + defer trex.Close() + + msgID, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData()) + return +} + +func (h *HMICarUpdateProgress) GetRedisHashKey(status *common.CarUpdateProgress) string { + return redis.CarUpdateStatusHMIHashKey(status.CarUpdateID) +} + +// Car Update Done +func (cu *CarUpdateProgress) onUpdateManifestComplete(status *common.CarUpdateProgress, vin string) (err error) { + success := false + final := false + submitSAP := false + + switch status.Status { + case s.ManifestSucceeded: + success = true + submitSAP = true + final = true + case s.ManifestCanceled, s.ManifestError, s.ManifestRejected: + success = false + submitSAP = true + final = true + case s.DownloadFailed, s.ManifestCancelPending, s.RollbackSucceeded, s.RollbackFailed, s.CleanupSucceeded: + final = true + default: + return nil + } + + carUpdatesDB := cu.DB.GetCarUpdates() + carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID) + if err != nil { + err = errors.WithStack(err) + return + } + if carUpdate != nil { + // Notify car user of in progress update through FOA API + fs := services.GetFoaService() + foaResp, err := fs.OtaUpdateStatus(vin, carUpdate, status) + 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 final state %s for %s failed with http status %d and message %s", carUpdate.UpdateManifestID, status.Status, vin, foaResp.StatusCode, bodyString) + err = nil + } + } + + logger.Info().Msgf("Manifest update completed for %s with status of %s", vin, status.Status) + + if submitSAP { + logger.Info().Msg("SAP: No Longer Submit Updates") + // sap := services.GetSapService() + // err = sap.SubmitResult(vin, success) + // if err != nil { + // requestBody := struct { + // VIN string + // Success bool + // CarUpdateProgress common.CarUpdateProgress + // }{VIN: vin, Success: success, CarUpdateProgress: *status} + // logger.Err(err).Interface("body", requestBody).Msgf("failed to call sap submit result") + // err = nil + // } + } + + if success { + // If we are successful, we want to possibly update the cars sums version + // Need to pull the manifest to check it has a sums version, and then update the car + err = cu.updateCarsSUMSVersion(status) + if err != nil { + logger.Err(err).Msgf("failed to update car sums version for manifest with CarUpdateID %d", status.CarUpdateID) + err = nil + } + + // Send the read_ecu_versions remote command so that the ECU data is updated in postgres ASAP + client := services.RedisClientPool().GetFromPool() + defer client.Close() + err = client.SafePublishMessage( + common.TRex.Key(vin), + common.Message{ + Handler: "read_ecu_versions", + Data: common.RemoteReadVersionsCommandArgs{ + ECUName: "*", + }, + }, + ) + if err != nil { + logger.Err(err).Msgf("failed to send read_ecu_versions command to vin %s", vin) + err = nil + } + } + + if final { + // if the manifest is in a final state + // then delete the redundant requirements_await rows from car_update_statuses, to avoid overcrowding the table + err = cu.truncateRequirementsAwaitForUpdate(status) + if err != nil { + logger.Err(err).Msgf("failed to delete redundant requirements_await rows from car_update_statuses for manifest with CarUpdateID %d", status.CarUpdateID) + err = nil + } + } + + return err +} + +func (cu *CarUpdateProgress) truncateRequirementsAwaitForUpdate(status *common.CarUpdateProgress) error { + logger.Info().Msgf("Manifest with CarUpdateID %d successful with status %s. Deleting redundant requirements_await rows from car_update_statuses", status.CarUpdateID, status.Status) + + _, err := cu.DB.GetCarUpdates().TruncateRequirementsAwaitForUpdate(status.CarUpdateID) + if err != nil && !errors.Is(err, pg.ErrNoRows) { + return err + } + + return nil +} + +// Find the car update, and it gives the update manifest +// If the manifest has a sums version, apply it to the car +func (cu *CarUpdateProgress) updateCarsSUMSVersion(status *common.CarUpdateProgress) (err error) { + carUpdatesDB := cu.DB.GetCarUpdates() + carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID) + if err != nil { + err = errors.WithStack(err) + return + } + + if carUpdate.UpdateManifest == nil { + err = errors.New("failed to pull car updates update manifest") + return + } + + um := carUpdate.UpdateManifest + // So if we have have a sums version we want to update + if um.SUMS == "" { + return + } + + carsDB := cu.DB.GetCars() + filter := common.Car{ + VIN: carUpdate.VIN, + } + + cars, err := carsDB.Select(&filter, nil) + if err != nil { + err = errors.WithStack(err) + return + } + + if len(cars) != 1 { + err = fmt.Errorf("did not receive only one car, received: %d", len(cars)) + err = errors.WithStack(err) + return + } + + car := cars[0] + car.SUMSVersion = um.SUMS + _, err = carsDB.Update(&car) + if err != nil { + err = errors.WithStack(err) + } + return +} diff --git a/services/attendant/controllers/dtc_request.go b/services/attendant/controllers/dtc_request.go new file mode 100644 index 0000000..6e1e3c1 --- /dev/null +++ b/services/attendant/controllers/dtc_request.go @@ -0,0 +1,184 @@ +package controllers + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/services" + "encoding/base64" + "encoding/json" + "errors" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor" +) + +type DTCEntry struct { + CreatedAt time.Time `bson:"created_at"` + VIN string `bson:"vin"` + ECU string `json:"ecu" bson:"ecu"` + DTC uint64 `json:"dtc" bson:"dtc"` + Status uint8 `json:"status" bson:"status"` + Timestamp time.Time `json:"timestamp" bson:"timestamp"` + Speed uint16 `json:"speed" bson:"speed"` + Mileage uint32 `json:"mileage" bson:"mileage"` + Voltage uint16 `json:"voltage" bson:"voltage"` + + SnapshotBase64 string `json:"snapshot,omitempty" bson:"snapshot,omitempty"` +} + +func GRPCToDTCEntry(payload *kafka_grpc.GRPC_AttendantPayload) []byte { + + if payload.Data == nil { + return nil + } + data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_DtcEntry) + if data == nil { + return nil + } + + dtc := &DTCEntry{ + VIN: data.DtcEntry.Vin, + ECU: data.DtcEntry.Ecu, + CreatedAt: milliToDate(data.DtcEntry.CreatedAt), + DTC: data.DtcEntry.Dtc, + Status: uint8(data.DtcEntry.Status), + Timestamp: milliToDate(data.DtcEntry.Timestamp), + Speed: uint16(data.DtcEntry.Speed), + Mileage: data.DtcEntry.Mileage, + Voltage: uint16(data.DtcEntry.Volt), + SnapshotBase64: data.DtcEntry.SnapshotBase64, + } + bytes, _ := json.Marshal(dtc) + return bytes +} +func milliToDate(timestamp int64) time.Time { + seconds := timestamp / 1000 + nanoseconds := (timestamp % 1000) * int64(time.Millisecond) + return time.Unix(seconds, nanoseconds) +} + +const SNAPSHOT_LEN = 21 + +func (entry *DTCEntry) ParseSnapshot() error { + + data, errc := base64.StdEncoding.DecodeString(entry.SnapshotBase64) + if errc != nil { + return errc + } + payloadLen := len(data) - 2 + + if payloadLen < SNAPSHOT_LEN { + logger.Debug().Msgf("DTC snapshot payload is too small. Required length=%d, actual=%d, ECU=%s\n", SNAPSHOT_LEN, len(data), entry.ECU) + return errors.New("Snapshot too small") + } + + // Our "Basic Diagnostic" spec defines 4 mandatory DIDs that must be + // stored inside DTCStapshotRecord: + // 1. EF F6 - Timestamp (6 bytes). + // 2. EF F7 - Vehicle Speed (2 bytes). + // 3. EF F8 - Milage (idk, our spec doesn't specify exact number. + // Empirically it's 4 bytes (same as "ICC_0x531::TotMilg_ODO"). + // 4. EF F9 - Battery Voltage (2 bytes). + ///entry.Speed = int16(speed.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1])))) + //entry.Voltage = int16(voltage.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1])))) + + for idx := 2; idx < len(data)-1; { + if data[idx] != 0xEF { + idx++ + continue + } + + if data[idx+1] == 0xF6 && idx+7 < len(data) { + idx += 2 + entry.Timestamp, idx = consumeTimestamp(data, idx) + } else if data[idx+1] == 0xF7 && idx+3 < len(data) { + idx += 2 + entry.Speed, idx = consumeSpeed(data, idx) + } else if data[idx+1] == 0xF8 && idx+5 < len(data) { + idx += 2 + entry.Mileage, idx = consumeMileage(data, idx) + } else if data[idx+1] == 0xF9 && idx+3 < len(data) { + idx += 2 + entry.Voltage, idx = consumeVoltage(data, idx) + } else { + idx++ + } + } + return nil +} + +var onceTimestamp sync.Once +var ( + day *descriptor.Signal + hr *descriptor.Signal + mins *descriptor.Signal + yr *descriptor.Signal + month *descriptor.Signal + sec *descriptor.Signal +) + +func consumeTimestamp(data []byte, idx int) (res time.Time, idx_ret int) { + + onceTimestamp.Do(func() { + day, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Day") + hr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Hr") + mins, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mins") + yr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Yr") + month, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mth") + sec, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Sec") + + }) + + res = time.Date(int(yr.ToPhysical(float64(data[idx]))), + time.Month(month.ToPhysical(float64(data[idx+1]))), + int(day.ToPhysical(float64(data[idx+2]))), + int(hr.ToPhysical(float64(data[idx+3]))), + int(mins.ToPhysical(float64(data[idx+4]))), + int(sec.ToPhysical(float64(data[idx+5]))), 0, time.UTC) + idx_ret = idx + 6 + return res, idx_ret +} + +var onceSpeed sync.Once +var speed *descriptor.Signal + +func consumeSpeed(data []byte, idx int) (res uint16, idx_ret int) { + + onceSpeed.Do(func() { + speed, _ = services.GetDBC().Signal(0x318, "ESP_VehSpd") + }) + res = uint16(speed.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1])))) + idx_ret = idx + 2 + return res, idx_ret +} + +var onceMileage sync.Once +var mileage *descriptor.Signal + +func consumeMileage(data []byte, idx int) (res uint32, idx_ret int) { + onceMileage.Do(func() { + mileage, _ = services.GetDBC().Signal(0x531, "ICC_TotMilg_ODO") + }) + res = uint32(mileage.ToPhysical( + float64( + uint64(data[idx])<<24 | + uint64(data[idx+1])<<16 | + uint64(data[idx+2])<<8 | uint64(data[idx+3])))) + idx_ret = idx + 4 + return res, idx_ret +} + +var onceVoltage sync.Once +var voltage *descriptor.Signal + +func consumeVoltage(data []byte, idx int) (res uint16, idx_ret int) { + onceVoltage.Do(func() { + voltage, _ = services.GetDBC().Signal(0x507, "VCU_BattVolt") + }) + + res = uint16(voltage.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1])))) + idx_ret = idx + 2 + return res, idx_ret + +} diff --git a/services/attendant/controllers/dtc_request_test.go b/services/attendant/controllers/dtc_request_test.go new file mode 100644 index 0000000..7b2c9c8 --- /dev/null +++ b/services/attendant/controllers/dtc_request_test.go @@ -0,0 +1,60 @@ +package controllers_test + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "encoding/json" + "fmt" + "testing" +) + +func TestParsing(t *testing.T) { + ecc := []byte(`{"ecu":"ECC","dtc":1719200,"status":9,"snapshot":"AQTv9ucIARIKNO/3AADv+AAAAADv+TLI"}`) + icc := []byte(`{"ecu":"ICC","dtc":14302599,"status":9,"snapshot":"AQUFAAHv+AAAAADv+S2W7/cAAO/2B+cIChAB"}`) + gw := []byte(`{"ecu":"GW","dtc":10718486,"status":8,"snapshot":"AQTv+QLA7/cAAO/2CAcHFAMs7/gAAAAA"}`) + mcu := []byte(`{"ecu":"MCU","dtc":14123795,"status":47,"snapshot":"AQTv9ggHCRcBD+/3AADv+AAAAADv+TLI"}`) + + check := func(ecu []byte) { + var entry controllers.DTCEntry + err := json.Unmarshal(ecu, &entry) + if err != nil { + t.Error(err) + } + entry.ParseSnapshot() + if entry.Mileage != 0 { + t.Errorf("Incorrect Mileage %d", entry.Mileage) + } + + fmt.Println(entry.Timestamp.Unix()) + switch entry.ECU { + case "ECC": + if entry.Timestamp.Unix() != 8730871852 { + t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix()) + } + if entry.Voltage != 13 { + t.Errorf("Incorrect Voltage %d", entry.Voltage) + } + case "ICC": + if entry.Timestamp.Unix() != 1670580961 { + t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix()) + } + if entry.Voltage != 11 { + t.Errorf("Incorrect Voltage %d", entry.Voltage) + } + case "GW": + if entry.Timestamp.Unix() != 1691525024 { + t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix()) + } + case "MCU": + if entry.Timestamp.Unix() != 1691708475 { + t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix()) + } + + } + } + + check(ecc) + check(icc) + check(gw) + check(mcu) + +} diff --git a/services/attendant/controllers/get_filekeys.go b/services/attendant/controllers/get_filekeys.go new file mode 100644 index 0000000..cdb32e4 --- /dev/null +++ b/services/attendant/controllers/get_filekeys.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "encoding/json" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "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/validator" + + "github.com/pkg/errors" +) + +// Mega ducky repeat of /handlers/get_file_keys. Need a code re-org +func GetFileKeys(db *services.DB, device common.Device, id string, data []byte) error { + logger.Debug().Msgf("GetFileKeys %v %s", device, id) + var err error + var req *common.FileKeysRequest + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + req, err = parseGetFileKeysRequest(data) + if err != nil { + notifyFileKeysGeneralError(client, device, id, err) + return err + } + + keys, err := cache.RetrieveFileEncryptionParams(client, db.GetFileKeys(), req.FileIDs) + if err != nil { + notifyFileKeysGeneralError(client, device, id, err) + return err + } + + err = client.SafePublishMessage(device.Key(id), common.Message{ + Handler: "filekeys", + Data: keys, + }) + + if err != nil { + return err + } + + logger.Debug().Msgf("GetFileKeys sent %v %s", device, id) + + return nil +} + +func parseGetFileKeysRequest(data []byte) (*common.FileKeysRequest, error) { + var status common.FileKeysRequest + + err := json.Unmarshal(data, &status) + if err != nil { + return nil, errors.WithStack(err) + } + + err = validator.ValidateStruct(status) + if err != nil { + return &status, errors.WithStack(err) + } + + return &status, nil +} + +func notifyFileKeysGeneralError(client redis.Client, device common.Device, id string, err error) { + e := client.SafePublishMessage(device.Key(id), common.Message{ + Handler: "filekeys", + Data: []common.FileKeyResponse{ + { + FileID: "0", + Error: err.Error(), + }, + }, + }) + if e != nil { + logger.Error().Err(errors.WithStack(e)).Send() + } +} diff --git a/services/attendant/controllers/health_check.go b/services/attendant/controllers/health_check.go new file mode 100644 index 0000000..4dd6ce1 --- /dev/null +++ b/services/attendant/controllers/health_check.go @@ -0,0 +1,59 @@ +package controllers + +import ( + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/health" + "github.com/fiskerinc/cloud-services/pkg/logger" + + "github.com/pkg/errors" +) + +var mismatchTypeError = errors.New("mismatch type error") + +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, + }, + { + Name: "kafka", + Check: health.NewKafkaMultiCheck(getKafkaConsumer), + Timeout: time.Second * 1, + Vital: true, + }, + }) + if err != nil { + logger.Error().Err(err).Send() + } +} + +func getKafkaConsumer() (connections []health.KafkaConnCheckInterface, err error) { + client, oldClient, err := services.GetKafkaConsumer() + if err != nil { + return connections, err + } + + conn, ok := client.(health.KafkaConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + connections = append(connections, conn) + oldConn, ok := oldClient.(health.KafkaConnCheckInterface) + if !ok { + return connections, errors.WithStack(mismatchTypeError) + } + connections = append(connections, oldConn) + return connections, nil +} diff --git a/services/attendant/controllers/send_manifest.go b/services/attendant/controllers/send_manifest.go new file mode 100644 index 0000000..b2f3884 --- /dev/null +++ b/services/attendant/controllers/send_manifest.go @@ -0,0 +1,403 @@ +package controllers + +import ( + "encoding/json" + "fmt" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/carcommand" + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/hwversion" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/manifestsender" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/tmobile" + uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/fiskerinc/cloud-services/pkg/utils/randomvalues" + "github.com/fiskerinc/cloud-services/pkg/utils/whereami" + "github.com/fiskerinc/cloud-services/pkg/validator" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" + "google.golang.org/protobuf/proto" + + "sync" + "time" + + "github.com/pkg/errors" +) + +// 0100084000000101012200010101010001010101000000000000000000ff7eff7f000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100010101020101010101010000000000000000ffffff00010102010102020001010000000000000000000000000000000000000000000000000000000000000000000100202310010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6 +var defaultVOD string = envtool.GetEnv("DEFAULT_VOD", "00FF111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114A") + +var fileKeyBypass map[int64]string +var FileKeyBypassManifest_US map[int64]string = map[int64]string{ + 1189: "fe4f19fbc940ed58", + 1380: "6a44fd71c940716d", +} + +var FileKeyBypassManifest_EU map[int64]string = map[int64]string{ + 893: "fe4f19fbc940ed58", + 1002: "6a44fd71c940716d", + 1012: "d70e1d32c940a221", +} + +func init() { + if whereami.Environment == whereami.PRODUCTION_EU { + fileKeyBypass = FileKeyBypassManifest_EU + } else { + fileKeyBypass = FileKeyBypassManifest_US + } + +} + +func NewManifestSender( + r redis.ClientPoolInterface, + db *services.DB, + conf vconfig.ConfigServiceInterface, + device common.Device, + ka *services.KeepAwake, + seed int64, //This seed is used only for testing +) *ManifestSender { + randomGenerator := randomvalues.NewNonCryptoGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ", seed) + return &ManifestSender{ + Redis: r, + db: db, + conf: conf, + Device: device, + sms: services.GetSMSClient(), + ka: ka, + gen: randomGenerator, + } +} + +type ManifestSender struct { + Redis redis.ClientPoolInterface + db *services.DB + conf vconfig.ConfigServiceInterface + sms sms.SMSServiceClient + Device common.Device + ka *services.KeepAwake + gen randomvalues.NonCryptoGenerator +} + +func (m *ManifestSender) Process(id string, data []byte) error { + // id string parameter is unused except in the following log + // the data might not be used either, just for its single carUpdateID + u, err := m.parseRequest(data) + if err != nil { + logger.Err(err).Msgf("Manifest sender, unable to process update %s", id) + return err + } + + carUpdate := common.CarUpdate{ID: u.CarUpdateID} + err = m.loadCarUpdate(&carUpdate) + if err != nil { + return err + } + + logger.Info().Msgf("ManifestSender car_update_id %d", carUpdate.ID) + + carUpdate.UpdateManifest.CarUpdateID = carUpdate.ID + helper := uhelpers.NewECUKeys(m.db.GetECCKeys()) + err = helper.AddECUECCKeys(carUpdate.UpdateManifest) + if err != nil { + logger.Err(err).Msgf("Unable to decrypt keys for %d", carUpdate.ID) + return err //comment out for local debugging + } + + // If we fail to deliver the sms, then we don't believe the car has awoken + // then set the manifest as failed + err = m.send(carUpdate.VIN, carUpdate.UpdateManifest) + if err != nil { + update := NewCarUpdateProgress(m.Redis, m.ka, m.db, common.TRex) + err = update.ProcessStatus(carUpdate.VIN, common.CarUpdateProgress{ + CarUpdateID: u.CarUpdateID, + Status: carupdatestatus.ManifestCanceled, + Info: " Underlying error: " + err.Error(), + }) + } + + return err +} + +func (m *ManifestSender) destination(manifest *common.UpdateManifest) string { + if manifest.HasSelfDownload() { + return "ICC" + } else { + return "ICC/TBOX" + } +} + +func (m *ManifestSender) parseRequest(data []byte) (*common.UpdateManifest, error) { + var req common.UpdateManifest + + err := json.Unmarshal(data, &req) + if err != nil { + return nil, errors.WithStack(err) + } + + err = validator.ValidateIDField(req.CarUpdateID) + if err != nil { + return &req, errors.WithStack(err) + } + + return &req, nil +} + +func (m *ManifestSender) loadCarUpdate(cu *common.CarUpdate) error { + err := m.db.GetCarUpdates().Load(cu) + if err != nil { + return errors.WithStack(err) + } + + if cu.UpdateManifest == nil { + return nil + } + + return nil +} + +func (m *ManifestSender) queueManifest(vin string, manifest *common.UpdateManifest) error { + loggedManifest := manifest.Copy() + loggedManifest.RemoveECCKeysFromECUs() + logger.At(logger.Info(), common.HMI.Key(vin), "update").Interface("manifest.json", loggedManifest).Msgf("HMI ManifestSender sent %v %s %d", common.HMI, vin, manifest.CarUpdateID) + client := m.Redis.GetFromPool() + defer client.Close() + err := client.SafeQueueMessage(common.HMI.Key(vin), common.Message{ + Handler: "update_manifest", + Data: manifest, + }) + if err != nil { + logger.Err(err).Msgf("failed to queue manifest in redis vin %s ", vin) + } + return err +} + +func (m *ManifestSender) send(vin string, manifest *common.UpdateManifest) error { + updateManifestID := manifest.ID + manifest.TransformECUNames() + err := hwversion.SetHWVersion(manifest, vin, m.db.GetCars()) + if err != nil { + logger.Err(err).Msgf("manifest sender failed at SetHwVersion for vin %s", vin) + return err //comment out for local debugging + } + + manifest.SortECUs() + manifest.RemoveOriginalS19HexFiles() + manifest.RemoveOriginalS19HexFilesRollbacks() + + manifest.FilterCompatibleECUs(vin) + + err = uhelpers.PopulateECUsCurrentVersion(m.db.GetCars(), vin, manifest.ECUs) + if err != nil { + logger.Err(err).Msgf("manifest sender failed at PupulateECUsCurrentVersion for vin %s", vin) + return err + } + + fpparams := manifestfingerprintparams.GetFPParams() + manifest.GenerateFingerprint(fpparams.CurTime(), fpparams.ManifestSerial()) + + hmiManifest := manifest.Copy() + + cds, err := m.addVOD(hmiManifest, vin) + if err != nil { + logger.Err(err).Msgf("manifest sender failed at adding the vod for vin %s", vin) + return err // comment out for local debugging + } + + err = validator.ValidateField(hmiManifest.SUMS, "sums_version") + if err == nil { + err = hmiManifest.AddSUMSToVOD() + if err != nil { + logger.Err(err).Msgf("manifest sender failed at AddSUMSToVOD for vin %s", vin) + return err + } + } else { + logger.Err(err).Msgf("manifest sender failed at Validation for vin %s", vin) + return err + } + hmiManifest.Scrub(common.HMI) + + client := m.Redis.GetFromPool() + defer client.Close() + // If HasSelfDownload, we are not yet ready to send to t box, so we wake the car if the update is forced + if manifest.HasSelfDownload() && m.sms != nil { + // The function that calls this one handles canceling the manifest + var msgID string + res, err := carcommand.QueueSMSWakeUp(vin, true, client, m.db.GetCars(), m.sms) + if err != nil || res == nil || !res.SentSuccessful { + logger.Err(err).Msgf("Attendant:manifest sender failed at sendSMSWakeUp for vin %s", vin) + msgID = m.gen.GetString(10) + defer m.selfKafkaCallback(msgID) + } else { + msgID = res.SmsMsgID + } + + err = m.ka.SendFirstKeepAwakeMessage(vin) + if err != nil { + logger.Err(err).Msgf("failed to SendFirstKeepAwakeMessage on msgID %s", msgID) + } + m.queueManifest(vin, hmiManifest) + + fileID, ok := fileKeyBypass[updateManifestID] + if ok { + logger.Info().Str("VIN", vin).Msg("Queueing SendFileKeys") + time.AfterFunc(time.Second*3, func() { sendFileKeys(vin, fileID) }) + } + + // Do not send manifest to TBOX, wait until ICC has finished downloading + return err + } + + trex := manifestsender.NewTBOXManifestSender(client, m.conf, m.db, services.GetSMSClient(), cds) + defer trex.Close() + //smsID, err := trex.ProcessSoftwareUpdate(vin, manifest) + _, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData()) + logger.Debug().Msgf("Send HMI manifest to %s ", vin) + m.queueManifest(vin, hmiManifest) + + // We managed to send an sms, so we want to continue with an sms delivered check + // if smsID != "" { + // err = m.setManifestCache(smsID, ManifestCachedPoint{ + // VIN: vin, + // ManifestID: manifest.ID, + // Destination: m.destination(manifest), + // }) + // if err != nil { + // return err + // } + // } + return err +} + +// So this flow of continue will be activated at two different times, possible multiple times. +// 1) When a new car_update comes through and the .hasdownload is true, and then again when the manifest is being sent to the tbox +func (m *ManifestSender) ContinueTBOXSend(id string, data []byte) (err error) { + // After a text has been succesfully delivered, we can now continue the update + // Check the status and make sure that is is success, otherwise fail and set the update status as failed + var msgStatus tmobile.MessageStatus + err = json.Unmarshal(data, &msgStatus) + if err != nil { + return + } + + return nil +} + +func (m *ManifestSender) setManifestCache(msgID string, cache ManifestCachedPoint) (err error) { + client := m.Redis.GetFromPool() + defer client.Close() + return client.Set(msgIDToRedisKey(msgID), cache) +} + +type ManifestCachedPoint struct { + VIN string + ManifestID int64 + Destination string +} + +func msgIDToRedisKey(msgID string) (redisKey string) { + return fmt.Sprintf("manifest_tbox_send_cache:%s", msgID) +} + +func (m *ManifestSender) addVOD(manifest *common.UpdateManifest, vin string) (map[string]string, error) { + cds, err := uhelpers.GetCDS(m.conf, services.GetDB().GetCarConfigData(), vin) + if err != nil { + return nil, err + } + + if vod, ok := cds["VOD"]; ok && vod != "" { + manifest.VOD = vod + } else { + manifest.VOD = defaultVOD + } + + return cds, nil +} + +// When car is a virtual trex, we are going to produce to the kafka message queue as if the sms service did it +func (m *ManifestSender) selfKafkaCallback(messageID string) { + producer, err := services.GetKafkaProducer() + if err != nil { + logger.Err(err).Send() + return + } + + messageStatus := &kafka_grpc.GRPC_AttendantPayload_MessageStatus{ + MessageStatus: &kafka_grpc.MessageStatus{ + MessageId: messageID, + Status: kafka_grpc.EmumStatus_DELIVERED, + }, + } + kafkaMSG := kafka_grpc.GRPC_AttendantPayload{ + Handler: "sms_delivery_status_manifest", + Data: messageStatus, + } + + binaryPayload, _ := proto.Marshal(&kafkaMSG) + err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, "4:Service", binaryPayload, nil) + if err != nil { + err = errors.WithStack(err) + logger.Err(err).Msgf("failed to produce kafka message for sms id %s", messageID) + } +} + +func (m *ManifestSender) Release() { + m.Redis = nil + m.db = nil + m.conf = nil +} + +var fpParams FingerprintParamer +var fpParamsOnce sync.Once + +func SetFPParams(fpp FingerprintParamer) { + fpParams = fpp +} + +func GetFPParams() FingerprintParamer { + fpParamsOnce.Do(func() { + if fpParams == nil { + fpParams = &fingerprintParams{ + serialNum: envtool.GetEnv("OTA_MANIFEST_SERIAL", "00000000000000000"), + } + } + }) + + return fpParams +} + +type fingerprintParams struct { + serialNum string +} + +func (p *fingerprintParams) ManifestSerial() string { + return p.serialNum +} + +func (p *fingerprintParams) CurTime() time.Time { + return time.Now().UTC() +} + +type FingerprintParamer interface { + ManifestSerial() string + CurTime() time.Time +} + +// Mega hack mode + +func sendFileKeys(vin string, fileID string) { + logger.Info().Str("VIN", vin).Msg("Sending SendFileKeys") + db := services.GetDB() + err := GetFileKeys(db, common.HMI, vin, []byte(`{"file_ids": ["`+fileID+`"]}`)) + if err != nil { + logger.Err(err).Str("VIN", vin).Str("file ID", fileID).Msg("Failed to SendFileKeys") + } +} diff --git a/services/attendant/go.mod b/services/attendant/go.mod new file mode 100644 index 0000000..2a3efcb --- /dev/null +++ b/services/attendant/go.mod @@ -0,0 +1,115 @@ +module github.com/fiskerinc/cloud-services/services/attendant + +go 1.25 + +toolchain go1.25.0 + +require ( + github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000 + github.com/fiskerinc/cloud-services/pkg/can-go v0.0.0-00010101000000-000000000000 + github.com/pkg/errors v0.9.1 +) + +require ( + github.com/go-pg/pg/v10 v10.11.1 + github.com/gomodule/redigo v1.8.9 + github.com/jinzhu/copier v0.3.5 + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.67.3 + google.golang.org/protobuf v1.36.1 +) + +require ( + 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/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/cenkalti/backoff/v4 v4.2.1 // 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/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/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.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/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/google/uuid v1.6.0 // indirect + github.com/gorilla/schema v1.2.0 // 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/inflection v1.0.0 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // 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/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/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // 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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.mongodb.org/mongo-driver v1.14.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/DataDog/dd-trace-go.v1 v1.60.1 // 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 +) diff --git a/services/attendant/go.sum b/services/attendant/go.sum new file mode 100644 index 0000000..fa4804a --- /dev/null +++ b/services/attendant/go.sum @@ -0,0 +1,483 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/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-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-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= +github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.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/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.5/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +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/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/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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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.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/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/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +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-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/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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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/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/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.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.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-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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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/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-20190313153728-d0100b6bd8b3/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-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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-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-20190620200207-3b0461eec859/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-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-20211015210444-4f30a5c0130f/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/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-20190423024810-112230192c58/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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-20191120155948-bd437916bb0e/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-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-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.3.0/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.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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/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.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= +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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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.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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= diff --git a/services/attendant/handlers/car_update_progress.go b/services/attendant/handlers/car_update_progress.go new file mode 100644 index 0000000..54a5ad1 --- /dev/null +++ b/services/attendant/handlers/car_update_progress.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + + "github.com/pkg/errors" +) + +func CarUpdateProgressStatus(db *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) error { + logger.Debug().Msgf("CarUpdateProgressStatus %v %s", device, id) + + clientPool := services.RedisClientPool() + + handler := controllers.NewCarUpdateProgress(clientPool, ka, db, device) + if handler == nil { + return errors.Errorf("NewCarUpdateProgress cannot handle device %v", device) + } + defer handler.Dispose() + + err := handler.Process(id, data) + return err +} diff --git a/services/attendant/handlers/car_update_progress_func_test.go b/services/attendant/handlers/car_update_progress_func_test.go new file mode 100644 index 0000000..4d6dc39 --- /dev/null +++ b/services/attendant/handlers/car_update_progress_func_test.go @@ -0,0 +1,389 @@ +package handlers_test + +import ( + "fmt" + "testing" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestCarUpdateProgressFunctional(t *testing.T) { + t.Skip() + testVIN := "WBSEH93466B798124" + conn := tester.NewRedisMock() + db := services.GetDB() + manifest, carUpdateID, err := setupCarUpdateProgressFunc(db, testVIN) + if err != nil { + panic(err) + } + defer func() { + if carUpdateID > 0 { + db.GetCarUpdates().Delete(&common.CarUpdate{ID: carUpdateID}) + conn.Delete(redis.CarUpdateStatusTBOXHashKey(carUpdateID), redis.CarUpdateStatusHMIHashKey(carUpdateID)) + } + if manifest != nil && manifest.ID > 0 { + db.GetUpdateManifests().Delete(manifest) + } + }() + + ka := services.NewKeepAwakeService() + ka.SetService(&services.MockKeepAwakeImplementation{}) + + type testCase struct { + Name string + Device common.Device + Payload string + ExpectedMsg string + ExpectInstalled int + ExpectInstallTotal int + ExpectDBStatus string + ExpectDownloadCurrent uint64 + ExpectDownloadTotal uint64 + ExpectErrorCode int + } + + tests := []testCase{ + { + Name: "manifest_received", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "msg": "manifest_received" + }`, carUpdateID), + ExpectedMsg: "manifest_received", + ExpectDBStatus: "manifest_received", + }, + { + Name: "install_approval_await", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "msg": "install_approval_await" + }`, carUpdateID), + ExpectedMsg: "install_approval_await", + ExpectDBStatus: "install_approval_await", + }, + { + Name: "other error", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 3, + "total_files": 10, + "msg": "other error", + "err": -100 + }`, carUpdateID), + ExpectedMsg: "other error", + ExpectDBStatus: "other error", + ExpectInstalled: 3, + ExpectInstallTotal: 10, + ExpectErrorCode: -100, + }, + { + Name: "download_start", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "file_current": 0, + "file_total": 100, + "package_current": 0, + "package_total": 100, + "msg": "download_start", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "downloading", + ExpectDBStatus: "package_download_start", + ExpectDownloadCurrent: 0, + ExpectDownloadTotal: 100, + }, + { + Name: "downloading", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "file_current": 0, + "file_total": 100, + "package_current": 30, + "package_total": 100, + "msg": "downloading", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "downloading", + ExpectDBStatus: "package_download_start", + ExpectDownloadCurrent: 30, + ExpectDownloadTotal: 100, + }, + { + Name: "download_complete", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "file_current": 100, + "file_total": 100, + "package_current": 900, + "package_total": 1000, + "msg": "download_complete", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "downloading", + ExpectDBStatus: "package_download_start", + ExpectDownloadCurrent: 900, + ExpectDownloadTotal: 1000, + }, + { + Name: "package_download_complete", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "file_current": 100, + "file_total": 100, + "package_current": 1000, + "package_total": 1000, + "msg": "download_complete", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "package_download_complete", + ExpectDBStatus: "package_download_complete", + ExpectDownloadCurrent: 1000, + ExpectDownloadTotal: 1000, + }, + { + Name: "download_error", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "file_current": 0, + "file_total": 100, + "package_current": 0, + "package_total": 1000, + "msg": "download_error", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "download_error", + ExpectDBStatus: "download_error", + ExpectDownloadCurrent: 0, + ExpectDownloadTotal: 1000, + }, + { + Name: "install_start", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 0, + "total_files": 10, + "msg": "install_start", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "installing", + ExpectDBStatus: "package_install_start", + ExpectInstalled: 0, + ExpectInstallTotal: 10, + }, + { + Name: "installing", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 2, + "total_files": 10, + "msg": "installing", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "installing", + ExpectDBStatus: "package_install_start", + ExpectInstalled: 2, + ExpectInstallTotal: 10, + }, + { + Name: "install_complete", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 9, + "total_files": 10, + "msg": "install_complete", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "installing", + ExpectDBStatus: "package_install_start", + ExpectInstalled: 9, + ExpectInstallTotal: 10, + }, + { + Name: "package_install_complete", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 10, + "total_files": 10, + "msg": "install_complete", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "package_install_complete", + ExpectDBStatus: "package_install_complete", + ExpectInstalled: 10, + ExpectInstallTotal: 10, + }, + { + Name: "install_error", + Device: common.TRex, + Payload: fmt.Sprintf(`{ + "car_update_id": %d, + "ecu": "TEST", + "installed": 3, + "total_files": 10, + "msg": "install_error", + "err": 0 + }`, carUpdateID), + ExpectedMsg: "install_error", + ExpectDBStatus: "install_error", + ExpectInstalled: 3, + ExpectInstallTotal: 10, + }, + { + Name: "package_download_complete", + Device: common.HMI, + Payload: fmt.Sprintf(`{ + "car_update_id":%d, + "ecu":"ICC", + "file_current":null, + "file_total":null, + "package_current":920639485, + "package_total":920639485, + "installed":null, + "total_files":null, + "msg":"package_download_complete", + "err":null + }`, carUpdateID), + ExpectedMsg: "package_download_complete", + ExpectDBStatus: "package_download_complete", + ExpectInstalled: 3, + ExpectInstallTotal: 10, + ExpectDownloadCurrent: 920639485, + ExpectDownloadTotal: 920639485, + }, + } + + keys := make([]string, 1) + statuses := make([]interface{}, 1) + + for _, test := range tests { + err := handlers.CarUpdateProgressStatus(db, ka, test.Device, testVIN, []byte(test.Payload)) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s output error", test.Device, test.Name), nil, err) + } + + status := common.CarUpdateProgress{} + keys[0] = redis.CarUpdateStatusHashKey(carUpdateID) + statuses[0] = &status + + err = conn.GetObjectsMulti(keys, statuses) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "GetObjectsMulti", nil, err) + return + } + if status.Status != test.ExpectedMsg { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s Status", test.Device, test.Name), test.ExpectedMsg, status.Status) + } + if status.InstalledFiles != test.ExpectInstalled { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s InstalledFiles", test.Device, test.Name), test.ExpectInstalled, status.InstalledFiles) + } + if status.TotalFiles != test.ExpectInstallTotal { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s TotalFiles", test.Device, test.Name), test.ExpectInstallTotal, status.TotalFiles) + } + if status.PackageCurrent != test.ExpectDownloadCurrent { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s PackageCurrent", test.Device, test.Name), test.ExpectDownloadCurrent, status.PackageCurrent) + } + if status.PackageTotal != test.ExpectDownloadTotal { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s PackageTotal", test.Device, test.Name), test.ExpectDownloadTotal, status.PackageTotal) + } + if status.ErrorCode != test.ExpectErrorCode { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s ErrorCode", test.Device, test.Name), test.ExpectErrorCode, status.ErrorCode) + } + + cu, err := db.GetCarUpdates().SelectByID(carUpdateID) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "Get from DB", nil, err) + return + } + if cu.Status != test.ExpectDBStatus { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s DB Status", test.Device, test.Name), test.ExpectDBStatus, cu.Status) + } + if cu.ErrorCode != test.ExpectErrorCode { + t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s DB ErrorCode", test.Device, test.Name), test.ExpectErrorCode, cu.ErrorCode) + } + } +} + +func setupCarUpdateProgressFunc(db *services.DB, vin string) (*common.UpdateManifest, int64, error) { + _, err := db.GetCars().SelectOrInsert(&common.Car{ + VIN: vin, + Model: "Ocean", + Year: 2022, + Trim: "Sport", + }) + if err != nil { + return nil, 0, err + } + + manifest := common.UpdateManifest{ + Name: "TestCarUpdateProgressFunctional", + Version: "1000", + Description: "For TestCarUpdateProgressFunctional", + ReleaseNotes: "http://releasenotes.com", + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + } + _, err = db.GetUpdateManifests().Insert(&manifest) + if err != nil { + return nil, 0, err + } + _, err = db.GetUpdateManifests().ECUInsert(&common.UpdateManifestECU{ + UpdateManifestID: manifest.ID, + ECU: "ICC", + Version: "ICCVERSION", + }) + if err != nil { + return nil, 0, err + } + _, err = db.GetUpdateManifests().ECUInsert(&common.UpdateManifestECU{ + UpdateManifestID: manifest.ID, + ECU: "ADAS", + Version: "ADASVERSION", + }) + if err != nil { + return nil, 0, err + } + + carupdate := common.CarUpdate{ + VIN: vin, + UpdateManifestID: manifest.ID, + } + _, err = db.GetCarUpdates().Insert(&carupdate) + if err != nil { + return nil, 0, err + } + + return &manifest, carupdate.ID, nil +} diff --git a/services/attendant/handlers/car_update_progress_test.go b/services/attendant/handlers/car_update_progress_test.go new file mode 100644 index 0000000..1733ce6 --- /dev/null +++ b/services/attendant/handlers/car_update_progress_test.go @@ -0,0 +1,776 @@ +package handlers_test + +import ( + "errors" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +var ( + schemaToTRex = "file://" + th.GetSchemaDirPath() + "/trex/RXMessage.json" + schemaToHMI = "file://" + th.GetSchemaDirPath() + "/hmi/RXMessage.json" +) + +func TestCarUpdateProgress(t *testing.T) { + testGetSetResult := `["valid-cognito-id-1","valid-cognito-id-2"]` + testVIN := "JH4KA7680RC01" + mobile1Key := "3:valid-cognito-id-1" + mobile2Key := "3:valid-cognito-id-2" + hmiKey := "2:JH4KA7680RC01" + trexKey := common.TRex.Key(testVIN) + carupdateKey := "carupdate:297" + var bhex common.BinaryHex + expectedExpire := 3600 + bhex = []byte("test") + + fingerprintTime, _ := time.Parse("02/01/06", "19/01/24") + fpp := manifestfingerprintparams.MockFingerprintParamer{ + ManifestSerialValue: "00000000000000000", + Time: fingerprintTime, + } + manifestfingerprintparams.SetFPParams(&fpp) + + manifest := common.UpdateManifest{ + ID: 1, + Name: "test", + Version: "MANIFEST_VERSION", + SUMS: "2023.10.01.00.E", + Description: "description", + ReleaseNotes: "http://releasenotes.com", + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + RollbackEnabled: true, + Type: "standard", + ECUs: []*common.UpdateManifestECU{ + { + ECU: "ICC", + Version: "version", + HWVersions: []string{"hardware_version"}, + Mode: "D", + SelfDownload: true, + Files: []*common.UpdateManifestFile{ + { + FileID: "fileid", + URL: "http://download.com", + Filename: "filename.bin", + FileSize: 10000, + FileType: common.Software, + WriteRegionID: 2222, + WriteRegion: common.MemoryRegion{ + ID: 2000, + Offset: 10000, + Length: 20, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + { + ECU: "ADAS", + Version: "version", + HWVersions: []string{"hardware_version"}, + Mode: "A", + InstallPriority: 10, + Files: []*common.UpdateManifestFile{ + { + FileID: "fileid", + URL: "http://download.com", + Filename: "adas.bin", + FileSize: 9999, + FileType: common.Software, + WriteRegionID: 9999, + WriteRegion: common.MemoryRegion{ + ID: 8888, + Offset: 8888, + Length: 8888, + }, + DBModelBase: th.Timestamp, + }, + }, + ECCKeys: &common.ECCKeys{ + ECU: "ADAS", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + }, + DBModelBase: th.Timestamp, + }, + { + ECU: "ECUA", + Version: "version", + HWVersions: []string{"hardware_version"}, + Mode: "A", + InstallPriority: 5, + Files: []*common.UpdateManifestFile{ + { + FileID: "fileid", + URL: "http://download.com", + Filename: "adas.bin", + FileSize: 9999, + FileType: common.Software, + WriteRegionID: 9999, + WriteRegion: common.MemoryRegion{ + ID: 8888, + Offset: 8888, + Length: 8888, + }, + DBModelBase: th.Timestamp, + }, + { + FileID: "SHOULD_NOT_BE_IN_UPDATE", + URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: elptr.ElPtr(false), + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + DBModelBase: th.Timestamp, + }, + { + FileID: "MUST_BE_IN_UPDATE", + URL: "http://download.com/MUST_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: elptr.ElPtr(true), + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + DBModelBase: th.Timestamp, + }, + }, + ECCKeys: &common.ECCKeys{ + ECU: "ECUA", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + } + ecuaRollback := []*common.UpdateManifestECU{ + { + ID: 100, + UpdateManifestID: 200, + ECU: "ECUA", + Version: "VERSIONOLD", + HWVersions: []string{"hardware_version"}, + Mode: "A", + DBModelBase: th.Timestamp, + Files: []*common.UpdateManifestFile{ + { + FileID: "FILEIDOLD", + UpdateManifestECUID: 1001, + Filename: "FILENAMEOLD", + URL: "URLOLD", + FileType: common.Software, + WriteRegionID: 700, + WriteRegion: common.MemoryRegion{ + Offset: 701, + Length: 702, + }, + FileSize: 240, + DBModelBase: th.Timestamp, + }, + }, + ECCKeys: &common.ECCKeys{ + ECU: "ECUA", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + }, + }, + } + mockDB := &services.DB{} + mockCars := &mocks.MockCars{} + mockCarUpdates := &mocks.MockCarUpdates{ + SelectCarUpdateResponse: &common.CarUpdate{ + UpdateManifestID: 816, + UpdateManifest: &common.UpdateManifest{ + ID: 816, + }, + }, + } + mockManifests := &mocks.MockUpdateManifests{ + ECUUpdatesMock: func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) { + if man.ECU == "ECUA" { + return ecuaRollback, nil + } + + return nil, nil + }, + } + mockKeys := &mocks.MockEccKeys{ + MockListResponse: []common.ECCKeys{ + { + ECU: "PDU", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + }, + { + ECU: "TBOX", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + }, + }, + } + mockDB.SetCars(mockCars) + mockDB.SetCarUpdates(mockCarUpdates) + mockDB.SetECCKeys(mockKeys) + mockDB.SetManifests(mockManifests) + + mockFoa := FoaServiceMock{} + services.SetFoaService(&mockFoa) + + mockRedis := tester.NewRedisMock() + mockKeepAwake := services.NewKeepAwakeService() + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) + mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) { + return common.SAPResponse{ + ModelYear: 2023, + ModelType: "Ocean", + VersionDuringModelYear: "1", + Features: []common.SAPFeature{ + { + FamilyCode: "FamilyCode1", + FeatureCode: "FeatureCode1", + }, + { + FamilyCode: "FamilyCode2", + FeatureCode: "FeatureCode2", + }, + }, + }, nil + }} + mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) { + return map[string]string{ + "ECUA": "config", + "VOD": "00a62299027600000101012200010100010001010101000000000000000000fffeffff000101010101010101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101010100010001010101010101010201010000000000000100000101ff00000001010200000000000003ffffffff0000000201010200000100000000000000000000000000000000000000000000000000000000000000000001202310010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, nil + }} + + services.SetSapService(mockSap) + services.SetVehicleConfig(mockConf) + schemaTesterHMI := th.NewSchemaTestHelper(t, schemaToHMI) + schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex) + + tests := []AttendentRouteTestCase{ + { + Name: "[HMI] install_error", + RedisTestCase: tester.RedisTestCase{ + Device: common.HMI, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"install_error","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"install_failed","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[HMI] download_completed", + RedisTestCase: tester.RedisTestCase{ + Device: common.HMI, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"ICC","file_current":null,"file_total":null,"package_current":920639485,"package_total":920639485,"installed":null,"total_files":null,"msg":"download_completed","err":null}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":920639485,"ecu":"ICC","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":920639485}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + trexKey: `{"handler":"update_manifest","data":{"ecu_updates":[{"name":"ICC","version":"version","hw_version":"hardware_version","self_download":true},{"name":"ECUA","version":"version","hw_version":"hardware_version","configuration":"config","files":[{"file_id":"fileid","url":"http://download.com","file_size":9999,"type":"software","write_region":{"offset":8888,"length":8888}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}],"rollback":[{"version":"VERSIONOLD","files":[{"file_id":"FILEIDOLD","url":"URLOLD","file_size":240,"type":"software","write_region":{"offset":701,"length":702}}]}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"ADAS","version":"version","hw_version":"hardware_version","files":[{"file_id":"fileid","url":"http://download.com","file_size":9999,"type":"software","write_region":{"offset":8888,"length":8888}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}}],"fingerprint":"240119FISKER00000000000000000","car_update_id":297,"rollback":true,"type":"standard","vod":"01012299027600000101012200010100010001010101000000000000000000fffeffff000101010101010101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101010100010001010101010101010201010000000000000100000101ff00000001010200000000000003ffffffff00000002010102000001000000000000000000000000000000000000000000000000000000000000000000012023100100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021","update_duration":30}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`, + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + MockLoadManifest: &manifest, + }, + { + Name: "[HMI] manifest_succeeded", + RedisTestCase: tester.RedisTestCase{ + Device: common.HMI, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"ICC","file_current":null,"file_total":null,"package_current":920639485,"package_total":920639485,"installed":null,"total_files":null,"msg":"manifest_succeeded","err":null}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":920639485,"ecu":"ICC","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_succeeded","total_files":0,"total_size":920639485}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`, + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`, + "1:JH4KA7680RC01": `{"handler":"read_ecu_versions","data":{"ecu_name":"*"}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + SelectCarUpdate: &common.CarUpdate{ + UpdateManifest: &validUpdateManifest, + }, + }, + { + Name: "[TREX] manifest_received", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"msg":"manifest_received","err":-6,"extra_info":""}`, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`, + }, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"","errorcode":-6,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_received","total_files":0,"total_size":0}`, + Expires: expectedExpire, + }, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] manifest_accepted", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"msg":"manifest_accepted","err":-7,"extra_info":""}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"","errorcode":-7,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_accepted","total_files":0,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] download_started", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"msg":"download_started","err":-14,"extra_info":""}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"","errorcode":-14,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] downloading", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"ADAS","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"msg":"downloading","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":1048576,"ecu":"ADAS","errorcode":0,"file_size":1048576,"file_total":1264672,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] download_completed ECU 1", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"ADAS","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"msg":"download_completed","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":1264672,"ecu":"ADAS","errorcode":0,"file_size":1264672,"file_total":1264672,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] download_started ECU 2", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"msg":"download_started","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":1264672,"ecu":"EKS","errorcode":0,"file_size":0,"file_total":1265184,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] downloading ECU 2", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"msg":"downloading","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":2313248,"ecu":"EKS","errorcode":0,"file_size":1048576,"file_total":1265184,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] download_completed ECU 2", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"msg":"download_completed","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":2529856,"ecu":"EKS","errorcode":0,"file_size":1265184,"file_total":1265184,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":2529856}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] package_download_complete", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"msg":"download_completed","err":-15,"extra_info":""}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"","errorcode":-15,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] download_failed", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"msg":"download_failed","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":100,"id":297,"installed":0,"status":"download_failed","total_files":0,"total_size":1000}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] install_started", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":0,"total_files":10,"msg":"install_started","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"installing","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] installing", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"installing","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"installing","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] install_succeeded ECU", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"install_succeeded"}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"package_install_complete","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] package_install_complete", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"msg":"install_succeeded","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"installing","total_files":0,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] install_failed", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"install_failed","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"install_failed","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] requirements_failed", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"requirements_failed","err":0}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"requirements_failed","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] install_scheduled ECU", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"install_scheduled"}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"install_scheduled","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + }, + { + Name: "[TREX] manifest_succeeded", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"manifest_succeeded"}`, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + carupdateKey: { + Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"manifest_succeeded","total_files":10,"total_size":0}`, + Expires: expectedExpire, + }, + }, + ExpectedMessages: map[string]string{ + mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`, + mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`, + hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`, + "1:JH4KA7680RC01": `{"handler":"read_ecu_versions","data":{"ecu_name":"*"}}`, + }, + MockRedisGetSet: testGetSetResult, + }, + SelectCarUpdate: &common.CarUpdate{ + UpdateManifest: &validUpdateManifest, + }, + }, + } + + for i := range tests { + mockRedis.Reset() + test := &tests[i] + test.SetupRedis(mockRedis) + test.SetupDB(mockCars, mockCarUpdates, test) + + redisPool := tester.NewMockClientPool(mockRedis) + handler := controllers.NewCarUpdateProgress(redisPool, mockKeepAwake, mockDB, test.Device) + if handler == nil { + t.Error(errors.New("NewCarUpdateProgress cannot handle device %v")) + continue + } + err := handler.Process(test.DeviceKey, []byte(test.PayloadData)) + + test.CheckHandlerError(t, test.Name, err) + test.Validate(t, test.Name, mockRedis) + + for key, m := range test.RedisTestCase.ExpectedMessages { + name := fmt.Sprintf("%s %s", test.Name, key) + if strings.Contains(key, "1:") { + schemaTesterTRex.ValidateSchemaObject(name, []byte(m)) + } else if strings.Contains(key, "2:") { + schemaTesterHMI.ValidateSchemaObject(name, []byte(m)) + } + } + } +} + +type FoaServiceMock struct{} + +func (f *FoaServiceMock) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) { + return &http.Response{StatusCode: 200}, nil +} diff --git a/services/attendant/handlers/car_update_state.go b/services/attendant/handlers/car_update_state.go new file mode 100644 index 0000000..109f832 --- /dev/null +++ b/services/attendant/handlers/car_update_state.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "encoding/json" + "errors" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + fv "github.com/fiskerinc/cloud-services/pkg/flashpackversion" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/validator" +) + +func UpdateCarState(db *services.DB, vin string, data []byte) error { + logger.Debug().Msgf("UpdateCarState %s", vin) + var update common.CarStateUpdate + + err := json.Unmarshal(data, &update) + if err != nil { + return err + } + + err = validator.ValidateStruct(&update) + if err != nil { + logger.Err(err).Interface("CarStateUpdate", update).Str("CarStateUpdateByteValue as string", string(data)).Send() + return err + } + + return processUpdateCarStateECUs(db, vin, update.ECUs) +} + +func processUpdateCarStateECUs(db *services.DB, vin string, ecus map[string]common.CarECU) error { + cache := services.GetCarEcuCache() + insert := []common.CarECU{} + errs := []error{} + + for name, ecu := range ecus { + ecu.VIN = vin + ecu.ECU = name + err := validator.ValidateStruct(&ecu) + if err != nil { + logger.Error().Msgf("invalid CarECU %v", err) + errs = append(errs, err) + continue + } + + if !cache.Exists(ecu.CacheKey(), ecu.HashValues()) { + insert = append(insert, ecu) + } + } + + if len(insert) == 0 { + return combineErrors(errs) + } + + err := fv.InsertCarECUsAndUpdateFlashpackVersion(db.GetCars(), db.GetCarVersionsLog(), vin, insert) + if err != nil { + errs = append(errs, err) + } + + return combineErrors(errs) +} + +func combineErrors(errs []error) error { + if len(errs) == 0 { + return nil + } + errString := errs[0].Error() + for i := 1; i < len(errs); i++ { + errString += " " + errs[i].Error() + } + return errors.New(errString) +} diff --git a/services/attendant/handlers/car_update_state_test.go b/services/attendant/handlers/car_update_state_test.go new file mode 100644 index 0000000..f243762 --- /dev/null +++ b/services/attendant/handlers/car_update_state_test.go @@ -0,0 +1,113 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/testhelper" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +func TestUpdateCarState(t *testing.T) { + setupDBMock() + setupRedisMock() + + mockSap := vconfig.SAPServiceMock{} + services.SetSapService(mockSap) + + type testCase struct { + Name string + VIN string + Payload string + ExpectedErr string + } + + tests := []testCase{ + { + Name: "Empty ECU", + VIN: "JH4KA7680RC011845", + Payload: `{"ecus": {}}`, + ExpectedErr: "Key: 'CarStateUpdate.ECUs' Error:Field validation for 'ECUs' failed on the 'min' tag", + }, + { + Name: "Bad VIN and ECU", + VIN: "JH4KA7680RC01", + Payload: `{ + "ecus": { + "ECU1": { + "serial_number": "AAAAA", + "hw_version": "2000", + "boot_loader_version": "3000", + "fingerprint": "0xffffffffffffffffff", + "config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f", + "vendor": "A021E00029" + } + } + }`, + ExpectedErr: "Key: 'CarECU.VIN' Error:Field validation for 'VIN' failed on the 'vin' tag", + }, + { + Name: "Good ECUs with/without software version", + VIN: "JH4KA7680RC011845", + Payload: `{ + "ecus": { + "ECU1": { + "sw_version": "1000", + "serial_number": "AAAAA", + "hw_version": "2000", + "boot_loader_version": "3000", + "fingerprint": "0xffffffffffffffffff", + "config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f", + "vendor": "A021E00029" + }, + "ECU2": { + "serial_number": "BBBBBBB", + "hw_version": "2001", + "boot_loader_version": "3001", + "fingerprint": "0xffffffffffffffffff", + "config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f", + "vendor": "A021E00029" + } + } + }`, + ExpectedErr: "", + }, + { + Name: "bad ECU followed by a Good ECU", + VIN: "JH4KA7680RC011845", + Payload: `{ + "ecus": { + "ECU1": { + "serial_number": "AAAAA", + "hw_version": "2000", + "boot_loader_version": "3000", + "fingerprint": "0xffffffffffffffffff", + "config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f", + "vendor": "A021E00029", + "epoch_usec": "ABC" + }, + "ECU2": { + "serial_number": "BBBBBBB", + "hw_version": "2001", + "boot_loader_version": "3001", + "fingerprint": "0xffffffffffffffffff", + "config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f", + "vendor": "A021E00029" + } + } + }`, + ExpectedErr: "json: cannot unmarshal string into Go struct field CarECU.ecus.epoch_usec of type int64", + }, + } + + for _, test := range tests { + err := handlers.UpdateCarState(mockDB, test.VIN, []byte(test.Payload)) + if err != nil && test.ExpectedErr != err.Error() { + t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedErr, err.Error()) + } else if err == nil && test.ExpectedErr != "" { + t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedErr, err) + } + } +} diff --git a/services/attendant/handlers/get_ecc_keys.go b/services/attendant/handlers/get_ecc_keys.go new file mode 100644 index 0000000..b197fde --- /dev/null +++ b/services/attendant/handlers/get_ecc_keys.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/services" + "encoding/json" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" +) + +func GetAllEccKeys(db *services.DB, id string, data []byte) error { + logger.Debug().Msgf("GetAllEccKeys %v %s", common.TRex, id) + var err error + var eccKeys []common.ECCKeys + + req, err := parseGetAllEccKeysRequest(data) + if err != nil { + return err + } + + if req.CarUpdateID == 0 { + eccKeys, err = db.GetECCKeys().SelectAllPrivateKeysByVIN(id) + } else { + eccKeys, err = db.GetECCKeys().SelectAllPrivateKeysByCarUpdateID(req.CarUpdateID) + } + if err != nil { + return err + } + + if eccKeys == nil { + eccKeys = make([]common.ECCKeys, 0) + } + eccKeys = replaceECU(eccKeys) + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + err = client.SafePublishMessage(common.TRex.Key(id), common.Message{ + Handler: "ecc_keys", + Data: eccKeys, + }) + + if err != nil { + return err + } + + logger.Debug().Msgf("GetAllEccKeys sent %v %s", common.TRex, id) + + return nil +} + +func parseGetAllEccKeysRequest(data []byte) (common.CarUpdateRequest, error) { + var req common.CarUpdateRequest + + if len(data) == 0 { + return req, nil + } + + err := json.Unmarshal(data, &req) + if err != nil { + return req, errors.WithStack(err) + } + + return req, nil +} + +func replaceECU(eccKeys []common.ECCKeys) []common.ECCKeys { + + ecuReplacements := common.ECUReplacement() + for i := 0; i < len(eccKeys); i++ { + ecu := eccKeys[i].ECU + if replacement, exist := ecuReplacements[ecu]; exist { + eccKeys[i].ECU = replacement + } + } + return eccKeys +} + +/* +func notifyEccKeysGeneralError(client redis.Client, device common.Device, id string, err error) { + e := client.PublishMessage(device.Key(id), common.Message{ + Handler: "ecc_keys", + Data: []struct{ Error string }{ + { + Error: err.Error(), + }, + }, + }) + if e != nil { + logger.Error().Err(errors.WithStack(e)).Send() + } +} +*/ diff --git a/services/attendant/handlers/get_ecc_keys_test.go b/services/attendant/handlers/get_ecc_keys_test.go new file mode 100644 index 0000000..43ef1ee --- /dev/null +++ b/services/attendant/handlers/get_ecc_keys_test.go @@ -0,0 +1,111 @@ +package handlers_test + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/pkg/errors" +) + +func TestGetAllEccKeys(t *testing.T) { + testVIN := "JH4KA7680RC01" + trexKey := "1:JH4KA7680RC01" + + pk11 := common.NewBinaryHex([]byte("testprivkey11")) + pk12 := common.NewBinaryHex([]byte("testprivkey12")) + pk13 := common.NewBinaryHex([]byte("testprivkey13")) + pk21 := common.NewBinaryHex([]byte("testprivkey21")) + pk22 := common.NewBinaryHex([]byte("testprivkey22")) + pk23 := common.NewBinaryHex([]byte("testprivkey23")) + + testDBQuery := []common.ECCKeys{ + { + ECU: "testecu1", + PrivKey1: &pk11, + PrivKey2: &pk12, + PrivKey3: &pk13, + }, + { + ECU: "testecu2", + PrivKey1: &pk21, + PrivKey2: &pk22, + PrivKey3: &pk23, + }, + { + ECU: "PDU", + PrivKey1: &pk21, + PrivKey2: &pk22, + PrivKey3: &pk23, + }, + } + + mockRedis := tester.NewRedisMock() + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) + mockEccKeys := &mocks.MockEccKeys{} + mockDB := &services.DB{} + mockDB.SetECCKeys(mockEccKeys) + + tests := []AttendentRouteTestCase{ + { + Name: "[TREX] From DB, no car update id", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + ExpectedMessages: map[string]string{ + trexKey: `{"handler":"ecc_keys","data":[{"ecu":"testecu1","level_1":"74657374707269766b65793131","level_2":"74657374707269766b65793132","level_3":"74657374707269766b65793133"},{"ecu":"testecu2","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"},{"ecu":"OBC","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"}]}`, + }, + }, + MockEccKeysSelect: testDBQuery, + }, + { + Name: "[TREX] From DB, with car update id", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + ExpectedMessages: map[string]string{ + trexKey: `{"handler":"ecc_keys","data":[{"ecu":"testecu1","level_1":"74657374707269766b65793131","level_2":"74657374707269766b65793132","level_3":"74657374707269766b65793133"},{"ecu":"testecu2","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"},{"ecu":"OBC","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"}]}`, + }, + PayloadData: `{"car_update_id":10000}`, + }, + MockEccKeysSelect: testDBQuery, + }, + { + Name: "Error", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + ExpectedError: "something went wrong", + }, + MockEccKeysSelect: nil, + }, + } + + schemaTester := testhelper.NewSchemaTestHelper(t, schemaToTRex) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + mockRedis.Reset() + test.SetupRedis(mockRedis) + mockEccKeys.MockListResponse = test.MockEccKeysSelect + + if test.Name == "Error" { + mockEccKeys.Error = errors.New("something went wrong") + } else { + mockEccKeys.Error = nil + } + + err := handlers.GetAllEccKeys(mockDB, test.DeviceKey, []byte(test.PayloadData)) + + test.CheckHandlerError(t, test.Name, err) + test.Validate(t, test.Name, mockRedis) + + for _, m := range test.ExpectedMessages { + schemaTester.ValidateSchemaObject(test.Name, []byte(m)) + } + }) + } +} diff --git a/services/attendant/handlers/get_filekeys.go b/services/attendant/handlers/get_filekeys.go new file mode 100644 index 0000000..b8892b8 --- /dev/null +++ b/services/attendant/handlers/get_filekeys.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "encoding/json" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "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/validator" + + "github.com/pkg/errors" +) + +func GetFileKeys(db *services.DB, device common.Device, id string, data []byte) error { + logger.Debug().Msgf("GetFileKeys %v %s", device, id) + var err error + var req *common.FileKeysRequest + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + req, err = parseGetFileKeysRequest(data) + if err != nil { + notifyFileKeysGeneralError(client, device, id, err) + return err + } + + keys, err := cache.RetrieveFileEncryptionParams(client, db.GetFileKeys(), req.FileIDs) + if err != nil { + notifyFileKeysGeneralError(client, device, id, err) + return err + } + + err = client.SafeQueueMessage(device.Key(id), common.Message{ + Handler: "filekeys", + Data: keys, + }) + + if err != nil { + return err + } + + logger.Debug().Msgf("GetFileKeys sent %v %s", device, id) + + return nil +} + +func parseGetFileKeysRequest(data []byte) (*common.FileKeysRequest, error) { + var status common.FileKeysRequest + + err := json.Unmarshal(data, &status) + if err != nil { + return nil, errors.WithStack(err) + } + + err = validator.ValidateStruct(status) + if err != nil { + return &status, errors.WithStack(err) + } + + return &status, nil +} + +func notifyFileKeysGeneralError(client redis.Client, device common.Device, id string, err error) { + e := client.SafePublishMessage(device.Key(id), common.Message{ + Handler: "filekeys", + Data: []common.FileKeyResponse{ + { + FileID: "0", + Error: err.Error(), + }, + }, + }) + if e != nil { + logger.Error().Err(errors.WithStack(e)).Send() + } +} diff --git a/services/attendant/handlers/get_filekeys_test.go b/services/attendant/handlers/get_filekeys_test.go new file mode 100644 index 0000000..a09908b --- /dev/null +++ b/services/attendant/handlers/get_filekeys_test.go @@ -0,0 +1,124 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/testhelper" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" +) + +func TestGetFileKey(t *testing.T) { + testVIN := "JH4KA7680RC01" + trexKey := "1:JH4KA7680RC01" + hmiKey := "2:JH4KA7680RC01" + fileCache1 := "fileid:b7d94be8c94062cf" + fileCache2 := "fileid:83165a80c940e8b3" + testPayload := `{"file_ids": ["b7d94be8c94062cf","83165a80c940e8b3"]}` + testDBQuery := []common.FileKey{ + { + FileID: "b7d94be8c94062cf", + Auth: []byte("AuthValue"), + Key: []byte("KeyValue"), + Nonce: []byte("NonceValue"), + }, + { + FileID: "83165a80c940e8b3", + Auth: []byte("AuthValue2"), + Key: []byte("KeyValue2"), + Nonce: []byte("NonceValue2"), + }, + } + + mockRedis := tester.NewRedisMock() + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) + mockFileKeys := &mocks.MockFileKeys{} + mockDB := &services.DB{} + mockDB.SetFileKeys(mockFileKeys) + + tests := []AttendentRouteTestCase{ + { + Name: "[TREX] From DB", + RedisTestCase: tester.RedisTestCase{ + Device: common.TRex, + DeviceKey: testVIN, + PayloadData: testPayload, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + fileCache1: { + Value: `{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="}`, + Expires: 86400, + }, + fileCache2: { + Value: `{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}`, + Expires: 86400, + }, + }, + ExpectedMessages: map[string]string{ + trexKey: `{"handler":"filekeys","data":[{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="},{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}]}`, + }, + }, + MockFileKeysSelect: testDBQuery, + MockFileKeysError: nil, + }, + { + Name: "[HMI] From DB", + RedisTestCase: tester.RedisTestCase{ + Device: common.HMI, + DeviceKey: testVIN, + PayloadData: testPayload, + ExpectedCaches: map[string]tester.ExpiringCacheResult{ + fileCache1: { + Value: `{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="}`, + Expires: 86400, + }, + fileCache2: { + Value: `{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}`, + Expires: 86400, + }, + }, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"filekeys","data":[{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="},{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}]}`, + }, + }, + MockFileKeysSelect: testDBQuery, + MockFileKeysError: nil, + }, + } + + schemaTester := testhelper.NewSchemaTestHelper(t, schemaToTRex) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + mockRedis.Reset() + test.SetupRedis(mockRedis) + mockFileKeys.GetMultiResponse = test.MockFileKeysSelect + mockFileKeys.Error = test.MockFileKeysError + + err := handlers.GetFileKeys(mockDB, test.Device, test.DeviceKey, []byte(test.PayloadData)) + + test.CheckHandlerError(t, test.Name, err) + test.Validate(t, test.Name, mockRedis) + + for _, m := range test.ExpectedMessages { + schemaTester.ValidateSchemaObject(test.Name, []byte(m)) + } + }) + } +} + +func BenchmarkGetFileKey(b *testing.B) { + db := services.GetDB() + vin := "1F15K3R45N1234567" + id := []byte(`{"file_ids": ["b7d94be8c94062cf","83165a80c940e8b3"]}`) + + for n := 0; n < b.N; n++ { + err := handlers.GetFileKeys(db, common.TRex, vin, id) + if err != nil { + b.Error(err) + } + } +} diff --git a/services/attendant/handlers/mock_test.go b/services/attendant/handlers/mock_test.go new file mode 100644 index 0000000..6dd825f --- /dev/null +++ b/services/attendant/handlers/mock_test.go @@ -0,0 +1,113 @@ +package handlers_test + +import ( + "encoding/json" + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/go-pg/pg/v10/orm" +) + +var mockRedis *redis.Connection +var mockDB *services.DB +var testDateTime time.Time = time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC) + +func setupRedisMock() { + redis.MockRedisConnection() + mockRedis = &redis.Connection{} +} + +func setupDBMock() { + db := services.DB{} + db.SetCarUpdates(&mocks.MockCarUpdates{ + SelectCarUpdateResponse: &common.CarUpdate{ + ID: 1, + VIN: "FISKER123", + UpdateManifestID: 2, + UpdateManifest: &common.UpdateManifest{ + ID: 2, + Name: "TEST_PACKAGE", + Version: "1.0.0", + ReleaseNotes: "http://releasenotes.com", + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + }, + }, + }) + db.SetECU(&mocks.MockEcuDtc{}) + db.SetCars(&mocks.MockCars{ + SelectResponse: &common.Car{}, + }) + carVersionLogMock := mocks.MockCarVersionsLog{MockLogVersionChange: func(log *common.CarVersionLogs) (orm.Result, error) { + return nil, nil + }} + db.SetCarVersionsLog(&carVersionLogMock) + mockDB = &db +} + +type mockRedisCache struct { + redis.Connection +} + +func (c *mockRedisCache) GetSet(id string, data interface{}) error { + driverIDs := []string{"valid-cognito-id-1", "valid-cognito-id-2"} + + dataBytes, err := json.Marshal(driverIDs) + if err != nil { + return err + } + + err = json.Unmarshal(dataBytes, data) + if err != nil { + return err + } + + return nil +} + +func NewRedisMock() *tester.MockRedis { + redis.MockRedisConnection() + return &tester.MockRedis{} +} + +type AttendentRouteTestCase struct { + Name string + SelectCarUpdate *common.CarUpdate + SelectCarUpdates []common.CarUpdate + CarUpdateError error + SelectCarToDrivers []common.CarToDriver + CarToDriversError error + MockLoadManifest *common.UpdateManifest + MockFileKeysSelect []common.FileKey + MockFileKeysError error + MockEccKeysSelect []common.ECCKeys + tester.RedisTestCase +} + +func (at *AttendentRouteTestCase) SetupDB(mockCars *mocks.MockCars, mockCarUpdates *mocks.MockCarUpdates, test *AttendentRouteTestCase) { + if test == nil { + return + } + + if mockCars != nil { + mockCars.SelectCarsForDrivers = test.SelectCarToDrivers + mockCars.Error = test.CarToDriversError + } + + if mockCarUpdates != nil { + mockCarUpdates.SelectCarUpdateResponse = test.SelectCarUpdate + mockCarUpdates.SelectCarUpdatesResponse = test.SelectCarUpdates + mockCarUpdates.LoadManifest = test.MockLoadManifest + mockCarUpdates.Error = test.CarUpdateError + } +} diff --git a/services/attendant/handlers/order_updated.go b/services/attendant/handlers/order_updated.go new file mode 100644 index 0000000..3e33566 --- /dev/null +++ b/services/attendant/handlers/order_updated.go @@ -0,0 +1,160 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/manifestsender" + "github.com/fiskerinc/cloud-services/pkg/tmobile" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ENABLE_ORDERUPDATE_SENDS = envtool.GetEnvBool("ENABLE_ORDERUPDATE_SENDS", false) + +// Take the feature codes from VehicleOrder message to generate the VOD and CDSs +// Create a new common.UpdateManifest with the VOD and CDS and save it as a common.ConfigUpdateType +// Create a new common.CarUpdate for the VIN and the manifest in step 2 and save it to the database +// Copy the car update id into the UpdateManifest +// Send the UpdateManifest to both trex and hmi +func OrderUpdated(db *services.DB, smsClient sms.SMSServiceClient, vin string, data []byte) error { + logger.Debug().Msgf("Order updated %s", vin) + + var order common.VehicleOrder + err := json.Unmarshal(data, &order) + if err != nil { + return err + } + + err = setCarSettings(db, vin, order) + if err != nil { + return err + } + + err = changeRatePlan(db, smsClient, vin, order.VehicleSpecification.DestinationCountry) + if err != nil { + logger.Error().Msgf("Failed to change rate plan for %s, %s", vin, err) + } + + if !ENABLE_ORDERUPDATE_SENDS { + return nil + } + + cs := services.GetVehicleConfig() + r := services.RedisClientPool().GetFromPool() + defer r.Close() + + trex := manifestsender.NewTBOXManifestSender(r, cs, services.GetDB(), nil, nil) + defer trex.Close() + + // I don't think this has ever been called on production, or dev ! + input := manifestsender.ProcessConfigUpdateStruct{ + VIN: vin, + Name: "SAP order change update", + Username: "unidentified attendant sap user", + SendToCar: true, + DontCreateDatabaseEntry: false, + Forced: false, + } + _, err = trex.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData()) + + return err +} + +func setCarSettings(db *services.DB, vin string, order common.VehicleOrder) (err error) { + // If the sequence number is missing, go will fill it in as 0, which is the case where the car has no sequence number + if order.VehicleSpecification.SequenceNumber != "" { + _, err = db.GetCars().SetSetting(&common.CarSetting{ + VIN: vin, + Name: common.SEQUENCE_NUMBER, + Value: fmt.Sprint(order.VehicleSpecification.SequenceNumber), + Type: "string", + }) + if err != nil { + return err + } + } + + _, err = db.GetCars().SetSetting(&common.CarSetting{ + VIN: vin, + Name: common.BODY_COLOR, + Value: common.FeatureCodeToBodyColor(order.VehicleSpecification.VehicleFeatures), + Type: "string", + }) + if err != nil { + return err + } + + // Incase SAP hasn't sent this value yet + if order.VehicleSpecification.DestinationCountry != "" { + _, err = db.GetCars().SetSetting(&common.CarSetting{ + VIN: vin, + Name: common.DELIVERY_DESTINATION, + Value: order.VehicleSpecification.DestinationCountry, + Type: "string", + }) + if err != nil { + return err + } + } + + return +} + +func changeRatePlan(db *services.DB, smsClient sms.SMSServiceClient, vin, destinationCountry string) error { + car, err := services.GetDB().GetCars().SelectByVIN(vin) + if err != nil { + return err + } + + if len(car.ICCID) == 0 { + return fmt.Errorf("no iccid found for vehicle %s", vin) + } + iccid := strings.TrimSuffix(strings.ToLower(car.ICCID), "f") + + customAtributeReq := sms.CustomAtributesRequest{ + ICCID: iccid, + AccountCustom1: destinationCountry, + } + _, err = smsClient.HandleCustomAttributes(context.Background(), &customAtributeReq) + if err != nil { + return err + } + + ratePlan, err := db.GetRatePlan().Select(destinationCountry) + if err != nil { + return err + } + changeRatePlanRequest := sms.ChangeRatePlanRequest{ + ICCID: iccid, + ProductId: ratePlan.ProductID, + AccountId: tmobile.FISKER_TMOBILE_ACCOUNT_ID, + } + _, err = smsClient.HandleChangeRatePlan(context.Background(), &changeRatePlanRequest) + if err != nil { + return err + } + + go verifyRatePlan(smsClient, iccid, destinationCountry) + return nil +} + +func verifyRatePlan(smsClient sms.SMSServiceClient, iccid, destinationCountry string) error { + deviceDetailsRequest := sms.DeviceDetailsRequest{ + ICCID: iccid, + } + + details, err := smsClient.HandleDeviceDetails(context.Background(), &deviceDetailsRequest) + if err != nil { + logger.Error().Msgf("failed to check device details for iccid %s", iccid) + } + + logger.Info().Msgf("device details for iccid %s: %+v", iccid, details) + return nil +} diff --git a/services/attendant/handlers/order_updated_test.go b/services/attendant/handlers/order_updated_test.go new file mode 100644 index 0000000..19b6f83 --- /dev/null +++ b/services/attendant/handlers/order_updated_test.go @@ -0,0 +1,224 @@ +package handlers_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + q "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" + "github.com/stretchr/testify/assert" +) + +var ( + someErr = errors.New("some error") + vinMock = "FISKER123" + vodMock = "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + confMock = "000000000000000000000000000000000000000" + validOrder = common.VehicleOrder{ + SpecID: 800010200, + OrderNumber: 8000102, + MessageIdentifier: "VEHICLEORDERSUBMISSION", + VehicleSpecification: common.VehicleSpecification{ + OrderIndicator: "S", + FleetOrderIndicator: "N", + ProductionPhaseIndicator: "01", + VehicleIndicator: "000", + ManufacturingPlant: "G", + ExpectedReferenceDate: common.ExpectedReferenceDate{ + Time: time.Date(2022, 5, 26, 0, 0, 0, 0, time.UTC), + }, + ModelType: "FM29", + ModelYearIndicator: 2023, + VehicleModel: "F29", + VinPrefix: "VCF1ZBU2_PG", + VehicleFeatures: []common.FeatureCodes{ + { + FamilyCode: "2801", + FeatureCode: "280102", + }, + { + FamilyCode: "2804", + FeatureCode: "280401", + }, + { + FamilyCode: "2805", + FeatureCode: "280501", + }, + }, + }, + } + validUpdateManifest = common.UpdateManifest{ + ID: 1, + CarUpdateID: 1, + Version: fmt.Sprint(validOrder.OrderNumber), + Description: fmt.Sprintf("configuration %s %s", vinMock, validOrder.MessageIdentifier), + ManifestType: common.ConfigUpdateType, + VOD: "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + } + validRespECUs = []*common.UpdateManifestECU{ + { + UpdateManifestID: 1, + ECU: "ICC", + Configuration: confMock, + }, + } + smsMock = sms.NewSMSMockSuccess() + mockCars = mocks.MockCars{} +) + +func TestOrderUpdated(t *testing.T) { + handlers.ENABLE_ORDERUPDATE_SENDS = true + validUpdateManifestResp := validUpdateManifest + validUpdateManifestResp.ID = 1 + validUpdateManifestResp.ECUs = validRespECUs + + scrubbedValidUpdateManifest := validUpdateManifestResp.ToUpdateConfigManifest() + scrubbedValidUpdateManifest.Type = "standard" + + successPayload, _ := json.Marshal(validOrder) + redisMock := tester.NewRedisMock() + services.SetRedisClientPool(tester.NewMockClientPool(redisMock)) + mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) { + return common.SAPResponse{ + ModelYear: 2023, + ModelType: "Ocean", + VersionDuringModelYear: "1", + Features: []common.SAPFeature{ + { + FamilyCode: "FamilyCode1", + FeatureCode: "FeatureCode1", + }, + { + FamilyCode: "FamilyCode2", + FeatureCode: "FeatureCode2", + }, + { + FamilyCode: "VOD", + FeatureCode: vodMock, + }, + }}, nil + }} + services.SetSapService(mockSap) + mockCars.SetLoadResp(&common.Car{ + VIN: vinMock, + ICCID: "1234567890", + SoldStatus: common.CarSoldStatusRetailed, + }) + + tests := map[string]struct { + updateManifest q.UpdateManifestsInterface + cars q.CarsInterface + carsUpdate q.CarUpdatesInterface + ratePlan q.RatePlanInterface + vConfig vconfig.ConfigServiceInterface + payload []byte + expRedisMsgs map[string]interface{} + expErr error + }{ + "success": { + updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}}, + cars: &mockCars, + carsUpdate: &mocks.MockCarUpdates{}, + ratePlan: &mocks.MockRatePlan{}, + vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock}, + payload: successPayload, + expRedisMsgs: map[string]interface{}{ + "2:" + vinMock: common.Message{ + Handler: "car_update", + Data: common.CarUpdate{ + ID: 1, + VIN: vinMock, + UpdateManifestID: 1, + UpdateManifest: &validUpdateManifestResp, + }, + }, + "1:" + vinMock: common.Message{ + Handler: "config_update", + Data: scrubbedValidUpdateManifest, + }, + }, + expErr: nil, + }, + "json_parse_err": { + payload: []byte(`12`), + expErr: errors.New("json: cannot unmarshal number into Go value of type common.VehicleOrder"), + }, + "failed_cars_db": { + updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}}, + cars: &mocks.MockCars{ + DBMockHelper: mocks.DBMockHelper{ + Error: someErr, + }, + }, + carsUpdate: &mocks.MockCarUpdates{}, + ratePlan: &mocks.MockRatePlan{}, + vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock}, + payload: successPayload, + expErr: someErr, + }, + "failed_cds": { + updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}}, + carsUpdate: &mocks.MockCarUpdates{}, + cars: &mockCars, + ratePlan: &mocks.MockRatePlan{}, + vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: FailedGetCDSMock}, + payload: successPayload, + expErr: someErr, + }, + "failed_db": { + updateManifest: &mocks.MockUpdateManifests{ + DBMockHelper: mocks.DBMockHelper{ + Error: someErr, + }, + }, + cars: &mockCars, + carsUpdate: &mocks.MockCarUpdates{}, + ratePlan: &mocks.MockRatePlan{}, + vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock}, + payload: successPayload, + expErr: someErr, + }, + } + + for tname, tt := range tests { + t.Run(tname, func(t *testing.T) { + redisMock.Reset() + db := services.GetDB() + db.SetManifests(tt.updateManifest) + db.SetCars(tt.cars) + db.SetCarUpdates(tt.carsUpdate) + db.SetRatePlan(tt.ratePlan) + services.SetVehicleConfig(tt.vConfig) + err := handlers.OrderUpdated(db, &smsMock, vinMock, tt.payload) + if err != nil && tt.expErr != nil { + assert.Equal(t, tt.expErr.Error(), err.Error()) + return + } + assert.Equal(t, tt.expErr, err) + // assert.Equal(t, tt.expRedisMsgs, redisMock.PublishedMessages) + }) + } +} + +func SuccessGetCDSMock(request common.VODCDSRequest) (map[string]string, error) { + ecus := map[string]string{ + "VOD": vodMock, + "ICC": confMock, + } + + return ecus, nil +} +func FailedGetCDSMock(request common.VODCDSRequest) (map[string]string, error) { + return nil, someErr +} diff --git a/services/attendant/handlers/send_manifest.go b/services/attendant/handlers/send_manifest.go new file mode 100644 index 0000000..06cd9e8 --- /dev/null +++ b/services/attendant/handlers/send_manifest.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var PrivateSeed int64 // Used only for testing. Do not change otherwise + +func SendManifest(d *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) error { + logger.Debug().Msgf("SendManifest %v %s", device, id) + + clientPool := services.RedisClientPool() + + configService := services.GetVehicleConfig() + + generator := controllers.NewManifestSender(clientPool, d, configService, device, ka, PrivateSeed) + defer generator.Release() + + return generator.Process(id, data) +} diff --git a/services/attendant/handlers/send_manifest_sms_wake_up_callback.go b/services/attendant/handlers/send_manifest_sms_wake_up_callback.go new file mode 100644 index 0000000..3ee92f2 --- /dev/null +++ b/services/attendant/handlers/send_manifest_sms_wake_up_callback.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +// If the update is not finished installing on the car, we send out an sms to wake the car up. +// If this message is failed to deliver, we need to cancel the car update and set its status as failed +func CarSendManifestSMSWakeUpCallback(d *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) (err error) { + logger.Info().Msgf("SMS delivered but not sending TBox manifest %v %s", device, id) + return nil + /* + clientPool := services.RedisClientPool() + + sap := services.GetSapService() + configService := services.GetVehicleConfig() + + generator := controllers.NewManifestSender(clientPool, d, sap, configService, device, ka, PrivateSeed) + defer generator.Release() + + return generator.ContinueTBOXSend(id, data) */ +} diff --git a/services/attendant/handlers/send_manifest_test.go b/services/attendant/handlers/send_manifest_test.go new file mode 100644 index 0000000..e1b0d0b --- /dev/null +++ b/services/attendant/handlers/send_manifest_test.go @@ -0,0 +1,962 @@ +package handlers_test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "os" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel" + "github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams" + dbm "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/kafka" + kafkaMock "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" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" + "github.com/jinzhu/copier" +) + +func TestSendManifest(t *testing.T) { + os.Setenv("APP_SERVICE_NAME", "ATTENDANT") + handlers.PrivateSeed = 123456789 + + ka := services.NewKeepAwakeService() + ka.SetService(&services.MockKeepAwakeImplementation{}) + + now := time.Now() + testVIN := "JH4KA7680RC01" + tboxKey := common.TRex.Key(testVIN) + hmiKey := common.HMI.Key(testVIN) + + smsKey := "manifest_tbox_send_cache:100" + + var bhex common.BinaryHex = []byte("test") + redis.MockRedisConnection() + falsePtrValue := elptr.ElPtr(false) + truePtrValue := elptr.ElPtr(true) + ecuaRollback := []*common.UpdateManifestECU{ + { + ID: 100, + UpdateManifestID: 200, + ECU: "ECUA", + Version: "VERSIONOLD", + Mode: "A", + DBModelBase: th.Timestamp, + Files: []*common.UpdateManifestFile{ + { + FileID: "FILEIDOLD", + UpdateManifestECUID: 1001, + Filename: "FILENAMEOLD", + URL: "URLOLD", + FileType: common.Software, + WriteRegionID: 700, + WriteRegion: common.MemoryRegion{ + Offset: 701, + Length: 702, + }, + FileSize: 240, + DBModelBase: th.Timestamp, + }, + }, + }, + } + ecus := []*common.UpdateManifestECU{ + { + ECU: "ICC", + Version: "SWVERSION", + HWVersions: []string{"HWVERSION"}, + Mode: "D", + SelfDownload: true, + InstallPriority: 13, + Files: []*common.UpdateManifestFile{ + { + FileID: "AAAAAAA", + URL: "http://download.com/file1.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + DBModelBase: th.Timestamp, + }, + { + FileID: "SHOULD_NOT_BE_IN_UPDATE", + URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: falsePtrValue, + DBModelBase: th.Timestamp, + }, + { + FileID: "MUST_BE_IN_UPDATE", + URL: "http://download.com/MUST_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: truePtrValue, + DBModelBase: th.Timestamp, + }, + }, + }, + { + ECU: "ECUD", + Version: "SWVERSION", + HWVersions: []string{"HWVERSION"}, + Mode: "D", + InstallPriority: 4, + Files: []*common.UpdateManifestFile{ + { + FileID: "SHOULD_NOT_BE_IN_UPDATE", + URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: falsePtrValue, + DBModelBase: th.Timestamp, + }, + { + FileID: "MUST_BE_IN_UPDATE", + URL: "http://download.com/MUST_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: truePtrValue, + DBModelBase: th.Timestamp, + }, + { + FileID: "BBBBBBB", + URL: "http://download.com/file2.bin", + FileSize: 2000, + Checksum: "BBBBBBB", + FileType: common.Calibration, + WriteRegionID: 300, + WriteRegion: common.MemoryRegion{ + Offset: 301, + Length: 302, + }, + EraseRegionID: 400, + EraseRegion: &common.MemoryRegion{ + Offset: 401, + Length: 402, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + { + ID: 100, + UpdateManifestID: 200, + ECU: "PDU", + Version: "PDU-VERS", + HWVersions: []string{"PDU-VERS"}, + Mode: "PDU", + ConfigurationMask: "AAAAAAAA", + InstallPriority: 7, + Files: []*common.UpdateManifestFile{ + { + FileID: "NOT_BE_IN_UPDATE", + URL: "http://download.com/NOT_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: falsePtrValue, + DBModelBase: th.Timestamp, + }, + { + FileID: "MUST_BE_IN_UPDATE", + URL: "http://download.com/MUST_BE_IN_UPDATE.bin", + FileSize: 1000, + Checksum: "AAAAAAA", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Calibration, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: truePtrValue, + DBModelBase: th.Timestamp, + }, + { + FileID: "AAAAAAAA", + URL: "http://download.com/filea.bin", + FileSize: 2000, + Checksum: "AAAAAAAA", + WriteRegionID: 500, + FileType: common.Calibration, + WriteRegion: common.MemoryRegion{ + Offset: 501, + Length: 502, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + { + ID: 100, + UpdateManifestID: 200, + ECU: "ECUA", + Version: "A-VERS", + HWVersions: []string{"A-VERS"}, + Mode: "A", + ConfigurationMask: "AAAAAAAA", + InstallPriority: 1, + Files: []*common.UpdateManifestFile{ + { + FileID: "FILEID", + UpdateManifestECUID: 100, + Filename: "FILENAME", + URL: "URL", + FileType: common.Calibration, + WriteRegionID: 600, + WriteRegion: common.MemoryRegion{ + Offset: 601, + Length: 602, + }, + FileSize: 240, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + } + + fingerprint := getTodaysFingerprint() + + standardManifest := common.UpdateManifest{ + ID: 100, + CarUpdateID: 297, + Name: "TEST", + Version: "MANIFEST_VERSION", + SUMS: "2023.10.01.00", + Description: "TESTDESC", + ReleaseNotes: "http://fiskerinc.com/release_notes", + Type: common.ManifestTypeForced, + Active: truePtrValue, + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + RollbackEnabled: true, + ECUs: ecus, + UpdateDuration: 30, + } + + rollbackDisabledManifest := common.UpdateManifest{ + ID: 100, + CarUpdateID: 297, + Name: "TEST", + Version: "MANIFEST_VERSION", + SUMS: "2023.10.01.00.E", + Description: "TESTDESC", + ReleaseNotes: "http://fiskerinc.com/release_notes", + Type: "standard", + Active: truePtrValue, + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + ECUs: ecus, + UpdateDuration: 30, + } + standardManifestNoICC := common.UpdateManifest{ + ID: 100, + CarUpdateID: 297, + Name: "TEST", + Version: "MANIFEST_VERSION", + SUMS: "2023.10.01.00.E", + Description: "TESTDESC", + ReleaseNotes: "http://fiskerinc.com/release_notes", + Type: "standard", + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + RollbackEnabled: true, + BodyType: "truck", + ECUs: []*common.UpdateManifestECU{ + { + ECU: "ECUD", + HWVersions: []string{"HWVERSION"}, + Version: "SWVERSION", + Mode: "D", + InstallPriority: 7, + Files: []*common.UpdateManifestFile{ + { + FileID: "BBBBBBB", + URL: "http://download.com/file2.bin", + FileSize: 2000, + Checksum: "BBBBBBB", + FileType: common.Calibration, + WriteRegionID: 700, + WriteRegion: common.MemoryRegion{ + Offset: 701, + Length: 702, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + { + ID: 100, + UpdateManifestID: 200, + ECU: "PDU", + Version: "PDU-VERS", + HWVersions: []string{"PDU-VERS"}, + Mode: "PDU", + ConfigurationMask: "AAAAAAAA", + InstallPriority: 10, + Files: []*common.UpdateManifestFile{ + { + FileID: "AAAAAAAA", + URL: "http://download.com/filea.bin", + FileSize: 2000, + FileType: common.Calibration, + Checksum: "AAAAAAAA", + WriteRegionID: 800, + WriteRegion: common.MemoryRegion{ + Offset: 801, + Length: 802, + }, + EraseRegionID: 900, + EraseRegion: &common.MemoryRegion{ + Offset: 901, + Length: 902, + }, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + { + ID: 100, + UpdateManifestID: 200, + ECU: "ECUA", + Version: "A-VERS", + HWVersions: []string{"A-VERS"}, + Mode: "A", + ConfigurationMask: "AAAAAAAA", + InstallPriority: 4, + Files: []*common.UpdateManifestFile{ + { + FileID: "FILEID", + UpdateManifestECUID: 100, + Filename: "FILENAME", + URL: "URL", + FileType: common.Calibration, + WriteRegionID: 600, + WriteRegion: common.MemoryRegion{ + Offset: 601, + Length: 602, + }, + FileSize: 240, + DBModelBase: th.Timestamp, + }, + }, + DBModelBase: th.Timestamp, + }, + }, + UpdateDuration: 30, + } + mockCarUpdate := &common.CarUpdate{ + ID: 297, + VIN: testVIN, + UpdateManifestID: 100, + } + mockCarECUs := []common.CarECU{ + {ECU: "ECUD", HWVersion: "HWVERSION"}, + {ECU: "ECUA", HWVersion: "A-VERS", Version: "TEST122"}, + {ECU: "OBC", HWVersion: "PDU-VERS", Version: "TEST121"}, + {ECU: "BCM", HWVersion: ";#A;#"}, + {ECU: "ICC", HWVersion: "HWVERSION", Version: "TEST123"}, + } + mockRedis := rm.MockRedis{} + mockCUDB := dbm.MockCarUpdates{} + mockManDB := dbm.MockUpdateManifests{} + mockKeys := dbm.MockEccKeys{ + MockListResponse: []common.ECCKeys{ + { + ECU: "PDU", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + Env: "current", + DBModelBase: dbbasemodel.DBModelBase{ + CreatedAt: &now, + UpdatedAt: &now, + }, + }, + { + ECU: "ECUA", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + Env: "current", + DBModelBase: dbbasemodel.DBModelBase{ + CreatedAt: &now, + UpdatedAt: &now, + }, + }, + { + ECU: "ECUD", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + Env: "current", + DBModelBase: dbbasemodel.DBModelBase{ + CreatedAt: &now, + UpdatedAt: &now, + }, + }, + }, + } + services.GetDB().SetCarUpdates(&mockCUDB) + services.GetDB().SetCars(&dbm.MockCars{SelectCarECUs: mockCarECUs, SelectResponse: &common.Car{ICCID: "8000000000000000000"}}) + services.GetDB().SetManifests(&mockManDB) + services.GetDB().SetECCKeys(&mockKeys) + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) { + return common.SAPResponse{ + ModelYear: 2023, + ModelType: "Ocean", + VersionDuringModelYear: "1", + Features: []common.SAPFeature{ + { + FamilyCode: "FamilyCode1", + FeatureCode: "FeatureCode1", + }, + { + FamilyCode: "FamilyCode2", + FeatureCode: "FeatureCode2", + }, + }, + }, nil + }} + mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) { + return map[string]string{ + "EPS": "000147303031313632011D", + "ESP": "000F47303031313632000001012301027600000101000002B0", + "ECUA": "config", + "VOD": "", + "ADAS": "000147303031313632011D", + }, nil + }} + mockSMS := &sms.SMSMock{} + mockSMS.SetHandleSMSQueueResponse(&sms.SMSQueueResponse{ + SmsMsgID: "100", + SentSuccessful: true, + }, nil) + + services.SetSapService(mockSap) + services.SetVehicleConfig(mockConf) + services.SetSmsClient(mockSMS) + + mockKafkaProducer := kafkaMock.GetKafkaMock(nil) + services.SetKafkaProducer(mockKafkaProducer) + + clonedStandardManifest := common.UpdateManifest{} + clonedStandardManifestNoICC := common.UpdateManifest{} + err := copier.CopyWithOption(&clonedStandardManifest, &standardManifest, copier.Option{DeepCopy: true}) + if err != nil { + t.Fatal(err) + } + err = copier.CopyWithOption(&clonedStandardManifestNoICC, &standardManifestNoICC, copier.Option{DeepCopy: true}) + if err != nil { + t.Fatal(err) + } + + tests := []testrunner.TestCase{ + { + Name: "Send manifest w Self Download", + DBTestCase: &dbm.DBTestCase{ + MockLoadResponse: mockCarUpdate, + SetupMockResponse: func() { + mockCUDB.LoadManifest = &clonedStandardManifest + mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) { + if man.ECU == "ECUA" { + return ecuaRollback, nil + } + + return nil, nil + } + }, + }, + RedisTestCase: &rm.RedisTestCase{ + Device: common.Service, + DeviceKey: kafka.OTAUpdateService, + PayloadData: `{"car_update_id":297}`, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":301,"length":302},"erase_region":{"offset":401,"length":402}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":501,"length":502}}]},{"name":"ICC","version":"SWVERSION","current_version":"TEST123","hw_version":"HWVERSION","self_download":true,"files":[{"file_id":"AAAAAAA","url":"http://download.com/file1.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"forced","vod":"01011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110100202310010011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100001e","update_duration":30}}`, + }, + ExpectedCaches: map[string]rm.ExpiringCacheResult{ + // carupdateKey: { + // Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":297,"info":"ICC","installed":0,"status":"sent","total_files":0,"total_size":0}`, + // Expires: expectedExpire, + // }, + smsKey: { + Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`, + }, + }, + }, + }, + { + Name: "Send manifest w Self Download rollback disabled", + DBTestCase: &dbm.DBTestCase{ + MockLoadResponse: mockCarUpdate, + SetupMockResponse: func() { + mockCUDB.LoadManifest = &rollbackDisabledManifest + mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) { + if man.ECU == "ECUA" { + return ecuaRollback, nil + } + + return nil, nil + } + }, + }, + RedisTestCase: &rm.RedisTestCase{ + Device: common.Service, + DeviceKey: kafka.OTAUpdateService, + PayloadData: `{"car_update_id":297}`, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":301,"length":302},"erase_region":{"offset":401,"length":402}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":501,"length":502}}]},{"name":"ICC","version":"SWVERSION","current_version":"TEST123","hw_version":"HWVERSION","self_download":true,"files":[{"file_id":"AAAAAAA","url":"http://download.com/file1.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":false,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`, + }, + ExpectedCaches: map[string]rm.ExpiringCacheResult{ + smsKey: { + Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`, + }, + }, + }, + }, + { + Name: "Bad message", + RedisTestCase: &rm.RedisTestCase{ + Device: common.HMI, + DeviceKey: testVIN, + PayloadData: `{"car_update_id":`, + ExpectedError: "unexpected end of JSON input", + }, + }, + { + Name: "Send manifest w No Self Download", + DBTestCase: &dbm.DBTestCase{ + MockLoadResponse: mockCarUpdate, + SetupMockResponse: func() { + mockCUDB.LoadManifest = &clonedStandardManifestNoICC + }, + }, + RedisTestCase: &rm.RedisTestCase{ + Device: common.Service, + DeviceKey: kafka.OTAUpdateService, + PayloadData: `{"car_update_id":297}`, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":701,"length":702}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":801,"length":802},"erase_region":{"offset":901,"length":902}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`, + tboxKey: `{"handler":"update_manifest","data":{"ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","configuration":"config","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}],"rollback":[{"version":"VERSIONOLD","files":[{"file_id":"FILEIDOLD","url":"URLOLD","file_size":240,"type":"software","write_region":{"offset":701,"length":702}}]}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":701,"length":702}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":801,"length":802},"erase_region":{"offset":901,"length":902}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`, + }, + ExpectedCaches: map[string]rm.ExpiringCacheResult{ + "manifest_tbox_send_cache:UQIDEPFQUH": { + Value: `{"VIN":"JH4KA7680RC01","ManifestID":0,"Destination":"ICC/TBOX"}`, + }, + }, + }, + }, + } + + schemaTesterHMI := th.NewSchemaTestHelper(t, schemaToHMI) + schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex) + + for _, test := range tests { + mockRedis.Reset() + for _, ecu := range ecus { + ecu.Rollback = nil + } + + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mockCUDB) + } + if test.RedisTestCase != nil { + test.RedisTestCase.SetupRedis(&mockRedis) + + err := handlers.SendManifest(services.GetDB(), ka, test.RedisTestCase.Device, test.RedisTestCase.DeviceKey, []byte(test.RedisTestCase.PayloadData)) + + test.RedisTestCase.CheckHandlerError(t, test.Name, err) + test.RedisTestCase.Validate(t, test.Name, &mockRedis) + } + + if test.DBTestCase != nil { + test.DBTestCase.Validate(t, test.Name, &mockCUDB) + } + + for key, m := range test.RedisTestCase.ExpectedMessages { + name := fmt.Sprintf("%s %s", test.Name, key) + if strings.Contains(key, "2:") { + schemaTesterHMI.ValidateSchemaObject(name, []byte(m)) + continue + } + + schemaTesterTRex.ValidateSchemaObject(name, []byte(m)) + } + } +} + +func TestSendManifestMultiFile(t *testing.T) { + os.Setenv("APP_SERVICE_NAME", "ATTENDANT") + testVIN := "JH4KA7680RC01" + now := time.Now() + hmiKey := common.HMI.Key(testVIN) + falsePtrValue := elptr.ElPtr(false) + truePtrValue := elptr.ElPtr(true) + var bhex common.BinaryHex = []byte("test") + redis.MockRedisConnection() + ka := services.NewKeepAwakeService() + ka.SetService(&services.MockKeepAwakeImplementation{}) + + ecus := []*common.UpdateManifestECU{ + { + ECU: "BCM", + Version: "DB22121A", + HWVersions: []string{";#A;#"}, + Mode: "D", + SelfDownload: true, + Files: []*common.UpdateManifestFile{ + { + FileID: "ShouldNotSee", + Filename: "notparsed.s19", + URL: "fakeurl.com", + Checksum: "shouldnotsee", + Parsed: falsePtrValue, + }, + { + FileID: "3ee5fd6bc9402d67", + Filename: "MAGNA_BCM_FBL_driver.s19_0.bin", + URL: "https://upload-dev.fiskerdps.com/4466d78a-50e1-4f1d-a4b8-5f78076758ed/MAGNA_BCM_FBL_driver.s19_0.bin", + FileSize: 1000, + Checksum: "shouldsee", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Bootloader, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + DBModelBase: th.Timestamp, + Parsed: truePtrValue, + }, + { + FileID: "7878ede9c9407779", + Filename: "FM29_Application_FR40_220111.s19_0.bin", + URL: "ttps://upload-dev.fiskerdps.com/b5c3c8c8-86cd-422e-aa11-a248697edfa3/FM29_Application_FR40_220111.s19_0.bin", + FileSize: 1000, + Checksum: "SHOULD NOT SEE", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Software, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: truePtrValue, + Signature: "ShouldNOtSee", + DBModelBase: th.Timestamp, + }, + { + FileID: "7bb5083dc9407ae9", + Filename: "FM29_Application_FR40_220111.s19_1.bin", + URL: "https://upload-dev.fiskerdps.com/2cdeb566-cb00-42cd-93a8-69769b19269a/FM29_Application_FR40_220111.s19_1.bin", + FileSize: 1000, + Checksum: "shantsee", + WriteRegionID: 100, + WriteRegion: common.MemoryRegion{ + Offset: 101, + Length: 102, + }, + FileType: common.Software, + EraseRegionID: 200, + EraseRegion: &common.MemoryRegion{ + Offset: 201, + Length: 202, + }, + Parsed: truePtrValue, + DBModelBase: th.Timestamp, + Signature: "SHOULD NOT SEE", + }, + { + FileID: "7ebb4fadc9409b91", + Filename: "FM29_Application_FR40_220111.s19_2.bin", + URL: "https://upload-dev.fiskerdps.com/ec9f8a84-61fb-4a9b-9735-ac6f91b07a83/FM29_Application_FR40_220111.s19_2.bin", + FileType: common.Software, + Parsed: truePtrValue, + Signature: "SomeSignatureYouShouldSee", + }, + }, + }, + } + + fingerprint := getTodaysFingerprint() + + standardManifest := common.UpdateManifest{ + ID: 100, + CarUpdateID: 297, + Name: "TEST", + Version: "MANIFEST_VERSION", + SUMS: "2023.10.01.00", + Description: "TESTDESC", + ReleaseNotes: "http://fiskerinc.com/release_notes", + Type: "standard", + Active: truePtrValue, + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + RollbackEnabled: false, + ECUs: ecus, + UpdateDuration: 30, + } + + mockCarUpdate := &common.CarUpdate{ + ID: 297, + VIN: testVIN, + UpdateManifestID: 100, + } + mockCarECUs := []common.CarECU{ + {ECU: "ECUD", HWVersion: "HWVERSION"}, + {ECU: "ECUA", HWVersion: "A-VERS"}, + {ECU: "OBC", HWVersion: "PDU-VERS"}, + {ECU: "BCM", HWVersion: ";#A;#"}, + {ECU: "ICC", HWVersion: "HWVERSION"}, + } + mockRedis := rm.MockRedis{} + mockCUDB := dbm.MockCarUpdates{} + mockManDB := dbm.MockUpdateManifests{} + mockKeys := dbm.MockEccKeys{ + MockListResponse: []common.ECCKeys{ + { + ECU: "BCM", + PrivKey1: &bhex, + PrivKey2: &bhex, + PrivKey3: &bhex, + PubKey1: &bhex, + PubKey2: &bhex, + PubKey3: &bhex, + Env: "current", + DBModelBase: dbbasemodel.DBModelBase{ + CreatedAt: &now, + UpdatedAt: &now, + }, + }, + }, + } + services.GetDB().SetCarUpdates(&mockCUDB) + services.GetDB().SetCars(&dbm.MockCars{SelectCarECUs: mockCarECUs, SelectResponse: &common.Car{ICCID: ""}}) + services.GetDB().SetManifests(&mockManDB) + services.GetDB().SetECCKeys(&mockKeys) + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + + mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) { + return common.SAPResponse{ + ModelYear: 2023, + ModelType: "Ocean", + VersionDuringModelYear: "1", + Features: []common.SAPFeature{ + { + FamilyCode: "FamilyCode1", + FeatureCode: "FeatureCode1", + }, + { + FamilyCode: "FamilyCode2", + FeatureCode: "FeatureCode2", + }, + }, + }, nil + }} + mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) { + return map[string]string{ + "ECUA": "config", + "VOD": "", + }, nil + }} + mockSMS := &sms.SMSMock{} + mockSMS.SetHandleSMSQueueResponse(&sms.SMSQueueResponse{ + SmsMsgID: "100", + SentSuccessful: true, + }, nil) + services.SetSmsClient(mockSMS) + services.SetSapService(mockSap) + services.SetVehicleConfig(mockConf) + smsKey := "manifest_tbox_send_cache:100" + tests := []testrunner.TestCase{ + { + Name: "MultiFile right order", + DBTestCase: &dbm.DBTestCase{ + MockLoadResponse: mockCarUpdate, + SetupMockResponse: func() { + mockCUDB.LoadManifest = &standardManifest + mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) { + if man.ECU == "ECUA" { + return nil, nil + } + + return nil, nil + } + }, + }, + RedisTestCase: &rm.RedisTestCase{ + Device: common.Service, + DeviceKey: kafka.OTAUpdateService, + PayloadData: `{"car_update_id":297}`, + ExpectedMessages: map[string]string{ + hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"BCM","version":"DB22121A","hw_version":";#A;#","self_download":true,"files":[{"file_id":"3ee5fd6bc9402d67","url":"https://upload-dev.fiskerdps.com/4466d78a-50e1-4f1d-a4b8-5f78076758ed/MAGNA_BCM_FBL_driver.s19_0.bin","file_size":1000,"checksum":"shouldsee","type":"bootloader","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7878ede9c9407779","url":"ttps://upload-dev.fiskerdps.com/b5c3c8c8-86cd-422e-aa11-a248697edfa3/FM29_Application_FR40_220111.s19_0.bin","file_size":1000,"type":"software","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7bb5083dc9407ae9","url":"https://upload-dev.fiskerdps.com/2cdeb566-cb00-42cd-93a8-69769b19269a/FM29_Application_FR40_220111.s19_1.bin","file_size":1000,"type":"software","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7ebb4fadc9409b91","url":"https://upload-dev.fiskerdps.com/ec9f8a84-61fb-4a9b-9735-ac6f91b07a83/FM29_Application_FR40_220111.s19_2.bin","type":"software","write_region":{"offset":0,"length":0},"signature":"SomeSignatureYouShouldSee"}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":false,"type":"standard","vod":"01011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110100202310010011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100001e","update_duration":30}}`, + }, + ExpectedCaches: map[string]rm.ExpiringCacheResult{smsKey: { + Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`, + }}, + }, + }, + } + + for _, test := range tests { + mockRedis.Reset() + for _, ecu := range ecus { + ecu.Rollback = nil + } + + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mockCUDB) + } + if test.RedisTestCase != nil { + test.RedisTestCase.SetupRedis(&mockRedis) + + err := handlers.SendManifest(services.GetDB(), ka, test.RedisTestCase.Device, test.RedisTestCase.DeviceKey, []byte(test.RedisTestCase.PayloadData)) + + test.RedisTestCase.CheckHandlerError(t, test.Name, err) + test.RedisTestCase.Validate(t, test.Name, &mockRedis) + } + + if test.DBTestCase != nil { + test.DBTestCase.Validate(t, test.Name, &mockCUDB) + } + } +} + +func TestSendManifestIntegration(t *testing.T) { + os.Setenv("APP_SERVICE_NAME", "ATTENDANT") + ka := services.NewKeepAwakeService() + ka.SetService(&services.MockKeepAwakeImplementation{}) + err := handlers.SendManifest(services.GetDB(), ka, common.Service, "", []byte(`{"car_update_id":297}`)) + if err != nil { + t.Error(err) + } +} + +func getTodaysFingerprint() string { + // generate today's fingerprint + fpparams := manifestfingerprintparams.GetFPParams() + var fm = common.UpdateManifest{} + fm.GenerateFingerprint(fpparams.CurTime(), fpparams.ManifestSerial()) + return fm.Fingerprint +} diff --git a/services/attendant/handlers/update_approve.go b/services/attendant/handlers/update_approve.go new file mode 100644 index 0000000..da56b33 --- /dev/null +++ b/services/attendant/handlers/update_approve.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "encoding/json" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + + "github.com/pkg/errors" +) + +// UpdateData extracts update ID from byte slice +type UpdateData struct { + ID int `json:"id"` +} + +// ApproveUpdate updates DB update field and +// +// sends redis message to vehicle to initialize download +func ApproveUpdate(db *services.DB, id string, data []byte) error { + logger.Debug().Msgf("ApproveUpdate %s", id) + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + // TODO: NEEDS VALIDATION THAT INCOMING ID CAN APPROVE + // CAN COME FROM MOBILE OR HMI - NEEDS TO HANDLE BOTH CASES + + var u UpdateData + err := json.Unmarshal(data, &u) + if err != nil { + return errors.WithStack(err) + } + + update, err := db.ModifyUpdateStatus(u.ID, "approved") + if err != nil { + return err + } + logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", update.VIN, "update_download", u) + + err = client.SafeQueueMessage( + common.TRex.Key(update.VIN), + common.Message{ + Handler: "update_download", + Data: u, + }, + ) + if err != nil { + return err + } + + return nil +} diff --git a/services/attendant/handlers/update_approve_test.go b/services/attendant/handlers/update_approve_test.go new file mode 100644 index 0000000..39c03c8 --- /dev/null +++ b/services/attendant/handlers/update_approve_test.go @@ -0,0 +1,28 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestApproveUpdate(t *testing.T) { + mockRedis := &tester.MockRedis{} + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) + + data := []byte(`{"id": 1}`) + + err := handlers.ApproveUpdate(mockDB, "FISKER123", data) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "ApproveUpdate", "no error", err) + } + + err = handlers.ApproveUpdate(mockDB, "FISKER123", nil) + if err == nil { + t.Errorf(testhelper.TestErrorTemplate, "ApproveUpdate", "error", err) + } +} diff --git a/services/attendant/handlers/updates_get.go b/services/attendant/handlers/updates_get.go new file mode 100644 index 0000000..0ab84a7 --- /dev/null +++ b/services/attendant/handlers/updates_get.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "encoding/json" + + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/common" + s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/pkg/errors" +) + +// VehicleData extracts VIN from byte slice +type VehicleData struct { + VIN string `json:"vin" validate:"required,vin"` +} + +// GetUpdates queries DB for updates based off VIN and +// +// sends redis message back to requester +func GetUpdates(db *services.DB, id string, data []byte) error { + clientPool := services.RedisClientPool() + + var v VehicleData + err := json.Unmarshal(data, &v) + if err != nil { + return errors.WithStack(err) + } + + err = validator.ValidateStruct(&v) + if err != nil { + return errors.WithStack(err) + } + + ok, err := cache.VerifyCarToDriver(clientPool, db.GetCars(), v.VIN, id) + if err != nil { + return err + } else if !ok { + return cache.ErrInvalidCarToDriverAssociation(v.VIN, id) + } + + cu := common.CarUpdate{ + VIN: v.VIN, + Status: s.InstallApprovalAwait, + } + updates, err := db.GetCarUpdates().Select(&cu, nil) + if err != nil { + return err + } + + result := make([]common.ApprovalUpdate, len(updates)) + for i := range updates { + result[i] = common.NewApprovalUpdates(&updates[i]) + } + + client := clientPool.GetFromPool() + defer client.Close() + logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", id, "updates", result) + return client.SafeQueueMessage( + common.Mobile.Key(id), + common.Message{ + Handler: "updates", + Data: result, + }, + ) +} diff --git a/services/attendant/handlers/updates_get_test.go b/services/attendant/handlers/updates_get_test.go new file mode 100644 index 0000000..08a720b --- /dev/null +++ b/services/attendant/handlers/updates_get_test.go @@ -0,0 +1,148 @@ +package handlers_test + +import ( + "fmt" + "testing" + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/pkg/errors" +) + +func TestUpdatesGet(t *testing.T) { + mockRedis := &tester.MockRedis{} + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) + mockCars := &mocks.MockCars{} + mockCarUpdates := &mocks.MockCarUpdates{} + mockDB = &services.DB{} + mockDB.SetCars(mockCars) + mockDB.SetCarUpdates(mockCarUpdates) + + cognitoID := "valid-cognito-id-1" + mobileKey := common.Mobile.Key(cognitoID) + vin := "JM1BG2241R0797923" + data := fmt.Sprintf(`{"vin":"%s"}`, vin) + now := time.Now() + selectList := []common.CarUpdate{ + { + ID: 1234, + VIN: vin, + UpdateManifestID: 4321, + Status: s.InstallApprovalAwait, + UpdateManifest: &common.UpdateManifest{ + Name: "TEST", + Description: "TEST", + ReleaseNotes: "http://releasenotes.com", + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + ECUs: []*common.UpdateManifestECU{ + { + ECU: "ADAS", + Version: "VERSION", + Mode: "A", + }, + }, + DBModelBase: dbbasemodel.DBModelBase{ + CreatedAt: &now, + UpdatedAt: &now, + }, + }, + }, + } + carToDrivers := []common.CarToDriver{ + { + ID: 2000, + VIN: vin, + DriverID: cognitoID, + }, + } + + tests := []AttendentRouteTestCase{ + { + Name: "No data", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: "", + ExpectedError: "unexpected end of JSON input", + }, + }, + { + Name: "Bad request", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: "{}", + ExpectedError: "Key: 'VehicleData.VIN' Error:Field validation for 'VIN' failed on the 'required' tag", + }, + }, + { + Name: "Bad association", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: data, + ExpectedError: "no relationship found between vin JM1BG2241R0797923 and driver valid-cognito-id-1", + }, + }, + { + Name: "Cache error", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: data, + ExpectedError: "cache error", + }, + CarToDriversError: errors.New("cache error"), + }, + { + Name: "Good request", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: data, + ExpectedMessages: map[string]string{ + mobileKey: `{"handler":"updates","data":[{"id":1234,"vin":"JM1BG2241R0797923","name":"TEST","description":"TEST","release_notes":"http://releasenotes.com"}]}`, + }, + }, + SelectCarToDrivers: carToDrivers, + SelectCarUpdates: selectList, + }, + { + Name: "DB error", + RedisTestCase: tester.RedisTestCase{ + Device: common.Mobile, + DeviceKey: cognitoID, + PayloadData: data, + ExpectedError: "database error", + }, + SelectCarToDrivers: carToDrivers, + SelectCarUpdates: nil, + CarUpdateError: errors.New("database error"), + }, + } + + for i := range tests { + mockRedis.Reset() + test := &tests[i] + test.SetupRedis(mockRedis) + test.SetupDB(mockCars, mockCarUpdates, test) + + err := handlers.GetUpdates(mockDB, test.DeviceKey, []byte(test.PayloadData)) + + test.CheckHandlerError(t, test.Name, err) + test.Validate(t, test.Name, mockRedis) + } +} diff --git a/services/attendant/handlers/upload_dtc.go b/services/attendant/handlers/upload_dtc.go new file mode 100644 index 0000000..03444b9 --- /dev/null +++ b/services/attendant/handlers/upload_dtc.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "encoding/json" + "time" + + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/pkg/errors" +) + +func UploadDtc(id string, data []byte) error { + // ID is a vin in this case + logger.Debug().Msgf("Upload DTC for %s", id) + var DTCEntry controllers.DTCEntry + DTCEntry.VIN = id + + err := json.Unmarshal(data, &DTCEntry) + if err != nil { + return err + } + + if err = validator.GetValidator().Struct(&DTCEntry); err != nil { + return errors.WithMessage(err, "failed structure validation") + } + + err = DTCEntry.ParseSnapshot() + if err != nil { + logger.Warn().Msg(err.Error()) + return err + } + client, err := services.GetMongoClient() + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + return err + } + dtcs := mongo.NewCollection(client.Collection("dtcs")) + DTCEntry.CreatedAt = time.Now() + _, err = dtcs.InsertOne(DTCEntry) + return err +} diff --git a/services/attendant/main.go b/services/attendant/main.go new file mode 100644 index 0000000..d6fb4e7 --- /dev/null +++ b/services/attendant/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/server" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/app" +) + +func init() { + app.Setup("attendant", cleanup) +} + +func main() { + defer cleanup() + + go controllers.HealthCheck() + go server.StartConsumer(kafka.AttendantServiceGRPCKafka, kafka.AttendantService) + + select {} +} + +func cleanup() { + logger.Close() +} diff --git a/services/attendant/server/errors.go b/services/attendant/server/errors.go new file mode 100644 index 0000000..275ad00 --- /dev/null +++ b/services/attendant/server/errors.go @@ -0,0 +1,7 @@ +package server + +import ( + "github.com/pkg/errors" +) + +var ErrInvalidDevice = errors.New("invalid device associated to message") diff --git a/services/attendant/server/server_consumer.go b/services/attendant/server/server_consumer.go new file mode 100644 index 0000000..7b5a3c8 --- /dev/null +++ b/services/attendant/server/server_consumer.go @@ -0,0 +1,221 @@ +package server + +import ( + "github.com/fiskerinc/cloud-services/services/attendant/controllers" + "github.com/fiskerinc/cloud-services/services/attendant/handlers" + "github.com/fiskerinc/cloud-services/services/attendant/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/tmobile" + + "google.golang.org/protobuf/proto" +) + +// StartConsumer runs consumer and puts events into a channel for router +func StartConsumer(topic, oldTopic string) { + defer func() { + if err := recover(); err != nil { + logger.Error().Msgf("PanicConsumer %v", err) + } + }() + + events := make(chan *kafka.Message) + eventsJSON := make(chan common.EventRawJSON) + + go routeEvents(events) + go routeOldEvents(eventsJSON) + + logger.Info().Msgf("consumer intialized for topic: %v", topic) + consumer, oldConsumer, err := services.GetKafkaConsumer() + if err != nil { + panic(err) + } + go func() { + err = oldConsumer.ConsumeToChannelJson([]string{oldTopic}, eventsJSON) + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) + }() + + err = consumer.ConsumeToChannel([]string{topic}, events) + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) +} + +func routeEvents(events chan *kafka.Message) { + db := services.GetDB() + defer db.Close() + + sms := services.GetSMSClient() + ka := services.NewKeepAwakeService() + + for { + event := <-events + var err error + payload := &kafka_grpc.GRPC_AttendantPayload{} + err = proto.Unmarshal(event.Value, payload) + if err != nil { + logger.Warn().Err(err).Send() + } + device, key := common.ParseDeviceKey(string(event.Key)) + logger.Debug().Str("id", key).Msgf("source: %s, type: %s, handler: %s", key, device, payload.GetHandler()) + + switch device { + case common.TRex: + if payload.GetHandler() == "dtcs" { + d := &common.ConsumerPayload{ + Handler: payload.GetHandler(), + Data: controllers.GRPCToDTCEntry(payload), + } + routeService(db, ka, sms, key, d) + break + } + d, _ := common.AttendantRouteTRexPayload(payload) + routeTRex(db, ka, key, d) + case common.HMI: + d, _ := common.AttendantRouteHMIPayload(payload) + routeHMI(db, ka, key, d) + case common.Mobile: + d, _ := common.AttendantRouteMobilePayload(payload) + routeMobile(db, key, d) + case common.Service: + if payload.GetHandler() == "sms_delivery_status_manifest" { + d := &common.ConsumerPayload{ + Handler: payload.GetHandler(), + Data: tmobile.GRPCToTMessage(payload), + } + routeService(db, ka, sms, key, d) + break + } + d, _ := common.AttendantRouteServicePayload(payload) + routeService(db, ka, sms, key, d) + default: + { + logger.Error().Str("id", key).Msgf("Unknown device: %s, type: %s, handler: %s", device, key, payload.GetHandler()) + + loggerdataresp.BadDataError(ErrInvalidDevice) + } + } + loggerdataresp.BadDataError(err) + } +} + +func routeOldEvents(events chan common.EventRawJSON) { + db := services.GetDB() + defer db.Close() + + sms := services.GetSMSClient() + ka := services.NewKeepAwakeService() + + for { + event := <-events + var p common.Payload + err := p.Unmarshal(event.Payload) + device, key := common.ParseDeviceKey(event.Key) + payload := &common.ConsumerPayload{ + Handler: p.Handler, + Data: p.Data, + } + logger.Debug().Str("id", key).Msgf("source: %s, type: %s, handler: %s", key, device, payload.GetHandler()) + switch device { + case common.TRex: + routeTRex(db, ka, key, payload) + case common.HMI: + routeHMI(db, ka, key, payload) + case common.Mobile: + routeMobile(db, key, payload) + case common.Service: + routeService(db, ka, sms, key, payload) + default: + { + logger.Error().Str("id", key).Msgf("Unknown device: %s, type: %s, handler: %s", device, key, p.Handler) + + loggerdataresp.BadDataError(ErrInvalidDevice) + } + } + loggerdataresp.BadDataError(err) + } +} + +func routeTRex(db *services.DB, ka *services.KeepAwake, id string, p common.ConsumerPayloadInterface) { + // route TRex messages. Also update cloud/modules_go/kafka/topics.go + var err error + + switch p.GetHandler() { + case "car_state": + err = handlers.UpdateCarState(db, id, p.GetData()) + loggerdataresp.BadDataError(err) + case "car_update_status", "car_update_download", "car_update_install": + err = handlers.CarUpdateProgressStatus(db, ka, common.TRex, id, p.GetData()) + loggerdataresp.BadDataError(err) + case "get_filekeys": + err = handlers.GetFileKeys(db, common.TRex, id, p.GetData()) + case "dtcs": + err = handlers.UploadDtc(id, p.GetData()) + case "ecc_keys": + err = handlers.GetAllEccKeys(db, id, p.GetData()) + default: + err = kafka.ErrUnhandledMessage(common.TRex, id, p.GetHandler(), string(p.GetData())) + } + loggerdataresp.BadDataError(err) + +} + +func routeHMI(db *services.DB, ka *services.KeepAwake, id string, p common.ConsumerPayloadInterface) { + // route HMI messages. Also update cloud/modules_go/kafka/topics.go + var err error + + switch p.GetHandler() { + case "update_approve": + err = handlers.ApproveUpdate(db, id, p.GetData()) + case "car_update_status", "car_update_download", "car_update_install": + go func() { + err = handlers.CarUpdateProgressStatus(db, ka, common.HMI, id, p.GetData()) + loggerdataresp.BadDataError(err) + }() + err = handlers.CarUpdateProgressStatus(db, ka, common.HMI, id, p.GetData()) + loggerdataresp.BadDataError(err) + case "get_filekeys": + err = handlers.GetFileKeys(db, common.HMI, id, p.GetData()) + default: + err = kafka.ErrUnhandledMessage(common.HMI, id, p.GetHandler(), string(p.GetData())) + } + + loggerdataresp.BadDataError(err) +} + +func routeMobile(db *services.DB, id string, p common.ConsumerPayloadInterface) { + // route mobile messages. Also update cloud/modules_go/kafka/topics.go + var err error + + switch p.GetHandler() { + case "update_approve": + err = handlers.ApproveUpdate(db, id, p.GetData()) + case "updates_get": + err = handlers.GetUpdates(db, id, p.GetData()) + default: + err = kafka.ErrUnhandledMessage(common.Mobile, id, p.GetHandler(), string(p.GetData())) + } + loggerdataresp.BadDataError(err) +} + +func routeService(db *services.DB, ka *services.KeepAwake, sms sms.SMSServiceClient, id string, p common.ConsumerPayloadInterface) { + // route cloud service messages. Also update cloud/modules_go/kafka/topics.go + var err error + + // We should switch out these strings for constants in the kafka library + switch p.GetHandler() { + case "send_manifest": + err = handlers.SendManifest(db, ka, common.Service, id, p.GetData()) + case "order_updated": + err = handlers.OrderUpdated(db, sms, id, p.GetData()) + case "sms_delivery_status_manifest": + err = handlers.CarSendManifestSMSWakeUpCallback(db, ka, common.Service, id, p.GetData()) + default: + err = kafka.ErrUnhandledMessage(common.Service, id, p.GetHandler(), string(p.GetData())) + } + + loggerdataresp.BadDataError(err) +} diff --git a/services/attendant/services/config.go b/services/attendant/services/config.go new file mode 100644 index 0000000..b161b2e --- /dev/null +++ b/services/attendant/services/config.go @@ -0,0 +1,29 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/logger" + + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +var ( + configOnce sync.Once + configInstance vconfig.ConfigServiceInterface +) + +func GetVehicleConfig() vconfig.ConfigServiceInterface { + configOnce.Do(func() { + if configInstance != nil { + return + } + logger.Info().Msg("init vehicle config instance") + configInstance = vconfig.NewConfigService() + }) + return configInstance +} + +func SetVehicleConfig(c vconfig.ConfigServiceInterface) { + configInstance = c +} diff --git a/services/attendant/services/db.go b/services/attendant/services/db.go new file mode 100644 index 0000000..bd78ae7 --- /dev/null +++ b/services/attendant/services/db.go @@ -0,0 +1,278 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db" + q "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var ( + dbOnce sync.Once + dbInstance *DB +) + +type DB struct { + client *db.DBClient + cars q.CarsInterface + carVersionsLog q.CarVersionsLogInterface + carupdates q.CarUpdatesInterface + filekeys q.FileKeysInterface + manifests q.UpdateManifestsInterface + ecu q.ECUInterface + eccKeys q.EccKeysInterface + ratePlan q.RatePlanInterface + updateManifestSUMSVersions q.SUMSVersionsInterface + carConfigData q.CarConfigDataInterface + + onceEcu sync.Once + onceClient sync.Once + onceCars sync.Once + onceCarVersionsLog sync.Once + onceCarUpdates sync.Once + onceFileKeys sync.Once + onceManifests sync.Once + onceEccKeys sync.Once + onceRatePlan sync.Once + onceUpdateManifestSUMSVersions sync.Once + onceCarConfigData sync.Once + +} + +func GetDB() *DB { + dbOnce.Do(func() { + if dbInstance != nil { + return + } + logger.Info().Msg("init DB instance") + dbInstance = &DB{} + }) + return dbInstance +} + +func SetDB(db *DB) { + if dbInstance != nil { + dbInstance.Close() + } + dbInstance = db +} + +func (d *DB) GetDBClient() *db.DBClient { + d.onceClient.Do(func() { + if d.client != nil { + return + } + logger.Info().Msg("init DBClient instance") + client := &db.DBClient{} + client.RegisterManyToManyRel([]interface{}{ + (*common.CarToDriver)(nil), + }) + err := client.InitSchema([]interface{}{ + (*common.UpdateManifest)(nil), + (*common.Car)(nil), + (*common.CarToDriver)(nil), + (*common.CarUpdateStatus)(nil), + (*common.CarUpdate)(nil), + (*common.FileKey)(nil), + (*common.RatePlanTMobile)(nil), + }) + if err != nil { + logger.Error().Err(err).Send() + } + // Uncomment below to show generated SQL queries + // client.GetConn().AddQueryHook(db.SQLLogger{}) + d.client = client + }) + return d.client +} + +func (d *DB) SetDBClient(client *db.DBClient) { + if d.client != nil { + d.client.Close() + } + d.client = client +} + +func (d *DB) Close() { + if d.client == nil { + return + } + d.client.Close() +} + +func (d *DB) GetCarUpdates() q.CarUpdatesInterface { + d.onceCarUpdates.Do(func() { + if d.carupdates != nil { + return + } + instance := &q.CarUpdates{} + instance.SetClient(d.GetDBClient()) + d.carupdates = instance + }) + return d.carupdates +} + +func (d *DB) SetCarUpdates(carupdates q.CarUpdatesInterface) { + d.carupdates = carupdates +} + +func (d *DB) GetCars() q.CarsInterface { + d.onceCars.Do(func() { + if d.cars != nil { + return + } + instance := &q.Cars{} + instance.SetClient(d.GetDBClient()) + d.cars = instance + }) + return d.cars +} + +func (d *DB) SetCars(cars q.CarsInterface) { + d.cars = cars +} + +func (d *DB) GetCarVersionsLog() q.CarVersionsLogInterface { + d.onceCarVersionsLog.Do(func() { + if d.carVersionsLog != nil { + return + } + instance := &q.CarVersionsLog{} + instance.SetClient(d.GetDBClient()) + d.carVersionsLog = instance + }) + return d.carVersionsLog +} + +func (d *DB) SetCarVersionsLog(log q.CarVersionsLogInterface) { + d.carVersionsLog = log +} + +func (d *DB) GetFileKeys() q.FileKeysInterface { + d.onceFileKeys.Do(func() { + if d.filekeys != nil { + return + } + instance := &q.FileKeys{} + instance.SetClient(d.GetDBClient()) + d.filekeys = instance + }) + return d.filekeys +} + +func (d *DB) SetFileKeys(filekeys q.FileKeysInterface) { + d.filekeys = filekeys +} + +func (d *DB) GetUpdateManifests() q.UpdateManifestsInterface { + d.onceManifests.Do(func() { + if d.manifests != nil { + return + } + instance := q.NewUpdateManifest(nil) + instance.SetClient(d.GetDBClient()) + d.manifests = instance + }) + return d.manifests +} +func (d *DB) GetECU() q.ECUInterface { + + d.onceEcu.Do(func() { + if d.ecu != nil { + return + } + instance := &q.ECU{} + instance.SetClient(d.GetDBClient()) + d.ecu = instance + }) + return d.ecu + +} + +func (d *DB) SetECU(instance q.ECUInterface) { + d.ecu = instance +} + +func (d *DB) SetManifests(manifests q.UpdateManifestsInterface) { + d.manifests = manifests +} + +func (d *DB) ModifyUpdateStatus(id int, status string) (*common.CarUpdate, error) { + cu := d.GetCarUpdates() + updates, err := cu.SelectByID(int64(id)) + if err != nil { + return updates, err + } + + updates.Status = status + _, err = cu.UpdateStatus(updates) + + return updates, err +} + +func (d *DB) GetECCKeys() q.EccKeysInterface { + d.onceEccKeys.Do(func() { + if d.eccKeys != nil { + return + } + eccKeys := &q.EccKeys{} + eccKeys.SetClient(d.GetDBClient()) + d.eccKeys = eccKeys + }) + return d.eccKeys +} + +func (d *DB) SetECCKeys(eccKeys q.EccKeysInterface) { + d.eccKeys = eccKeys +} + +func (d *DB) GetRatePlan() q.RatePlanInterface { + d.onceRatePlan.Do(func() { + if d.ratePlan != nil { + return + } + instance := &q.RatePlanTmobile{} + instance.SetClient(d.GetDBClient()) + d.ratePlan = instance + }) + return d.ratePlan +} + +func (d *DB) SetRatePlan(ratePlan q.RatePlanInterface) { + d.ratePlan = ratePlan +} + +func (d *DB) GetUpdateManifestSUMSVersions() q.SUMSVersionsInterface { + d.onceUpdateManifestSUMSVersions.Do(func() { + if d.updateManifestSUMSVersions != nil { + return + } + instance := &q.SUMSVersions{} + instance.SetClient(d.GetDBClient()) + d.updateManifestSUMSVersions = instance + }) + return d.updateManifestSUMSVersions +} + +func (d *DB) SetUpdateManifestVersions(umv q.SUMSVersionsInterface) { + d.updateManifestSUMSVersions = umv +} + +func (d *DB) GetCarConfigData() q.CarConfigDataInterface { + d.onceCarConfigData.Do(func() { + if d.carConfigData != nil { + return + } + logger.Debug().Msg("Init CarConfigData instance") + carConfigData := &q.CarConfigData{} + carConfigData.SetClient(d.GetDBClient()) + d.carConfigData = carConfigData + }) + return d.carConfigData +} + +func (d *DB) SetCarConfigData(carConfigData q.CarConfigDataInterface) { + d.carConfigData = carConfigData +} diff --git a/services/attendant/services/dbc.go b/services/attendant/services/dbc.go new file mode 100644 index 0000000..09a96cd --- /dev/null +++ b/services/attendant/services/dbc.go @@ -0,0 +1,20 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/dbc" + "github.com/fiskerinc/cloud-services/pkg/dbc/models" + "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor" +) + +var model models.DBCVersionInterface +var collectionOnce sync.Once + +// GetDBCCollection returns singleton instance of collection of DBCs +func GetDBC() *descriptor.Database { + collectionOnce.Do(func() { + model = dbc.NewFM29_FRSD390_DBC() + }) + return model.GetDatabase() +} diff --git a/services/attendant/services/dtc_cache.go b/services/attendant/services/dtc_cache.go new file mode 100644 index 0000000..5d366f7 --- /dev/null +++ b/services/attendant/services/dtc_cache.go @@ -0,0 +1,24 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + MAX_KEY_CACHE int = envtool.GetEnvInt("MAX_KEY_CACHE", 10000) + carDtcsCache cache.CarDTCsCacheInterface + onceDTCCache sync.Once +) + +func GetCarDtcCache() cache.CarDTCsCacheInterface { + onceDTCCache.Do(func() { + if carDtcsCache == nil { + carDtcsCache = cache.NewCarDTCsCache(MAX_KEY_CACHE) + } + }) + + return carDtcsCache +} diff --git a/services/attendant/services/ecu_cache.go b/services/attendant/services/ecu_cache.go new file mode 100644 index 0000000..9c63f1c --- /dev/null +++ b/services/attendant/services/ecu_cache.go @@ -0,0 +1,28 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + MAX_ECU_KEY_CACHE int = envtool.GetEnvInt("MAX_ECU_KEY_CACHE", 10000) + ring cache.RingMapInterface + onceECUCache sync.Once +) + +func GetCarEcuCache() cache.RingMapInterface { + onceECUCache.Do(func() { + if ring == nil { + ring = cache.NewRingMap(MAX_ECU_KEY_CACHE) + } + }) + + return ring +} + +func SetCarEcuCache(value cache.RingMapInterface) { + ring = value +} diff --git a/services/attendant/services/foa.go b/services/attendant/services/foa.go new file mode 100644 index 0000000..7871a75 --- /dev/null +++ b/services/attendant/services/foa.go @@ -0,0 +1,88 @@ +package services + +import ( + "net/http" + "net/url" + "slices" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/common" + s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/foa" + "github.com/fiskerinc/cloud-services/pkg/httpclient" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA = []int64{816, 817, 818, 819, 820} + +var ( + foaService FoaServiceInterface + foaOnce sync.Once +) + +func GetFoaService() FoaServiceInterface { + foaOnce.Do(func() { + if foaService != nil { + return + } + foaService = NewFoaService() + }) + + return foaService +} + +func SetFoaService(foa FoaServiceInterface) { + foaService = foa +} + +func NewFoaService() FoaServiceInterface { + return &FoaService{ + foaURL: envtool.GetEnv("FOA_URL", "REPLACE_ME"), + foaAPIToken: envtool.GetEnv("FOA_API_KEY", "REPLACE_ME"), + } +} + +type FoaServiceInterface interface { + OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) +} + +type FoaService struct { + foaURL string + foaAPIToken string +} + +func (f *FoaService) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) { + if !slices.Contains(UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA, carUpdate.UpdateManifestID) { + // Nothing to send if the manifest is not one of the specified IDs + return nil, nil + } + + var body interface{} = nil + + switch status.Status { + case s.ManifestSucceeded: + body = foa.BuildOtaUpdateStatusSuccessRequest(vin, carUpdate.UpdateManifestID) + case s.ManifestError: + body = foa.BuildOtaUpdateStatusFailedRequest(vin, carUpdate.UpdateManifestID, status.Info) + case s.ManifestCanceled: + body = foa.BuildOtaUpdateStatusCanceledRequest(vin, carUpdate.UpdateManifestID, status.Info) + } + + if body == nil { + return nil, nil + } + + logger.Info().Msgf("Notifying FOA for %s of update %d status %s", vin, carUpdate.UpdateManifestID, status.Status) + + urlString, err := url.JoinPath(f.foaURL, "ota/update_status") + if err != nil { + return nil, err + } + + postHeader := http.Header{} + postHeader.Add("Authorization", "Bearer "+f.foaAPIToken) + postHeader.Add("Content-Type", "application/json") + + return httpclient.Post(urlString, body, postHeader) +} diff --git a/services/attendant/services/kafka.go b/services/attendant/services/kafka.go new file mode 100644 index 0000000..7a2b22f --- /dev/null +++ b/services/attendant/services/kafka.go @@ -0,0 +1,62 @@ +package services + +import ( + "context" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +const serviceName = "attendant" +const oldServiceName = "old-attendant" + +var consumer, oldConsumer kafka.ConsumerInterface +var consumerOnce sync.Once + +// GetKafkaConsumer returns singleton instance of kafka consumer +func GetKafkaConsumer() (kafka.ConsumerInterface, kafka.ConsumerInterface, error) { + var err error + + consumerOnce.Do(func() { + consumer, err = kafka.NewConsumer(serviceName) + if err != nil { + logger.Error().Err(err).Send() + } + oldConsumer, err = kafka.NewConsumer(oldServiceName) + if err != nil { + logger.Error().Err(err).Send() + } + }) + if err != nil { + return nil, nil, err + } + + return consumer, oldConsumer, nil +} + +var producer kafka.ProducerInterface +var producerOnce sync.Once + +func GetKafkaProducer() (kafka.ProducerInterface, error) { + var err error + producerOnce.Do(func() { + if producer == nil { + var producerTemp kafka.ProducerInterface + producerTemp, err = kafka.NewProducer(context.Background()) + if err != nil { + logger.Err(err).Send() + } + producer = producerTemp + } + }) + + if err != nil { + return nil, err + } + return producer, err +} + +func SetKafkaProducer(k kafka.ProducerInterface) { + producer = k +} diff --git a/services/attendant/services/keep_awake.go b/services/attendant/services/keep_awake.go new file mode 100644 index 0000000..4ab5642 --- /dev/null +++ b/services/attendant/services/keep_awake.go @@ -0,0 +1,215 @@ +package services + +import ( + "fmt" + "reflect" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/pkg/errors" +) + +// This can probably be moved into a better spot, but we are on a time limit +var ADD_TIME = time.Minute + +type KeepAwake struct { + tick *time.Ticker + KAI KeepAwakeInterface +} + +type KeepAwakeInterface interface { + CheckKeepAwakeMessages() + // addToKeepAwakeMessages(vin string) (err error) + RemoveKeepAwakeMessage(vin string) (err error) + // getKeepAwakeMessages() (vins []string, err error) + SendFirstKeepAwakeMessage(vin string) (err error) + // sendKeepAwakeMessage(vin string) (err error) +} + +type KeepAwakeImplementation struct { + Cars map[string]time.Time // Map of car vins to the time it was last ran + oneTime sync.Once + mapLock sync.Mutex +} + +func NewKeepAwakeService() (ka *KeepAwake) { + ka = &KeepAwake{} + + ka.tick = time.NewTicker(time.Minute) + ka.KAI = &KeepAwakeImplementation{} + + // Start our timer + go func() { + for { + select { + case <-ka.tick.C: + ka.KAI.CheckKeepAwakeMessages() + } + } + }() + return +} + +func (ka *KeepAwake) SetService(inf KeepAwakeInterface) { + ka.KAI = inf +} + +func (ka *KeepAwake) RemoveKeepAwakeMessage(vin string) (err error) { + return ka.KAI.RemoveKeepAwakeMessage(vin) +} + +func (ka *KeepAwake) SendFirstKeepAwakeMessage(vin string) (err error) { + return ka.KAI.SendFirstKeepAwakeMessage(vin) +} + +// Need to move this out of here, as this manifest sender is created every time a manifest is to be sent +// On a timeout, check if the keep awake messages need to be sent +func (k *KeepAwakeImplementation) CheckKeepAwakeMessages() { + k.oneTime.Do(func() { + k.Cars = make(map[string]time.Time) + }) + + k.mapLock.Lock() + vins, _ := k.getKeepAwakeMessages() + + for _, vin := range vins { + k.sendKeepAwakeMessage(vin) + + k.addToKeepAwakeMessages(vin) + } + k.mapLock.Unlock() + // Call this function again in some amount of time +} + +func (k *KeepAwakeImplementation) addToKeepAwakeMessages(vin string) (err error) { + k.Cars[vin] = time.Now().Add(ADD_TIME) + return +} + +func (k *KeepAwakeImplementation) RemoveKeepAwakeMessage(vin string) (err error) { + k.oneTime.Do(func() { + k.Cars = make(map[string]time.Time) + }) + + // Remove from Redis + err = k.removeRedisCarEntry(vin) + if err != nil { + logger.Err(err).Msg("Unable to remove keeep awake message from redis") + } + + logger.Info().Msgf("Ended keep awake messaging for %s", vin) + k.mapLock.Lock() + defer k.mapLock.Unlock() + _, ok := k.Cars[vin] + if !ok { + logger.Debug().Msgf("Attempted to end keep awake for %s, but not in list", vin) + } + delete(k.Cars, vin) + + return err +} + +// This is a list of vins that need sendKeepAwakeMessage +func (k *KeepAwakeImplementation) getKeepAwakeMessages() (vins []string, err error) { + for vin, t := range k.Cars { + // Parse out the time and check if its before the time it is now + if t.Before(time.Now()) { + // This needs processing + vins = append(vins, vin) + } + } + return +} + +func (k *KeepAwakeImplementation) SendFirstKeepAwakeMessage(vin string) (err error) { + k.oneTime.Do(func() { + k.Cars = make(map[string]time.Time) + }) + + // Additionally add to the list of redis + err = k.addRedisCarEntry(vin) + logger.Err(err).Msg("") + + logger.Info().Msgf("Started keep awake messaging for %s", vin) + k.mapLock.Lock() + defer k.mapLock.Unlock() + err = k.sendKeepAwakeMessage(vin) + k.addToKeepAwakeMessages(vin) + return err +} + +// Send a special message to keep the tbox awake to download the update +// We want to continue to call this until the download is complete or if we fail +func (k *KeepAwakeImplementation) sendKeepAwakeMessage(vin string) (err error) { + client := RedisClientPool().GetFromPool() + defer client.Close() + + // check if in redis + found, err := k.checkRedisForCarEntry(vin, client) + if err != nil { + return + } + // If we did not find the redis entry, then another service has removed it from the keep alive set + // Don't call k.Delete as it creates a lock on the map, and then we will be deadlocked + if !found { + delete(k.Cars, vin) + return + } + + logger.Info().Msgf("sending keep awake message for %s", vin) + + type Action struct { + Action string `json:"action"` + Timeout int32 `json:"timeout"` + } + logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", vin, "can_network", Action{Action: "on"}) + err = client.SafeQueueMessage(common.TRex.Key(vin), common.Message{ + Handler: "can_network", + Data: Action{Action: "on", Timeout: 120}, + }) + + return errors.WithStack(err) +} + +func (k *KeepAwakeImplementation) checkRedisForCarEntry(vin string, client redis.Client) (found bool, err error) { + line, err := client.Execute("SISMEMBER", "can_keep_awake", vin) + if err != nil { + return false, errors.WithStack(err) + } + + check, ok := line.(int64) + if !ok { + err = fmt.Errorf("received wrong type from redis for the can_keep_awake for %s, %v, %s", vin, line, reflect.TypeOf(line)) + } + return check > 0, err +} + +func (k *KeepAwakeImplementation) removeRedisCarEntry(vin string) (err error) { + client := RedisClientPool().GetFromPool() + defer client.Close() + _, err = client.Execute("SREM", "can_keep_awake", vin) + return +} + +func (k *KeepAwakeImplementation) addRedisCarEntry(vin string) (err error) { + client := RedisClientPool().GetFromPool() + defer client.Close() + + _, err = client.Execute("SADD", "can_keep_awake", vin) + return +} + +type MockKeepAwakeImplementation struct{} + +func (k *MockKeepAwakeImplementation) CheckKeepAwakeMessages() {} + +func (k *MockKeepAwakeImplementation) RemoveKeepAwakeMessage(vin string) (err error) { + return err +} + +func (k *MockKeepAwakeImplementation) SendFirstKeepAwakeMessage(vin string) (err error) { + return +} diff --git a/services/attendant/services/keep_awake_test.go b/services/attendant/services/keep_awake_test.go new file mode 100644 index 0000000..0bff652 --- /dev/null +++ b/services/attendant/services/keep_awake_test.go @@ -0,0 +1,68 @@ +package services + +import ( + "testing" + "time" +) + +func TestKeepAwake(t *testing.T){ + t.Skip() + ADD_TIME = 0 + ka := KeepAwake{} + impl := &KeepAwakeImplementation{} + ka.SetService(impl) + + testVin := "JH4KA7680RC01" + // Check that trying to delete a non-existing set is okay + err := impl.RemoveKeepAwakeMessage(testVin) + if err != nil{ + t.Error(err) + } + client := RedisClientPool().GetFromPool() + found, err := impl.checkRedisForCarEntry(testVin, client) + if err != nil { + t.Error(err) + } + if found{ + t.Error("did find expected vin when not supposed to") + } + client.Close() + + // Check we can successfully add + err = impl.SendFirstKeepAwakeMessage(testVin) + if err != nil{ + t.Error(err) + } + + client = RedisClientPool().GetFromPool() + found, err = impl.checkRedisForCarEntry(testVin, client) + if err != nil { + t.Error(err) + } + if !found{ + t.Error("did not find expected vin") + } + client.Close() + + time.Sleep(time.Second * 2) + vins, err := impl.getKeepAwakeMessages() + if err != nil{ + t.Error(err) + } + if len(vins) != 1{ + t.Error("len of vins wrong ", len(vins)) + } + + err = impl.RemoveKeepAwakeMessage(testVin) + if err != nil{ + t.Error(err) + } + + vins, err = impl.getKeepAwakeMessages() + if err != nil{ + t.Error(err) + } + if len(vins) != 0{ + t.Error("len of vins wrong ", len(vins)) + } +} \ No newline at end of file diff --git a/services/attendant/services/mongo.go b/services/attendant/services/mongo.go new file mode 100644 index 0000000..f3e612a --- /dev/null +++ b/services/attendant/services/mongo.go @@ -0,0 +1,32 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/mongo" +) + +var ( + clientOnce sync.Once + client mongo.Client +) + +// GetMongoClient returns singleton instance of mongo client +func GetMongoClient() (mongo.Client, error) { + var err error + clientOnce.Do(func() { + client, err = initMongoClient() + }) + + return client, err +} + +func initMongoClient() (mongo.Client, error) { + var err error + + if client == nil { + client, err = mongo.NewClient(mongo.StandardDB) + } + + return client, err +} diff --git a/services/attendant/services/redis.go b/services/attendant/services/redis.go new file mode 100644 index 0000000..d9c8a44 --- /dev/null +++ b/services/attendant/services/redis.go @@ -0,0 +1,28 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +var ( + clientPoolOnce sync.Once + clientPool redis.ClientPoolInterface +) + +func RedisClientPool() redis.ClientPoolInterface { + clientPoolOnce.Do(func() { + if clientPool != nil { + return + } + + clientPool = redis.NewClientPool() + }) + + return clientPool +} + +func SetRedisClientPool(cp redis.ClientPoolInterface) { + clientPool = cp +} diff --git a/services/attendant/services/sap.go b/services/attendant/services/sap.go new file mode 100644 index 0000000..4eed48d --- /dev/null +++ b/services/attendant/services/sap.go @@ -0,0 +1,28 @@ +package services + +import ( + "sync" + + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +var ( + sapService vconfig.SAPServiceInterface + sapOnce sync.Once +) + +func GetSapService() vconfig.SAPServiceInterface { + sapOnce.Do(func() { + if sapService != nil { + return + } + sapService = vconfig.NewSAPService() + }) + + return sapService +} + +// SetSapService is supposed t be used for testing. +func SetSapService(sap vconfig.SAPServiceInterface) { + sapService = sap +} diff --git a/services/attendant/services/sms.go b/services/attendant/services/sms.go new file mode 100644 index 0000000..9dff521 --- /dev/null +++ b/services/attendant/services/sms.go @@ -0,0 +1,43 @@ +package services + +import ( + "fmt" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var smsClient sms.SMSServiceClient +var smsClientOnce sync.Once + +func newSmsClient() { + logger.Info().Msg("Init SMS client") + target := fmt.Sprintf("%s:%s", + envtool.GetEnv("SMS_HOST", "sms"), + envtool.GetEnv("SMS_PORT", "8077")) + c, err := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + logger.Error().Err(err).Send() + } + + smsClient = sms.NewSMSServiceClient(c) +} + +func GetSMSClient() sms.SMSServiceClient { + smsClientOnce.Do(func() { + if smsClient != nil { + return + } + newSmsClient() + }) + + return smsClient +} + +func SetSmsClient(c sms.SMSServiceClient) { + smsClient = c +} diff --git a/services/depot/Dockerfile b/services/depot/Dockerfile new file mode 100644 index 0000000..5df2ac5 --- /dev/null +++ b/services/depot/Dockerfile @@ -0,0 +1,25 @@ +ARG BASE_IMAGE=cloud_base_go +FROM ${BASE_IMAGE} as builder-go + +WORKDIR /build/depot +COPY ./depot/go.mod ./depot/go.sum ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go mod download + +COPY ./depot ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && 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/depot/depot . + +ENV LOG_CONFIG=log_config +EXPOSE 8077 + +CMD ./depot diff --git a/services/depot/controllers/health_check.go b/services/depot/controllers/health_check.go new file mode 100644 index 0000000..921a93b --- /dev/null +++ b/services/depot/controllers/health_check.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/health" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var mismatchTypeError = errors.New("mismatch type error") + +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, + }, + { + Name: "kafka", + Check: health.NewKafkaMultiCheck(getKafkaConsumer), + Timeout: time.Second * 1, + Vital: true, + }, + }) + if err != nil { + logger.Error().Err(err).Send() + } +} + +func getMongoClient() (health.MongoConnCheckInterface, error) { + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + conn, ok := client.(health.MongoConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + + return conn, nil +} + +func getKafkaConsumer() ([]health.KafkaConnCheckInterface, error) { + var connections []health.KafkaConnCheckInterface + client, oldClient, err := services.GetKafkaConsumer() + if err != nil { + return connections, err + } + conn, ok := client.(health.KafkaConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + connections = append(connections, conn) + + oldConn, ok := oldClient.(health.KafkaConnCheckInterface) + if !ok { + return connections, errors.WithStack(mismatchTypeError) + } + connections = append(connections, oldConn) + + return connections, nil +} diff --git a/services/depot/go.mod b/services/depot/go.mod new file mode 100644 index 0000000..c2b8e25 --- /dev/null +++ b/services/depot/go.mod @@ -0,0 +1,109 @@ +module github.com/fiskerinc/cloud-services/services/depot + +go 1.25 + +toolchain go1.25.0 + +require ( + github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000 + github.com/fiskerinc/cloud-services/pkg/can-go v0.0.0-00010101000000-000000000000 + github.com/go-pg/pg/v10 v10.11.1 + github.com/gomodule/redigo v1.8.9 + github.com/pkg/errors v0.9.1 + github.com/sony/gobreaker v0.5.0 + go.mongodb.org/mongo-driver v1.14.0 + google.golang.org/grpc v1.67.3 + google.golang.org/protobuf v1.36.1 +) + +require ( + 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/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/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/confluentinc/confluent-kafka-go/v2 v2.3.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-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/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/google/uuid v1.6.0 // indirect + github.com/gorilla/schema v1.2.0 // 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/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // 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/philhofer/fwd v1.1.2 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // 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/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect + github.com/stretchr/objx v0.5.2 // 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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // 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/DataDog/dd-trace-go.v1 v1.60.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 +) diff --git a/services/depot/go.sum b/services/depot/go.sum new file mode 100644 index 0000000..e63f2a5 --- /dev/null +++ b/services/depot/go.sum @@ -0,0 +1,481 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/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-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= +github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.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/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.5/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +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/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/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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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.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/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/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +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-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/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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/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/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/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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/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/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.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.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-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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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/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-20190313153728-d0100b6bd8b3/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-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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-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-20190620200207-3b0461eec859/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-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-20211015210444-4f30a5c0130f/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/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-20190423024810-112230192c58/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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-20191120155948-bd437916bb0e/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-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-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.3.0/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.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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/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.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= +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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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.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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= diff --git a/services/depot/handlers/common.go b/services/depot/handlers/common.go new file mode 100644 index 0000000..2ac3187 --- /dev/null +++ b/services/depot/handlers/common.go @@ -0,0 +1,118 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + fv "github.com/fiskerinc/cloud-services/pkg/flashpackversion" + "github.com/fiskerinc/cloud-services/pkg/logger" + + "github.com/pkg/errors" +) + +// get vehicle settings +func getSettings(db *services.DB, vin string) ([]common.CarSetting, error) { + settings, err := db.GetCars().GetVehicleSpecificSettings(&common.CarToDriver{ + VIN: vin, + }) + if err != nil { + return nil, errors.WithStack(err) + } + + osVersionSetting, err := getOSVersion(vin) + if err == nil && osVersionSetting != nil { + settings = append(settings, *osVersionSetting) + } else { + logger.Error().Msgf("failed to get os version %v", err) + } + + flashpackNumber, err := getFlashpackNumber(vin) + if err != nil { + return nil, errors.WithStack(err) + } + if flashpackNumber != nil { + settings = append(settings, *flashpackNumber) + } else { + logger.Warn().Msgf("no flashpack number yet for vin %s because not enough ECUs have been updated", vin) + } + + certNeedsRenewal, err := services.GetCertService().CheckCertificateNeedsRenewal(vin, common.CertICC) + if err != nil { + logger.Error().Msgf("failed to check for certificate renewal %v", err) + } else if certNeedsRenewal { + logger.Debug().Msg("ICC cert for vin " + vin + " is out of date") + + settings = append(settings, common.CarSetting{ + Name: "certificates", + Value: "expired", + Type: "string", + }) + } + + clearSettings(settings) + return settings, nil +} + +// Get the OS version given a vin +func getOSVersion(vin string) (*common.CarSetting, error) { + sumsVersion, err := getSUMS(vin) + if err != nil { + return nil, err + } + + sumsDB := services.GetDB().GetUpdateManifestSUMSVersions() + sums, err := sumsDB.Select(sumsVersion) + if err != nil { + logger.Error().Msgf("can not load SUMS, %v", err) + return nil, err + } + + cs := common.CarSetting{ + Name: "os_version", + Value: sums.OSVersion, + Type: "string", + } + cs.CreatedAt = sums.CreatedAt + cs.UpdatedAt = sums.UpdatedAt + + return &cs, nil +} + +// Get the SUMS version from update manifest for a given vin +func getSUMS(vin string) (sumsVersion string, err error) { + carsDB := services.GetDB().GetCars() + car, err := carsDB.SelectByVIN(vin) + if err != nil { + logger.Err(err).Msgf("failed in getSUMS(vin string) for car %s to fetch car", vin) + return + } + + return car.SUMSVersion, err +} + +// Get the flash pack number +func getFlashpackNumber(vin string) (*common.CarSetting, error) { + cars := services.GetDB().GetCars() + + car, err := cars.SelectByVIN(vin) + if err != nil { + return nil, err + } + + flashpackNumber, err := fv.FindCurrentFlashpackVersionForCar(cars, *car) + if err != nil { + return nil, err + } + + if flashpackNumber != "" { + cs := &common.CarSetting{ + Name: "flashpack_number", + Value: flashpackNumber, + Type: "string", + } + + return cs, nil + } + + return nil, nil +} diff --git a/services/depot/handlers/hmi_del.go b/services/depot/handlers/hmi_del.go new file mode 100644 index 0000000..61666c3 --- /dev/null +++ b/services/depot/handlers/hmi_del.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +func HMIDel(db *services.DB, id string) error { + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + logger.Info().Msgf("Remove HMI session in Redis for %s", id) + + err := removeHMISession(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + err = removeHMISessionID(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + return nil +} + +func removeHMISession(client redis.Client, id string) error { + _, err := client.Execute("SREM", redis.HMISessionsKey(), id) + return err +} + +func removeHMISessionID(client redis.Client, id string) error { + return client.Delete(redis.HMISessionKey(id)) +} diff --git a/services/depot/handlers/hmi_del_test.go b/services/depot/handlers/hmi_del_test.go new file mode 100644 index 0000000..6f054e5 --- /dev/null +++ b/services/depot/handlers/hmi_del_test.go @@ -0,0 +1,20 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHMIDel(t *testing.T) { + setupRedisMock() + setupDBMock() + + id := "FISKER123" + err := handlers.HMIDel(mockDB, id) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestHMIDel", nil, err) + } +} diff --git a/services/depot/handlers/hmi_init.go b/services/depot/handlers/hmi_init.go new file mode 100644 index 0000000..28ffff4 --- /dev/null +++ b/services/depot/handlers/hmi_init.go @@ -0,0 +1,99 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/redis" + + "github.com/pkg/errors" +) + +func HMIInit(db *services.DB, id string, data []byte) error { + fmt.Printf("HMIInit()") + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + var sessionPayload common.HMISessionData + err := json.Unmarshal(data, &sessionPayload) + if err != nil { + return errors.WithStack(err) + } + + logger.Info().Msgf("Initialize HMI session in Redis for %s with ID %s", id, sessionPayload.SessionID) + err = addHMISession(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + err = addHMISessionID(client, id, sessionPayload.SessionID) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + if sessionPayload.Salt != "" { + err = addHMISaltID(client, id, sessionPayload.Salt) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + } + + err = sendSettingsHMI(client, db, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + return err + } + + return nil +} + +func addHMISession(client redis.Client, id string) error { + _, err := client.Execute("SADD", redis.HMISessionsKey(), id) + return err +} + +func addHMISessionID(client redis.Client, id string, sessionID string) error { + _, err := client.Execute("SET", redis.HMISessionKey(id), sessionID) + return err +} + +func addHMISaltID(client redis.Client, id string, salt string) error { + _, err := client.Execute("SET", redis.HMISaltKey(id), salt) + return err +} + +func sendSettingsHMI(client redis.Client, db *services.DB, vin string) error { + + settings, err := getSettings(db, vin) + if err != nil { + return err + } + + if len(settings) == 0 { + logger.Info().Msgf("no settings to send for car %s", vin) + return nil + } + + return client.SafePublishMessage(common.HMI.Key(vin), + common.Message{ + Handler: "car_settings", + Data: settings, + }, + ) +} + +func clearSettings(settings []common.CarSetting) { + cTime := time.Now() + for i, s := range settings { + s.CreatedAt = nil + if s.UpdatedAt == nil { + s.UpdatedAt = &cTime + } + settings[i] = s + } +} diff --git a/services/depot/handlers/hmi_init_test.go b/services/depot/handlers/hmi_init_test.go new file mode 100644 index 0000000..799a0e0 --- /dev/null +++ b/services/depot/handlers/hmi_init_test.go @@ -0,0 +1,30 @@ +package handlers_test + +import ( + "encoding/json" + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHMIInit(t *testing.T) { + setupRedisMock() + setupDBMock() + + id := "FISKER123" + sessionData := common.HMISessionData{ + SessionID: "XXXXX", + } + data, err := json.Marshal(sessionData) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestHMIInit", nil, err) + } + + err = handlers.HMIInit(mockDB, id, data) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestHMIInit", nil, err) + } +} diff --git a/services/depot/handlers/mobile_del.go b/services/depot/handlers/mobile_del.go new file mode 100644 index 0000000..7046c62 --- /dev/null +++ b/services/depot/handlers/mobile_del.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +func MobileDel(db *services.DB, id string) error { + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + err := removeMobileSession(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + return nil +} + +func removeMobileSession(client redis.Client, id string) error { + _, err := client.Execute("SREM", redis.MobileSessionsKey(), id) + return err +} diff --git a/services/depot/handlers/mobile_del_test.go b/services/depot/handlers/mobile_del_test.go new file mode 100644 index 0000000..615c3be --- /dev/null +++ b/services/depot/handlers/mobile_del_test.go @@ -0,0 +1,20 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestMobileDel(t *testing.T) { + setupRedisMock() + setupDBMock() + + id := "VALID-COGNITO-ID-1" + err := handlers.MobileDel(mockDB, id) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestMobileDel", nil, err) + } +} diff --git a/services/depot/handlers/mobile_init.go b/services/depot/handlers/mobile_init.go new file mode 100644 index 0000000..542d81f --- /dev/null +++ b/services/depot/handlers/mobile_init.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +func MobileInit(db *services.DB, id string) error { + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + err := addMobileSession(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + carDrivers, err := db.GetCars().GetCarsForDriver(id) + if err != nil { + logger.Error().Msgf("can not load cars for driver %v", err) + return err + } + + for _, carDriver := range carDrivers { + sendSettingsMobile(client, db, carDriver.VIN, id) + // If we fail to send the digital twin, it is not a major problem + sendFullDigitalTwinToMobile(carDriver.VIN, id) + } + + return nil +} + +func addMobileSession(client redis.Client, id string) error { + _, err := client.Execute("SADD", redis.MobileSessionsKey(), id) + return err +} + +func sendSettingsMobile(client redis.Client, db *services.DB, vin, driverID string) error { + settings, err := getSettings(db, vin) + if err != nil { + return err + } + + return client.SafePublishMessage(common.Mobile.Key(driverID), + common.Message{ + Handler: "car_settings", + Data: settings, + }, + ) +} + +func sendFullDigitalTwinToMobile(vin string, driverID string) (err error) { + err = services.GetSendDigitalTwin().SendToDriver(vin, driverID) + if err != nil { + logger.Err(err).Str("VIN", vin).Str("Driver ID", driverID).Msg("Failed to Send Full Digital Twin to Mobile Connection on INIT") + } + return +} diff --git a/services/depot/handlers/mobile_init_test.go b/services/depot/handlers/mobile_init_test.go new file mode 100644 index 0000000..3320ae5 --- /dev/null +++ b/services/depot/handlers/mobile_init_test.go @@ -0,0 +1,20 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestMobileInit(t *testing.T) { + setupRedisMock() + setupDBMock() + + id := "VALID-COGNITO-ID-1" + err := handlers.MobileInit(mockDB, id) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestMobileInit", nil, err) + } +} diff --git a/services/depot/handlers/mock_test.go b/services/depot/handlers/mock_test.go new file mode 100644 index 0000000..4dae86f --- /dev/null +++ b/services/depot/handlers/mock_test.go @@ -0,0 +1,51 @@ +package handlers_test + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/go-pg/pg/v10/orm" + + "github.com/fiskerinc/cloud-services/pkg/common" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" +) + +var mockRedis *redis.Connection +var mockDB *services.DB + +func setupRedisMock() { + mockRedis := tester.NewRedisMock() + services.SetRedisClientPool(tester.NewMockClientPool(mockRedis)) +} + +func setupDBMock() { + db := services.DB{} + db.SetCars(&mocks.MockCars{ + SelectResponse: &common.Car{VIN: "FISKER123", ICCID: "1111111111111111111F"}, + SelectCarSettings: []common.CarSetting{}, + SelectCarECUs: []common.CarECU{ + { + VIN: "FISKER123", + ECU: "ADAS", + SupplierSWVersion: "ADASVersion", + }, + { + VIN: "FISKER123", + ECU: "ACUN", + SupplierSWVersion: "ACUNVersion", + }, + { + VIN: "FISKER123", + ECU: "BCM", + SupplierSWVersion: "BCMVersion", + }, + }, + }) + db.SetCarVersionsLog(&mocks.MockCarVersionsLog{MockLogVersionChange: func(log *common.CarVersionLogs) (orm.Result, error) { + return nil, nil + }}) + mockDB = &db + services.SetDB(&db) +} diff --git a/services/depot/handlers/trex_del.go b/services/depot/handlers/trex_del.go new file mode 100644 index 0000000..3241119 --- /dev/null +++ b/services/depot/handlers/trex_del.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +func TRexDel(id string) error { + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + err := removeTRexSession(client, id) + if err != nil { + return err + } + + return nil +} + +func removeTRexSession(client redis.Client, id string) error { + _, err := client.Execute("SREM", redis.CarSessionsKey(), id) + return err +} diff --git a/services/depot/handlers/trex_del_test.go b/services/depot/handlers/trex_del_test.go new file mode 100644 index 0000000..c766c79 --- /dev/null +++ b/services/depot/handlers/trex_del_test.go @@ -0,0 +1,20 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestTrexDel(t *testing.T) { + setupRedisMock() + setupDBMock() + + id := "FISKER123" + err := handlers.TRexDel(id) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestTrexDel", nil, err) + } +} diff --git a/services/depot/handlers/trex_init.go b/services/depot/handlers/trex_init.go new file mode 100644 index 0000000..de22090 --- /dev/null +++ b/services/depot/handlers/trex_init.go @@ -0,0 +1,336 @@ +package handlers + +import ( + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/fiskerinc/cloud-services/services/depot/services" + + "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/db/queries" + "github.com/fiskerinc/cloud-services/pkg/dbc/state" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/userconsent" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" + "github.com/go-pg/pg/v10" + redigo "github.com/gomodule/redigo/redis" + "github.com/pkg/errors" + mon "go.mongodb.org/mongo-driver/mongo" +) + +var errInvalidICCID = errors.New("invalid iccid submitted") +var errBlocked = errors.New("unable to connect: car is blocked") +var logLevel = envtool.GetEnv("TREX_LOG_LEVEL", common.CriticalLabel) +var wakeUpVINS = envtool.GetEnv("WAKE_UP_VINS", "VCF1EBU2XPG001140") +var defaultFleet = envtool.GetEnv("DEFAULT_FLEET", "Default-Ocean") + +// id is the vehicles VIN +func TRexInit(id string, vMap map[string]string) error { + carsDB := services.GetDB().GetCars() + blocked, err := insertIfNotExist(id, carsDB) + if err != nil { + return err + } + if blocked { + return errBlocked + } + + if strings.Contains(wakeUpVINS, id) { + go wakeup(id) + } + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + err = addTRexSession(client, id) + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } + + m, err := services.GetMongoClient() + if err != nil { + logger.Warn().Str("id", id).Err(err).Send() + } else { + msg, err := retrieveTRexSettings(client, m, id) + if err != nil { + return err + } + + // Log the config that will be sent to TREX + config, err := json.Marshal(msg) + if err != nil { + return err + } + logger.Info().Msgf("TREX Config sent for vin %s: \n%s", id, config) + + err = sendTRexSettings(client, id, msg) + if err != nil { + return err + } + } + + err = setTRexVersion(client, id, vMap["version"]) + if err != nil { + return err + } + + err = setDBCVersion(client, id, vMap["dbc_version"]) + if err != nil { + return err + } + + err = setTRexIP(client, id, vMap["ip"]) + if err != nil { + return err + } + + err = addICCID(carsDB, id, vMap["iccid"]) + if err != nil { + logger.Warn().Err(err).Str("VIN", id).Str("ICCID", vMap["iccid"]).Msg("failed to add iccid to car") + } + + uMsg, err := getUserConsentData(id) + if err != nil { + return err + } + + err = sendTRexUserConsentData(client, id, uMsg) + if err != nil { + return err + } + + err = checkAndRenewCertificate(id) + if err != nil { + return err + } + + return nil +} + +func checkAndRenewCertificate(id string) error { + logger.Debug().Msg("on trex init: checking TBOX cert for vin " + id) + + cs := services.GetCertService() + certNeedsRenewal, err := cs.CheckCertificateNeedsRenewal(id, common.CertTBOX) + if err != nil { + return err + } else if certNeedsRenewal { + logger.Debug().Msg("TBOX cert for vin " + id + " is out of date") + + cert, err := cs.RenewCertificate(id, common.CertTBOX) + if err != nil { + return err + } + + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + err = client.SafeQueueMessage( + common.TRex.Key(id), + common.Message{ + Handler: "update_cert", + Data: common.UpdateCert{ + SSLCertBase64: cert.PublicKey, + }, + }, + ) + if err != nil { + return err + } + } + + logger.Debug().Msg("TBOX cert for vin " + id + " is not out of date") + + return nil +} + +func getUserConsentData(id string) ([]common.UserConsentDataTrexMsg, error) { + var ucdToSend = []common.UserConsentDataTrexMsg{} + + resp, err := userconsent.GetUserConsentService().UserConsentByVehicleNumber(id) + if err != nil { + return nil, errors.WithStack(err) + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("call to user-consent/byVehicleNumber endpoint returned " + resp.Status) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.WithStack(err) + } + + ucdResult := []common.UserConsentDataResponse{} + err = json.Unmarshal(respBody, &ucdResult) + if err != nil { + return nil, errors.WithStack(err) + } + if len(ucdResult) < 1 { + return ucdToSend, nil + } + + // only send to TREX the consents that have the userID of the first consent returned + var userId = ucdResult[0].UserID + for _, ucd := range ucdResult { + if ucd.UserID == userId { + ucdToSend = append(ucdToSend, common.BuildUserConsentTrexMessage(ucd)) + } + } + + return ucdToSend, nil +} + +func sendTRexUserConsentData(r redis.Client, id string, msg []common.UserConsentDataTrexMsg) error { + + if len(msg) > 0 { + return r.SafePublishMessage( + common.TRex.Key(id), + common.Message{ + Handler: "consent", + Data: msg, + }, + ) + } + return nil +} + +func insertIfNotExist(id string, carsDB queries.CarsInterface) (bool, error) { + car, err := carsDB.SelectByVIN(id) + if err == nil && car != nil { + return car.Blocked, nil + } + if errors.Is(err, pg.ErrNoRows) { + err = nil + + c, err := utils.ParseVIN(id) + if err != nil { + return false, errors.WithStack(err) + } + + _, err = carsDB.Insert(c) + if err != nil { + return false, errors.WithStack(err) + } + + client, err := services.GetMongoClient() + if err != nil { + return false, errors.WithStack(err) + } + + v := &mongo.Vehicle{ + VIN: id, + CANBus: common.CANBus{Enabled: true, DataLogger: true, DTCEnabled: elptr.ElPtr(false)}, + LogLevel: common.UnmarshalLogLevelString(logLevel), + } + err = client.GetVehicles().AddVehicle(v) + if mon.IsDuplicateKeyError(err) { + err = client.GetVehicles().UpdateVehicle(v) + } + if err != nil { + return false, errors.WithStack(err) + } + + err = client.GetFleets().AddVehiclesToFleet(defaultFleet, []string{id}) + if err != nil { + return false, errors.WithStack(err) + } + + return false, nil + } + + return false, errors.WithStack(err) +} + +func addICCID(db queries.CarsInterface, id, iccid string) error { + // Going to only update where an iccid is valid + if len(iccid) != 20 { + return errInvalidICCID + } + + car := &common.Car{ICCID: iccid, VIN: id} + + if ct, err := db.UpdateICCID(car); err != nil { + return errors.Wrapf(err, "failed to update car's iccid %s", id) + } else if ct.RowsAffected() == 1 { + logger.Info().Msgf("car %s has been updated with iccid %s", id, iccid) + } + + return nil +} + +func addTRexSession(client redis.Client, id string) error { + _, err := client.Execute("SADD", redis.CarSessionsKey(), id) + return err +} + +func retrieveTRexSettings(r redis.Client, m mongo.Client, id string) (*common.TRexConfigResponse, error) { + return cache.RetrieveVehicleConfig(r, m, id) +} + +func sendTRexSettings(r redis.Client, id string, msg *common.TRexConfigResponse) error { + err := r.SafePublishMessage( + common.TRex.Key(id), + common.Message{ + Handler: "config", + Data: msg, + }, + ) + return err +} + +func setTRexIP(r redis.Client, vin, ip string) error { + elms := strings.Split(ip, ":") + + return r.SetObjectField(redis.CarStateHashKey(vin), state.TREX_IP, elms[0]) +} + +func setDBCVersion(r redis.Client, vin, version string) error { + return setVersion(r, vin, version, state.DBC_VERSION, common.DBCVersionSource) +} + +func setTRexVersion(r redis.Client, vin, version string) error { + return setVersion(r, vin, version, state.TREX_VERSION, common.TREXVersionSource) +} + +func setVersion(r redis.Client, vin, version, redisField string, versionSource common.VersionSource) error { + v, err := r.GetObjectField(redis.CarStateHashKey(vin), redisField) + if err != nil && !errors.Is(err, redigo.ErrNil) { + return errors.WithStack(err) + } + + if v == version { + return nil + } + + db := services.GetDB().GetCarVersionsLog() + err = r.SetObjectField(redis.CarStateHashKey(vin), redisField, version) + if err != nil { + return errors.WithStack(err) + } + + _, err = db.LogVersionChange(&common.CarVersionLogs{ + VIN: vin, + VersionSource: versionSource, + Version: version, + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +func wakeup(vin string) { + wake := carcommand.NewCarWakeUp(services.GetDB().GetCars(), services.GetSMSClient()) + err := wake.WakeUp(vin, false) + loggerdataresp.BadDataError(err) +} diff --git a/services/depot/handlers/trex_init_test.go b/services/depot/handlers/trex_init_test.go new file mode 100644 index 0000000..57a466c --- /dev/null +++ b/services/depot/handlers/trex_init_test.go @@ -0,0 +1,78 @@ +package handlers_test + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/userconsent" +) + +func TestTrexInit(t *testing.T) { + rMock := tester.MockRedis{} + rMock.Reset() + + mockUserConsent := userconsent.UserConsentServiceMock{} + userconsent.SetUserConsentService(&mockUserConsent) + + mockCert := CertServiceMock{} + services.SetCertService(&mockCert) + + services.SetRedisClientPool(tester.NewMockClientPool(&rMock)) + id := "FISKER123" + + setupDBMock() + services.SetMongoClient(mongo.NewMockClient()) + + mockCertificates := mocks.MockCertificates{} + mockCertificates.MockCertificate = &common.Certificate{ + PublicKey: "test", + SerialNumber: "", + Type: common.CertTBOX, + } + services.GetDB().SetCertificates(&mockCertificates) + + mocksms := sms.NewSMSMockSuccess() + services.SetSmsClient(&mocksms) + + err := handlers.TRexInit( + id, + map[string]string{"version": "1.2.3", "iccid": "123456789123456789123456789", "ip": "172.20.0.17:49850"}) + if err != nil { + t.Errorf(testhelper.TestErrorTemplate, "TestTRexInit", nil, err) + } + + rez := rMock.SetValues[redis.CarStateHashKey(id)] + if rez.Value.(string) != `{"ip":"172.20.0.17","trex_version":"1.2.3"}` { + t.Errorf(testhelper.TestErrorTemplate, "TestTRexInit_wrongVersion", `{"ip":"172.20.0.17","trex_version":"1.2.3"}`, rez.Value) + } + + rez = rMock.SetValues[redis.CarConfigKey(id)] + if rez.Value.(string) != `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace","log":{"matches":[{"channel":"cmd","level":"trace"}]}}` { + t.Errorf(testhelper.TestErrorTemplate, "TestTRexInit_wrongVersion", `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace"}`, rez.Value) + } +} + +type CertServiceMock struct{} + +func (csm *CertServiceMock) CheckCertificateNeedsRenewal(vin string, certType string) (bool, error) { + return certType == common.CertTBOX, nil +} + +func (csm *CertServiceMock) RenewCertificate(vin string, certType string) (*common.Certificate, error) { + cert := common.Certificate{ + PublicKey: "test", + SerialNumber: "", + Type: common.CertTBOX, + } + + return &cert, nil +} diff --git a/services/depot/main.go b/services/depot/main.go new file mode 100644 index 0000000..2382c4f --- /dev/null +++ b/services/depot/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/fiskerinc/cloud-services/services/depot/controllers" + "github.com/fiskerinc/cloud-services/services/depot/server" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/tracer" + "github.com/fiskerinc/cloud-services/pkg/utils/app" +) + +func init() { + app.Setup("depot", cleanup) +} + +func main() { + defer cleanup() + + tracer.Start() + defer tracer.Stop() + + go controllers.HealthCheck() + go server.StartConsumer(kafka.DepotServiceGRPCKafka, kafka.DepotService) + + select {} +} + +func cleanup() { + logger.Close() +} diff --git a/services/depot/server/errors.go b/services/depot/server/errors.go new file mode 100644 index 0000000..275ad00 --- /dev/null +++ b/services/depot/server/errors.go @@ -0,0 +1,7 @@ +package server + +import ( + "github.com/pkg/errors" +) + +var ErrInvalidDevice = errors.New("invalid device associated to message") diff --git a/services/depot/server/server_consumer.go b/services/depot/server/server_consumer.go new file mode 100644 index 0000000..5f8d786 --- /dev/null +++ b/services/depot/server/server_consumer.go @@ -0,0 +1,158 @@ +package server + +import ( + "encoding/json" + + "github.com/fiskerinc/cloud-services/services/depot/handlers" + "github.com/fiskerinc/cloud-services/services/depot/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "google.golang.org/protobuf/proto" +) + +// StartConsumer runs consumer and puts events into a channel for router +func StartConsumer(topic, oldTopic string) { + defer func() { + if err := recover(); err != nil { + logger.Error().Msgf("PanicConsumer %v", err) + } + }() + + eventsJSON := make(chan common.EventRawJSON) + events := make(chan *kafka.Message) + go routeEvents(events) + go routeOldEvents(eventsJSON) + + logger.Info().Msgf("consumer initialized for topic: %v", topic) + consumer, oldConsumer, err := services.GetKafkaConsumer() + if err != nil { + panic(err) + } + go func() { + err = oldConsumer.ConsumeToChannelJson([]string{oldTopic}, eventsJSON) + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) + }() + err = consumer.ConsumeToChannel([]string{topic}, events) + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) +} + +func routeOldEvents(events chan common.EventRawJSON) { + p := services.GetDB() + defer p.Close() + + for { + event := <-events + var err error + var dt common.Payload + err = dt.Unmarshal(event.Payload) + device, k := common.ParseDeviceKey(event.Key) + payload := &common.ConsumerPayload{ + Handler: dt.Handler, + Data: dt.Data, + } + logger.Debug().Str("id", k).Msgf("source: %s, type: %s, handler: %s", k, device, payload.GetHandler()) + + switch device { + case common.TRex: + err = routeTRex(k, payload) + case common.HMI: + err = routeHMI(p, k, payload) + case common.Mobile: + err = routeMobile(p, k, payload) + default: + err = ErrInvalidDevice + } + + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) + } +} + +func routeEvents(events chan *kafka.Message) { + p := services.GetDB() + defer p.Close() + + for { + event := <-events + + var err error + payload := &kafka_grpc.GRPC_DepotPayload{} + err = proto.Unmarshal(event.Value, payload) + + device, k := common.ParseDeviceKey(string(event.Key)) + logger.Debug().Str("id", k).Msgf("source: %s, type: %s, handler: %s", k, device, payload.GetHandler()) + + switch device { + case common.TRex: + d, _ := common.DepotRouteTRexPayload(payload) + err = routeTRex(k, d) + case common.HMI: + d, _ := common.DepotRouteHMIPayload(payload) + err = routeHMI(p, k, d) + case common.Mobile: + d, _ := common.DepotRouteMobilePayload(payload) + err = routeMobile(p, k, d) + default: + err = ErrInvalidDevice + } + + loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) + } +} + +func routeTRex(id string, d common.ConsumerPayloadInterface) error { + // route TRex messages + var err error + + switch d.GetHandler() { + case "init": + var vMap map[string]string + err = json.Unmarshal(d.GetData(), &vMap) + if err != nil { + return err + } + err = handlers.TRexInit(id, vMap) + case "del": + err = handlers.TRexDel(id) + default: + + err = kafka.ErrUnhandledMessage(common.TRex, id, d.GetHandler(), string(d.GetData())) + } + + return err +} + +func routeHMI(p *services.DB, id string, d common.ConsumerPayloadInterface) error { + // route HMI messages + var err error + + switch d.GetHandler() { + case "init": + err = handlers.HMIInit(p, id, d.GetData()) + case "del": + err = handlers.HMIDel(p, id) + default: + err = kafka.ErrUnhandledMessage(common.HMI, id, d.GetHandler(), string(d.GetData())) + } + + return err +} + +func routeMobile(p *services.DB, id string, d common.ConsumerPayloadInterface) error { + // route mobile messages + var err error + + switch d.GetHandler() { + case "init": + err = handlers.MobileInit(p, id) + case "del": + err = handlers.MobileDel(p, id) + default: + err = kafka.ErrUnhandledMessage(common.Mobile, id, d.GetHandler(), string(d.GetData())) + } + + return err +} diff --git a/services/depot/services/cert.go b/services/depot/services/cert.go new file mode 100644 index 0000000..5ad8aef --- /dev/null +++ b/services/depot/services/cert.go @@ -0,0 +1,113 @@ +package services + +import ( + "bytes" + "encoding/json" + "net/http" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/go-pg/pg/v10" + "github.com/pkg/errors" +) + +var ( + certService CertServiceInterface + certOnce sync.Once +) + +func GetCertService() CertServiceInterface { + certOnce.Do(func() { + if certService != nil { + return + } + certService = NewCertService() + }) + + return certService +} + +func SetCertService(cs CertServiceInterface) { + certService = cs +} + +func NewCertService() CertServiceInterface { + return &CertService{ + certURL: envtool.GetEnv("CERT_URL", "REPLACE_ME"), + certAPIToken: envtool.GetEnv("CERTIFICATE_API_KEY", "REPLACE_ME"), + } +} + +type CertServiceInterface interface { + CheckCertificateNeedsRenewal(vin string, certType string) (bool, error) + RenewCertificate(vin string, certType string) (*common.Certificate, error) +} + +type CertService struct { + certURL string + certAPIToken string +} + +func (cs *CertService) CheckCertificateNeedsRenewal(vin string, certType string) (bool, error) { + logger.Debug().Msg("checking " + certType + " cert for vin " + vin) + + cert, err := GetDB().GetCertificates().SelectMostRecent(vin, certType) + if err != nil && !errors.Is(err, pg.ErrNoRows) { + return false, err + } + if cert == nil { + logger.Debug().Msg("no existing " + certType + " cert to renew for vin " + vin) + return false, nil + } + + var daysBeforeExp = 183 // six months + switch certType { + case common.CertTBOX: + daysBeforeExp = envtool.GetEnvInt("TBOX_CERT_RENEW_DAYS_BEFORE_EXP", daysBeforeExp) + case common.CertICC: + daysBeforeExp = envtool.GetEnvInt("ICC_CERT_RENEW_DAYS_BEFORE_EXP", daysBeforeExp) + } + + logger.Debug().Msg("checking validity of " + certType + " cert with serial number " + cert.SerialNumber + " for vin " + vin) + + return cert.IsExpiredOrInvalidAtTime(time.Now(), daysBeforeExp) +} + +func (cs *CertService) RenewCertificate(vin string, certType string) (*common.Certificate, error) { + logger.Debug().Msg("renewing " + certType + " cert for vin " + vin) + + jsonBytes, err := json.Marshal(common.CertificateRenewRequest{ + Type: certType, + CommonName: vin, + }) + if err != nil { + return nil, err + } + + request, err := http.NewRequest(http.MethodPost, cs.certURL+"renew", bytes.NewReader(jsonBytes)) + if err != nil { + return nil, err + } + request.Header.Add("Api-Key", cs.certAPIToken) + + resp, err := http.DefaultClient.Do(request) + if err != nil { + return nil, errors.WithStack(err) + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("renew " + certType + " certificate returned " + resp.Status + " for vin " + vin) + } + + defer resp.Body.Close() + + var cert common.Certificate + err = json.NewDecoder(resp.Body).Decode(&cert) + if err != nil { + return nil, err + } + + return &cert, err +} diff --git a/services/depot/services/db.go b/services/depot/services/db.go new file mode 100644 index 0000000..53b996c --- /dev/null +++ b/services/depot/services/db.go @@ -0,0 +1,170 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db" + q "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var ( + dbOnce sync.Once + dbInstance *DB +) + +type DB struct { + client *db.DBClient + cars q.CarsInterface + carVersionsLog q.CarVersionsLogInterface + certificates q.CertificatesInterface + updateManifest q.UpdateManifestsInterface + updateManifestSUMSVersions q.SUMSVersionsInterface + + onceClient sync.Once + onceCars sync.Once + onceCarVersionsLog sync.Once + onceCertificates sync.Once + onceUpdateManifest sync.Once + onceUpdateManifestSUMSVersions sync.Once +} + +func GetDB() *DB { + dbOnce.Do(func() { + if dbInstance != nil { + return + } + logger.Info().Msg("init DB instance") + dbInstance = &DB{} + }) + return dbInstance +} + +func SetDB(db *DB) { + if dbInstance != nil { + dbInstance.Close() + } + dbInstance = db +} + +func (d *DB) GetDBClient() *db.DBClient { + d.onceClient.Do(func() { + if d.client != nil { + return + } + logger.Info().Msg("init DBClient instance") + client := &db.DBClient{} + + err := client.InitSchema([]interface{}{ + (*common.UpdateManifest)(nil), + (*common.CarUpdate)(nil), + (*common.CarToDriver)(nil), + (*common.CarSetting)(nil), + (*common.SUMSVersion)(nil), + }) + if err != nil { + logger.Error().Err(err).Send() + } + d.client = client + }) + return d.client +} + +func (d *DB) SetDBClient(client *db.DBClient) { + if d.client != nil { + d.client.Close() + } + d.client = client +} + +func (d *DB) Close() { + if d.client == nil { + return + } + d.client.Close() +} + +///---------- + +func (d *DB) GetCars() q.CarsInterface { + d.onceCars.Do(func() { + if d.cars != nil { + return + } + instance := &q.Cars{} + instance.SetClient(d.GetDBClient()) + d.cars = instance + }) + return d.cars +} + +func (d *DB) SetCars(cars q.CarsInterface) { + d.cars = cars +} + +func (d *DB) GetCarVersionsLog() q.CarVersionsLogInterface { + d.onceCarVersionsLog.Do(func() { + if d.carVersionsLog != nil { + return + } + instance := &q.CarVersionsLog{} + instance.SetClient(d.GetDBClient()) + d.carVersionsLog = instance + }) + return d.carVersionsLog +} + +func (d *DB) SetCarVersionsLog(log q.CarVersionsLogInterface) { + d.carVersionsLog = log +} + +func (d *DB) GetCertificates() q.CertificatesInterface { + d.onceCertificates.Do(func() { + if d.certificates != nil { + return + } + logger.Debug().Msg("Init Certificates instance") + certificates := &q.Certificates{} + certificates.SetClient(d.GetDBClient()) + d.certificates = certificates + }) + return d.certificates +} + +func (d *DB) SetCertificates(certificates q.CertificatesInterface) { + d.certificates = certificates +} + +func (d *DB) GetUpdateManifests() q.UpdateManifestsInterface { + d.onceUpdateManifest.Do(func() { + if d.updateManifest != nil { + return + } + logger.Debug().Msg("Init UpdateManifest instance") + updateManifest := q.NewUpdateManifest(nil) + updateManifest.SetClient(d.GetDBClient()) + d.updateManifest = updateManifest + }) + return d.updateManifest +} + +func (d *DB) SetUpdateManifests(updateManifest q.UpdateManifestsInterface) { + d.updateManifest = updateManifest +} + +func (d *DB) GetUpdateManifestSUMSVersions() q.SUMSVersionsInterface { + d.onceUpdateManifestSUMSVersions.Do(func() { + if d.updateManifestSUMSVersions != nil { + return + } + instance := &q.SUMSVersions{} + instance.SetClient(d.GetDBClient()) + d.updateManifestSUMSVersions = instance + }) + return d.updateManifestSUMSVersions +} + +func (d *DB) SetUpdateManifestVersions(umv q.SUMSVersionsInterface) { + d.updateManifestSUMSVersions = umv +} diff --git a/services/depot/services/digital_twin.go b/services/depot/services/digital_twin.go new file mode 100644 index 0000000..707f53c --- /dev/null +++ b/services/depot/services/digital_twin.go @@ -0,0 +1,23 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/digitaltwin" +) + +// There is no need to recreate the digital twin sender every time we send a digital twin +// so keeping a reference here + +var sendDigitalTwin *digitaltwin.SendDigitalTwin +var sendDigitalTwinOnce sync.Once + +func GetSendDigitalTwin()(*digitaltwin.SendDigitalTwin){ + sendDigitalTwinOnce.Do(func(){ + if sendDigitalTwin == nil { + notSureWhyIcantTakeAddress := digitaltwin.NewSendDigitalTwin(RedisClientPool(), GetDB().GetCars()) + sendDigitalTwin = ¬SureWhyIcantTakeAddress + } + }) + return sendDigitalTwin +} \ No newline at end of file diff --git a/services/depot/services/kafka.go b/services/depot/services/kafka.go new file mode 100644 index 0000000..e907d80 --- /dev/null +++ b/services/depot/services/kafka.go @@ -0,0 +1,36 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +const serviceName = "depot" +const oldServiceName = "old-depot" + +var consumer kafka.ConsumerInterface +var oldConsumer kafka.ConsumerInterface +var consumerOnce sync.Once + +// GetKafkaConsumer returns singleton instance of kafka consumer +func GetKafkaConsumer() (kafka.ConsumerInterface, kafka.ConsumerInterface, error) { + var err error + + consumerOnce.Do(func() { + consumer, err = kafka.NewConsumer(serviceName) + if err != nil { + logger.Error().Err(err).Send() + } + oldConsumer, err = kafka.NewConsumer(oldServiceName) + if err != nil { + logger.Error().Err(err).Send() + } + }) + if err != nil { + return consumer, oldConsumer, err + } + + return consumer, oldConsumer, nil +} diff --git a/services/depot/services/mongo.go b/services/depot/services/mongo.go new file mode 100644 index 0000000..b9fed32 --- /dev/null +++ b/services/depot/services/mongo.go @@ -0,0 +1,72 @@ +package services + +import ( + "context" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/pkg/errors" + "github.com/sony/gobreaker" +) + +var ( + clientlock sync.Mutex + client mongo.Client + cb *gobreaker.CircuitBreaker +) + +func init() { + cb = gobreaker.NewCircuitBreaker( + gobreaker.Settings{ + Timeout: 10 * time.Second, + Interval: 15 * time.Minute, + }, + ) +} + +// GetMongoClient returns singleton instance of mongo client +func GetMongoClient() (mongo.Client, error) { + err := ping() + if err != nil { + return initMongoClient() + } + + return client, nil +} + +func initMongoClient() (mongo.Client, error) { + clientlock.Lock() + defer clientlock.Unlock() + var err error + + client, err = mongo.NewClient(mongo.StandardDB) + if err != nil { + return nil, err + } + + _, _ = cb.Execute(func() (interface{}, error) { + err = ping() + return nil, err + }) + + return client, errors.WithStack(err) +} + +func ping() error { + if client == nil { + return errors.New("client is nil") + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + return client.Ping(ctx) +} + +// SetMongoClient is supposed to be used for tests. +func SetMongoClient(cl mongo.Client) { + client = cl +} + + +// db.dtc_lookup.aggregate({"$match": {"ecuName": "TBOX", "DtcUniqueId": "V2.6.0"}}, {"$unwind": "$dtcData.DtcList"}, {"$match": {"dtcData.DtcList.TroubleCodeHex":"D77F16"}}, {"$project":{"_id": 0, "Obj": "$dtcData.DtcList"}}) \ No newline at end of file diff --git a/services/depot/services/redis.go b/services/depot/services/redis.go new file mode 100644 index 0000000..d9c8a44 --- /dev/null +++ b/services/depot/services/redis.go @@ -0,0 +1,28 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +var ( + clientPoolOnce sync.Once + clientPool redis.ClientPoolInterface +) + +func RedisClientPool() redis.ClientPoolInterface { + clientPoolOnce.Do(func() { + if clientPool != nil { + return + } + + clientPool = redis.NewClientPool() + }) + + return clientPool +} + +func SetRedisClientPool(cp redis.ClientPoolInterface) { + clientPool = cp +} diff --git a/services/depot/services/sms.go b/services/depot/services/sms.go new file mode 100644 index 0000000..0eb953c --- /dev/null +++ b/services/depot/services/sms.go @@ -0,0 +1,42 @@ +package services + +import ( + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "sync" +) + +var smsClient sms.SMSServiceClient +var smsClientOnce sync.Once + +func newSmsClient() { + logger.Info().Msg("Init SMS client") + target := fmt.Sprintf("%s:%s", + envtool.GetEnv("SMS_HOST", "sms"), + envtool.GetEnv("SMS_PORT", "8077")) + c, err := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + logger.Error().Err(err).Send() + } + + smsClient = sms.NewSMSServiceClient(c) +} + +func GetSMSClient() sms.SMSServiceClient { + smsClientOnce.Do(func() { + if smsClient != nil { + return + } + newSmsClient() + }) + + return smsClient +} + +func SetSmsClient(c sms.SMSServiceClient) { + smsClient = c +} diff --git a/services/jetfire/.env.template b/services/jetfire/.env.template new file mode 100644 index 0000000..7af7c92 --- /dev/null +++ b/services/jetfire/.env.template @@ -0,0 +1,25 @@ +CLICKHOUSE_USER="default" +CLICKHOUSE_PASS="" + +CLICKHOUSE_FEATURE_TABLE="feature_table" +CLICKHOUSE_VEHICLE_SIGNAL_TABLE="vehicle_signal" + +CLICKHOUSE_HOST="localhost" +KAFKA_HOSTS="localhost:9093" + +CLICKHOUSE_MAX_CONNS=5 + +JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS=10000 + +JETFIRE_FEATURE_BATCH_PERIOD_MS=10000 +JETFIRE_FEATURE_DOWNSAMPLE_US=1000000 + +JETFIRE_TRIP_TIMEOUT_MS=300000 +JETFIRE_STATE_TIMEOUT_MS=3600000 +JETFIRE_FUTURE_THRESHOLD_MS=604800000 + +JETFIRE_SCHEMA_RESET_PERIOD_MS=3600000 + +JETFIRE_MAX_BUFFER_ROWS=1000 + +LOG_LEVEL="debug" diff --git a/services/jetfire/Dockerfile b/services/jetfire/Dockerfile new file mode 100644 index 0000000..bd7a5be --- /dev/null +++ b/services/jetfire/Dockerfile @@ -0,0 +1,32 @@ + +## Build binaries for event detection using cloud_base_go image +ARG BASE_IMAGE=cloud_base_go +FROM ${BASE_IMAGE} as builder-go + +WORKDIR /build/jetfire + +COPY ./jetfire/go.mod ./jetfire/go.sum ./ + +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go mod download + +COPY ./jetfire ./ + +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go build -tags musl + + +## Build image for event detection, pulling binaries from builder image +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 ./jetfire/default-feature-vars.json . +COPY --from=builder-go /build/jetfire/jetfire . + +ENV LOG_LEVEL=log_config +EXPOSE 8077 + +CMD ./jetfire diff --git a/services/jetfire/README.md b/services/jetfire/README.md new file mode 100644 index 0000000..742213a --- /dev/null +++ b/services/jetfire/README.md @@ -0,0 +1,26 @@ +# Jetfire Data Routing Service +Jetfire Service listens to CAN Signals on the vehicle_signals topic on Kafka, +and performs a pivot operation for populating the feature_table on Clickhouse, +and populating the real_time table on Clickhouse. +CAN Signals are batched and inserted into vehicle_signal table without any transform performed on the data. + +Feature Table inserts are typically batched at `JETFIRE_FEATURE_BATCH_PERIOD_MS=10000`. + +Vehicle Signal Table inserts are typically batched at `JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS=10000` + +The schemas for sink tables are fetched periodically, set to `JETFIRE_SCHEMA_RESET_PERIOD_MS=3600000` +Additionally, a GET request to the `/reset` endpoint on port `8077` will also trigger an immediate schema reset. + +For more information about Jetfire service, see https://fiskerinc.atlassian.net/wiki/spaces/COM/pages/1401487522/Jetfire+Service + +## Usage + +Copy `./.env.tenmplate` to `./.env` file + +Secrets in the .env file will need to be filled in manually. + +Running jetfire locally +``` +source set_envs.sh +go run main.go +``` \ No newline at end of file diff --git a/services/jetfire/controllers/health_check.go b/services/jetfire/controllers/health_check.go new file mode 100644 index 0000000..f81994c --- /dev/null +++ b/services/jetfire/controllers/health_check.go @@ -0,0 +1,54 @@ +package controllers + +import ( + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "time" + + "github.com/fiskerinc/cloud-services/pkg/health" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" +) + +var mismatchTypeError = errors.New("mismatch type error") + +func HealthCheck() { + server := health.HealthCheckServer{} + err := server.Serve([]health.Config{ + { + Name: "clickhouse", + Check: health.NewClickhouseCheck(getClickhouseConsumer), + Timeout: time.Second * 1, + }, + }) + if err != nil { + logger.Error().Err(err).Send() + } +} + +func getKafkaConsumer() (health.KafkaConnCheckInterface, error) { + client, err := services.GetKafkaConsumer() + if err != nil { + return nil, err + } + + conn, ok := client.(health.KafkaConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + + return conn, nil +} + +func getClickhouseConsumer() (health.ClickhouseConnCheckInterface, error) { + client, err := services.GetClickhouseConnection() + if err != nil { + return nil, err + } + + conn, ok := client.(health.ClickhouseConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + + return conn, nil +} diff --git a/services/jetfire/default-feature-vars.json b/services/jetfire/default-feature-vars.json new file mode 100644 index 0000000..c7944e8 --- /dev/null +++ b/services/jetfire/default-feature-vars.json @@ -0,0 +1,65 @@ +[ + "BCM_DrFrntDoorSts", + "BCM_FrntDrDoorLockSts", + "BCM_TotMilg_ODO", + "BCM_PwrMod", + + "BMS_AccueChrgTotAh", + "BMS_AccueDchaTotAh", + "BMS_Bat_Actual_Pack_Capacity", + "BMS_Bat_HVmeasure_Current", + "BMS_Bat_SoC_usable", + "BMS_BattAvrgT", + "BMS_Bat_measure_Energy", + "BMS_PwrBattChrgDchaCrt1", + "BMS_PwrBattChrgDchaCrt2", + "BMS_PwrBattRmngCpSOC", + "BMS_PwrBattSOH", + "BMS_VehChrgDchgMod", + "BMS_Cell_Volt_max", + "BMS_Cell_Volt_min", + + "BMS_Bat_Coolant_in", + "BMS_Bat_Coolant_out", + + "ECC_OutdT", + + "ESP_TotBrkTqReq", + "ESP_VehSpd", + + "IBS_StateOfCharge", + "IBS_StateOfHealth", + "IBS_BatteryVoltage", + "IBS_BatteryCurrent", + "IBS_BatteryTemperature", + "IBS_AvgRi", + "IBS_AvailableCapacity", + + "ICC_DispVehSpd", + "ICC_FrntWiprCtrl", + + "MCU_F_AlrmLamp_FS", + "MCU_F_CrtTq", + "MCU_F_HVActvDchaSts", + "MCU_R_AlrmLamp_FS", + "MCU_R_CrtTq", + "MCU_R_HVActvDchaSts", + + "OBC_DCPosRlyCtrlSts", + + "VCU_ACChrgShttrSts", + "VCU_APSPerc", + "VCU_BrkPedlSts_GB", + "VCU_BrkSig", + "VCU_ChrgSts", + "VCU_ChrgSts_GB", + "VCU_ChrgSysOperCmd", + "VCU_DCChrgShttrSts", + "VCU_GearSig_GB", + "VCU_VehChrgDchgMod", + "VCU_VehOperMod", + + "TBOX_GPSHei", + "TBOX_GPSLati", + "TBOX_GPSLongi" +] \ No newline at end of file diff --git a/services/jetfire/go.mod b/services/jetfire/go.mod new file mode 100644 index 0000000..f1152be --- /dev/null +++ b/services/jetfire/go.mod @@ -0,0 +1,143 @@ +module github.com/fiskerinc/cloud-services/services/jetfire + +go 1.25 + +toolchain go1.25.0 + +require ( + github.com/ClickHouse/ch-go v0.58.2 + github.com/ClickHouse/clickhouse-go/v2 v2.6.0 + github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000 + 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/rs/zerolog v1.29.1 + github.com/sony/gobreaker v0.5.0 + github.com/stretchr/testify v1.10.0 + google.golang.org/protobuf v1.36.1 + gopkg.in/retry.v1 v1.0.3 +) + +require ( + 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/cespare/xxhash/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/dmarkham/enumer v1.5.8 // 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/frankban/quicktest v1.14.6 // 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-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // 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/pg/v10 v10.11.1 // 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/gomodule/redigo v1.8.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/inflection v1.0.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-isatty v0.0.20 // 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/pascaldekloe/name v1.0.1 // 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/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/swaggo/swag v1.8.8 // 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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.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/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 + gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 // 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 +) diff --git a/services/jetfire/go.sum b/services/jetfire/go.sum new file mode 100644 index 0000000..82bed75 --- /dev/null +++ b/services/jetfire/go.sum @@ -0,0 +1,548 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= +github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= +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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.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/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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-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-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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +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-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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/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/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/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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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.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.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/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/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-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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +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/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/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0= +github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= +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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= +github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/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/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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/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/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.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/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +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.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= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +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-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-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-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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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-20190313153728-d0100b6bd8b3/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-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +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-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/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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-20191120155948-bd437916bb0e/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-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-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.3.0/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.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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20200130002326-2f3ba24bd6e7/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.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= +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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= +gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= diff --git a/services/jetfire/handlers/batch.go b/services/jetfire/handlers/batch.go new file mode 100644 index 0000000..44b951d --- /dev/null +++ b/services/jetfire/handlers/batch.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/jetfire/models" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "time" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + //downsampling timers and delays + clearCacheTimer = time.Now().Truncate(time.Second) + featureDelay = envtool.GetEnvDuration("JETFIRE_FEATURE_DOWNSAMPLE_US", 1000000) * time.Microsecond + clearCacheDelay = envtool.GetEnvDuration("JETFIRE_STATE_TIMEOUT_MS", 3600000) * time.Millisecond + futureTimeThreshold = envtool.GetEnvDuration("JETFIRE_FUTURE_THRESHOLD_MS", 2*24*60*60*1000) * time.Millisecond +) + +// Handles the batch of Signals. returns batchFlag, error +// where batchFlag corresponds to FeatureUpdateFlag for which buffers were updated with this batch +func HandleSignalBatch(batchData []*kafka_grpc.GRPC_CANSignal, vehicleCache *models.VehicleCache, producerChannel chan models.InsertCommand) (uint, error) { + start := time.Now() + + batchFlag := uint(0) + + //Iterate through received can signals and add to cache + futureWarning := false //only one future warning per batch of data + + //sets of pointers to insert into insertion buffers + featureUpdates := make(map[*models.VehicleState]bool) + + logger.Debug().Msgf("Processing batch %d signals...", len(batchData)) + + skipLog := false + + for _, signal := range batchData { + signal.Timestamp = utils.FixFloatTimestampScale(signal.Timestamp) + if start.Add(futureTimeThreshold).Before(utils.FloatToTime(signal.Timestamp)) && !futureWarning { + // Throw out any signals that are from the future. + futureWarning = true + logger.Warn().Msgf("ignoring signal(s) from %s from future: %f (currently %f)", signal.Vin, signal.Timestamp, utils.TimeToFloat(start)) + continue + } + + err := vehicleCache.UpdateSignal(signal, utils.FeatureUpdateFlag) + if err != nil { + logger.Error().Err(err).Send() + //do not continue yet; attempt to add to vehicle signal buffer + } + + //Check vehicle state for update + // if signal is not in tracked vars, then skip. + // if VIN is not in vehicle cache, then log and skip + state, ok := vehicleCache.Cache[signal.Vin] + if !ok || state == nil { + if len(*vehicleCache.SignalsSet) > 0 { + _, ok = (*vehicleCache.SignalsSet)[signal.Name] + if ok && !skipLog { + // no vin state but signal update should have created a vin state... log warning + logger.Error().Msgf("unexpected missing VIN state from cache for VIN {%s} and signal {%s}", signal.Vin, signal.Name) + skipLog = true + } + } + continue + } + if state.TimeSincePolled(utils.FeatureUpdateFlag) > featureDelay { + featureUpdates[state] = true + } + } + + services.SchemaLock.Lock() + defer services.SchemaLock.Unlock() + + //TODO: investigate optimizing selecting VINs that need to be appended to buffer + // iterating through maps is slow! + + //batch rows for insertion for feature + for vinState := range featureUpdates { + err := services.GetFeatureBatch().AppendRow(services.GetFeatureVars(), vinState, producerChannel) + if err != nil { + logger.Error().Err(err).Send() + } else { + vinState.SetPollTime(utils.FeatureUpdateFlag) + batchFlag |= utils.FeatureUpdateFlag + } + + err = services.GetFeatureLastBatch().AppendRow(services.GetFeatureVars(), vinState, producerChannel) + if err != nil { + logger.Error().Err(err).Send() + } + } + + return batchFlag, nil +} diff --git a/services/jetfire/handlers/reset.go b/services/jetfire/handlers/reset.go new file mode 100644 index 0000000..a0e1abe --- /dev/null +++ b/services/jetfire/handlers/reset.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/fiskerinc/cloud-services/services/jetfire/services" + + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +func ResetSchemaDefinitions(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + logger.Info().Msg("resetting tracked variables and schemas") + services.ResetCacheVars() + endTime := time.Now() + logger.Debug().Msgf("reset tracked variables and schemas, time taken: %fms; %d variables", + float64(endTime.UnixMilli()-startTime.UnixMilli()), + len(services.GetFeatureVars()), + ) +} diff --git a/services/jetfire/main.go b/services/jetfire/main.go new file mode 100644 index 0000000..d72be66 --- /dev/null +++ b/services/jetfire/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + + "github.com/fiskerinc/cloud-services/services/jetfire/controllers" + "github.com/fiskerinc/cloud-services/services/jetfire/server" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/tracer" + "github.com/fiskerinc/cloud-services/pkg/utils/app" +) + +var ( + SERVICE_NAME = "jetfire" +) + +func init() { + app.Setup(SERVICE_NAME, cleanup) +} + +func main() { + + defer cleanup() + + tracer.Start() + defer tracer.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go controllers.HealthCheck() + + services.ResetCacheVars() + go server.StartHTTPServer() //listen for reset requests + go server.StartConsumer(ctx, kafka.VehicleSignal) + + select {} +} + +func cleanup() { + logger.Close() +} diff --git a/services/jetfire/models/clickhouse.go b/services/jetfire/models/clickhouse.go new file mode 100644 index 0000000..9ba9fa8 --- /dev/null +++ b/services/jetfire/models/clickhouse.go @@ -0,0 +1,933 @@ +package models + +import ( + "context" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "math" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/ClickHouse/ch-go" + "github.com/ClickHouse/ch-go/proto" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/sony/gobreaker" +) + +type InsertBlockType int + +const ( + NilBlockType InsertBlockType = -1 //insert buffers using this block type will have a nil block. + + PivotBlockType InsertBlockType = iota + SignalBlockType + VinLastBlockType +) + +var ( + initialBlockPoolSize = envtool.GetEnvInt("JETFIRE_MIN_BLOCKS", 2) + maxBlockPoolSize = envtool.GetEnvInt("JETFIRE_MAX_BLOCKS", 4) + maxBufferBytes = envtool.GetEnvInt("JETFIRE_BUFFER_MAX_BYTES", 128<<20) / maxBlockPoolSize +) + +const NO_TIMESTAMP_LOG_SAMPLE_RATE = 10000 +var logSampler zerolog.Logger +// Creating a log sampler +func init(){ + logSampler = logger.Sample(zerolog.RandomSampler(NO_TIMESTAMP_LOG_SAMPLE_RATE)) +} + +// InsertBuffer abstracts buffer appends and insertions to clickhouse, even for different underlying schemas. +// A pool of blocks are allocated for each InsertBuffer. +// +// These pools do not implement leaky buffer pattern to ensure data order and timeliness. +type InsertBuffer struct { + InsertTime time.Time + insertDelay time.Duration + + blockPool []insertBlock //pool of all blocks, including busy blocks. + + //linked list of available blocks in the pool + freeLock sync.Mutex //mutex for linked list + freeHead *BlockNode + freeTail *BlockNode + + blockType InsertBlockType + tripInfo bool + + TableName string +} + +// linked list node for insertBlock +type BlockNode struct { + block insertBlock + next *BlockNode +} + +// Command struct for inserter goroutine +type InsertCommand struct { + Buffer *InsertBuffer + Block insertBlock +} + +func NewInsertBuffer(tripInfo bool, signalNames []string, tableName string, insertDelay time.Duration, blockType InsertBlockType) *InsertBuffer { + newBlockPool := []insertBlock{} + + for i := 0; i < initialBlockPoolSize; i++ { + var newBlock insertBlock = AllocateNewBlock(signalNames, maxBufferBytes, blockType, tripInfo) + + if newBlock != nil { + newBlockPool = append(newBlockPool, newBlock) + } + } + + newBuffer := new(InsertBuffer) + newBuffer.insertDelay = insertDelay + newBuffer.blockPool = newBlockPool + newBuffer.TableName = tableName + newBuffer.blockType = blockType + newBuffer.tripInfo = tripInfo + + newBuffer.InitFreeBlocksList() + + return newBuffer +} + +// memory block linked list functions. These are for available blocks that are ready to accept data. +func (buffer *InsertBuffer) AppendFreeBlock(block insertBlock) { + buffer.freeLock.Lock() + defer buffer.freeLock.Unlock() + + if buffer.freeHead == nil { + buffer.freeHead = &BlockNode{block: block} + + buffer.freeTail = buffer.freeHead + return + } + if buffer.freeTail == nil { //this should never occur! + err := errors.Errorf("Unexpected nil tail for InsertBuffer blocks with non nil head!") + logger.Error().Err(err).Send() + return + } + + buffer.freeTail.next = &BlockNode{block: block} + buffer.freeTail = buffer.freeTail.next +} + +func (buffer *InsertBuffer) PopFreeBlock() insertBlock { + buffer.freeLock.Lock() + defer buffer.freeLock.Unlock() + + blockNode := buffer.freeHead + if blockNode == buffer.freeTail { + buffer.freeTail = nil + } + if blockNode == nil { + return nil + } + + buffer.freeHead = blockNode.next + return blockNode.block +} + +func (buffer *InsertBuffer) PeekFreeBlock() insertBlock { + buffer.freeLock.Lock() + defer buffer.freeLock.Unlock() + + blockNode := buffer.freeHead + if blockNode == nil { + return nil + } + return blockNode.block +} + +// allocates a new block with given params and returns it +func AllocateNewBlock(signalNames []string, maxBufferBytes int, blockType InsertBlockType, tripInfo bool) insertBlock { + var newBlock insertBlock = nil + if blockType == PivotBlockType { + newBlock = NewProtoPivotBlock(signalNames, maxBufferBytes, tripInfo) + } else if blockType == SignalBlockType { + newBlock = NewProtoSignalBlock(signalNames, maxBufferBytes) + } else if blockType == VinLastBlockType { + newBlock = NewProtoVinLastBlock(signalNames, maxVinCount, tripInfo) + } + + return newBlock +} + +// appends a row to the buffer. +// Row must match the expected type by the underlying proto block. +func (buffer *InsertBuffer) AppendRow(signalNames []string, row interface{}, producerChannel chan InsertCommand) error { + block := buffer.PeekFreeBlock() + loggedWaiting := false + + for block != nil && block.IsFull() { + //if the block is full before append, then pop it and put it on the producer channel. + buffer.ProduceBlock(producerChannel) + block = buffer.PeekFreeBlock() + } + + for block == nil { + //no more free blocks are available... + // if the buffer is able to allocate more blocks, then do so. otherwise, wait for a block... + if len(buffer.blockPool) == maxBlockPoolSize { + if !loggedWaiting { + loggedWaiting = true + logger.Warn().Msgf("no available %s blocks for appending, waiting for available block...", buffer.TableName) + } + //wait + time.Sleep(100 * time.Millisecond) + block = buffer.PeekFreeBlock() + } else { + logger.Info().Msgf("%s no available block, allocating new block", buffer.TableName) + block = AllocateNewBlock(signalNames, maxBufferBytes, buffer.blockType, buffer.tripInfo) + buffer.blockPool = append(buffer.blockPool, block) + buffer.AppendFreeBlock(block) + } + } + + err := block.AppendRow(signalNames, row) + + //if block is full after append, pop it from linked list and put onto producer channel + if block.IsFull() || block.TimeSinceThreshold(buffer.insertDelay) { + buffer.ProduceBlock(producerChannel) + } + return err +} + +func (buffer *InsertBuffer) ProduceBlock(producerChannel chan InsertCommand) { + if producerChannel != nil { + logger.Debug().Msgf("ProduceBlock...") + producerChannel <- InsertCommand{ + Buffer: buffer, + Block: buffer.PopFreeBlock(), + } + } +} + +// searches blockpool for nonEmpty blocks +func (buffer *InsertBuffer) getNonEmptyBlock() insertBlock { + for _, block := range buffer.blockPool { + if block.Len() > 0 { + return block + } + } + return buffer.blockPool[0] +} + +// returns any nonempty input from buffer +func (buffer *InsertBuffer) GetInput() proto.Input { + return buffer.getNonEmptyBlock().GetProtoInput(true, nil) +} + +// reinitializes the blocks list, assuming any empty block in the pool is free and can be added to list. +// returns size of blocks list +func (buffer *InsertBuffer) InitFreeBlocksList() int { + buffer.freeHead = nil + buffer.freeTail = nil + count := 0 + for _, block := range buffer.blockPool { + if block.Len() != 0 { + continue + } + buffer.AppendFreeBlock(block) + count++ + } + return count +} + +// Inserts and flushes only the head block. +func InsertAndFlushHead(buffer *InsertBuffer, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) { + block := buffer.PopFreeBlock() + + if buffer == nil || block == nil { + return 0, errors.Errorf("attempted to insert with nil buffer") + } + block.Lock() + defer block.Unlock() + + rows := block.Len() + protoInput := block.GetProtoInput(false, signalsSet) + if protoInput == nil { + // empty buffer, just return + return 0, nil + } + queryContext, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + insertFunc := func() (interface{}, error) { + return nil, conn.Do(queryContext, ch.Query{ + Body: protoInput.Into(buffer.TableName), + Input: protoInput, + }) + } + _, err := breaker.Execute(insertFunc) + if err != nil { + return 0, err + } + + block.Flush(false) + buffer.AppendFreeBlock(block) + + return rows, err +} + +// Inserts a block and flushes it, does not readd to free blocks list if error occurred +func InsertAndFlush(command InsertCommand, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) { + buffer := command.Buffer + block := command.Block + + if buffer == nil || block == nil { + return 0, errors.Errorf("attempted to insert with nil buffer") + } + block.Lock() + defer block.Unlock() + + rows := block.Len() + protoInput := block.GetProtoInput(false, signalsSet) + if protoInput == nil { + // empty buffer, just return + return 0, nil + } + queryContext, cancel := context.WithTimeout(ctx, 5*time.Minute) + defer cancel() + + logger.Debug().Msgf("Inserting %d rows to %s...", rows, conn.ServerInfo().DisplayName) + + insertFunc := func() (interface{}, error) { + return nil, conn.Do(queryContext, ch.Query{ + Body: protoInput.Into(buffer.TableName), + Input: protoInput, + }) + } + _, err := breaker.Execute(insertFunc) + if err != nil { + return 0, err + } + + block.Flush(false) + buffer.AppendFreeBlock(block) + + return rows, err +} + +func (buffer *InsertBuffer) Len() int { + length := 0 + for _, block := range buffer.blockPool { + length += block.Len() + } + return length +} + +func (buffer *InsertBuffer) Cap() int { + length := 0 + for _, block := range buffer.blockPool { + length += block.Cap() + } + return length +} + +func (buffer *InsertBuffer) Resize(signals []string) { + for _, block := range buffer.blockPool { + block.Resize(signals, maxBufferBytes) + } +} + +type insertBlock interface { + AppendRow(signalNames []string, row interface{}) error + GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input + Flush(useLock bool) + IsFull() bool + GetInsertBlockType() InsertBlockType + Resize(signals []string, maxBytes int) + Lock() + Unlock() + Len() int + Cap() int + IsBusy() bool + TimeSinceThreshold(time.Duration) bool +} + +type protoPivotBlock struct { + lock sync.Mutex + busy bool + + length int + capacity int + + maxBytes int + + vin proto.ColStr + timestamp proto.ColDateTime64 + tripStart proto.ColDateTime64 + tripID proto.ColStr + data []proto.ColFloat64 + + startTime *time.Time + + columnNames []string + + appendTripInfo bool +} + +// block type with issue +func NewProtoPivotBlock(signalNames []string, maxBufferBytes int, tripInfo bool) *protoPivotBlock { + newBlock := protoPivotBlock{ + columnNames: signalNames, + appendTripInfo: tripInfo, + } + logger.Debug().Msgf("NEW PIVOT BLOCK: %d %d", len(signalNames), maxBufferBytes) + newBlock.Resize(signalNames, maxBufferBytes) + return &newBlock +} + +func (block *protoPivotBlock) Lock() { + block.lock.Lock() + block.busy = true +} + +func (block *protoPivotBlock) Unlock() { + block.lock.Unlock() + block.busy = false +} + +func (block *protoPivotBlock) IsBusy() bool { + return block.busy +} + +func (block *protoPivotBlock) Len() int { + return block.length +} + +func (block *protoPivotBlock) Cap() int { + return block.capacity +} + +func (block *protoPivotBlock) TimeSinceThreshold(threshold time.Duration) bool { + return block.startTime == nil || time.Since(*block.startTime) > threshold +} + +// Resizes this pivotBlock to the desired number of signal columns, and scales rows to not exceed maxBytes +func (block *protoPivotBlock) Resize(signals []string, maxBytes int) { + block.Lock() + defer block.Unlock() + + if len(signals) == len(block.columnNames) && maxBytes == block.maxBytes { + block.Flush(false) + return + } + + oldCapacity := block.capacity + oldWidth := len(block.columnNames) + + block.columnNames = signals + block.maxBytes = maxBytes + + block.capacity = block.estimateMaxRows(len(signals), maxBytes) + block.length = 0 + + logger.Debug().Msgf("resizing block to %d rows, %d columns", block.capacity, len(signals)) + + //reallocate memory only if block dimensions have changed + if oldCapacity != block.capacity || oldWidth != len(signals) { + block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity) + block.vin.Pos = make([]proto.Position, 0, block.capacity) + + block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity) + block.tripStart.Data = make([]proto.DateTime64, 0, block.capacity) + block.timestamp.WithPrecision(proto.PrecisionNano) + block.tripStart.WithPrecision(proto.PrecisionNano) + + block.tripID.Buf = make([]byte, 0, (utils.MaxVinLength+utils.MaxTimestampLength+1)*block.capacity) + block.tripID.Pos = make([]proto.Position, 0, block.capacity) + + block.data = make([]proto.ColFloat64, len(signals)) + for i := range block.data { + block.data[i] = make([]float64, 0, block.capacity) + } + } + + block.Flush(false) +} + +func (block *protoPivotBlock) GetInsertBlockType() InsertBlockType { + return PivotBlockType +} + +func (block *protoPivotBlock) IsFull() bool { + block.Lock() + defer block.Unlock() + return block.length >= block.capacity +} + +// Given upper bound for bytes, estimate the maximum number of rows to allocate +func (block *protoPivotBlock) estimateMaxRows(numDataColumns int, maxBytes int) int { + rowBytes := 20 + 2 + 8 + 8*numDataColumns //vin, timestamp, data columns per row + if block.appendTripInfo { + rowBytes += 8 + 44 + 2 //tripstart and tripid + } + return maxBytes / rowBytes +} + +// Appends a VehicleState as a row to this Block. +func (block *protoPivotBlock) AppendRow(signalNames []string, row interface{}) error { + block.Lock() + defer block.Unlock() + + if block.length >= block.capacity { + return errors.WithStack(utils.ErrInsertFullBlock) + } + if len(signalNames) != len(block.data) { + return errors.WithStack(utils.ErrInsertWrongColumns) + } + state, valid := row.(*VehicleState) + if !valid { + return errors.WithStack(utils.ErrInvalidAppendType) + } + block.length++ + + if block.startTime == nil { + time := time.Now() + block.startTime = &time + } + + block.vin.Append(state.VIN) + block.timestamp.Append(state.Timestamp) + if block.appendTripInfo { + block.tripStart.Append(state.TripStart) + block.tripID.Append(state.TripID) + } + + // 3 hour leeway + // Saw a lot of signals with a delay around 3 hours, going to now only check for signals over a day old + catchTime := state.Timestamp.Add(-time.Hour * 24) + for i, signal := range signalNames { + value, valid := state.StateValues[signal] + if !valid { + value = math.NaN() + } else { + // If the signal is not included, I'm not going to check its timing. + // should no linger see the !ok case + // Block that should have my issue + signalTime, ok := state.StateTimes[signal] + if !ok { + // So a lot of signals do not have a timestamp on them, not sure why + logSampler.Warn().Str("Location", "protoPivotBlock").Str("VIN", state.VIN).Str("Signal", signal). + Str("Location", "protoPivotBlock").Float64("Value", value).Int("Sample Rate", NO_TIMESTAMP_LOG_SAMPLE_RATE).Msg("AppendRow No Timestamp") + } else { + // If the signal is from 3 hours before the suggested time of the signal + if signalTime.Before(catchTime) { + logger.Warn().Str("VIN", state.VIN). + Str("Signal", signal). + Float64("Value", value). + Time("timestamp.state", state.Timestamp). + Time("timestamp.signal", signalTime). + Str("Location", "protoPivotBlock"). + Msg("AppendRow Timestamp Old") + } + } + } + + block.data[i].Append(value) + } + + return nil +} + +// Clears and Resets the buffers in the Block. +func (block *protoPivotBlock) Flush(useLock bool) { + if useLock { + block.Lock() + defer block.Unlock() + } + block.length = 0 + block.startTime = nil + + block.vin.Reset() + block.timestamp.Reset() + block.tripStart.Reset() + block.tripID.Reset() + + for i := range block.data { + block.data[i].Reset() + } + +} + +// Gets protocol input struct for ch-go batch insertion +func (block *protoPivotBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input { + if useLock { + block.Lock() + defer block.Unlock() + } + + if block.length == 0 { + return nil + } + + input := proto.Input{ + {Name: "VIN", Data: block.vin}, + {Name: "Timestamp", Data: block.timestamp}, + } + + if block.appendTripInfo { + input = append(input, proto.InputColumn{Name: "TripStart", Data: block.tripStart}) + input = append(input, proto.InputColumn{Name: "TripID", Data: block.tripID}) + } + + for i := range block.data { + columnName := block.columnNames[i] + ok := true + if len(signalsSet) > 0 { + _, ok = signalsSet[columnName] + } + if !ok { + continue + } + input = append(input, proto.InputColumn{Name: columnName, Data: block.data[i]}) + } + return input +} + +// Block struct for feature_table_last type of schema +// This block aggregates only 1 row per VIN. vinIndexMap maps the VIN to a row index. +type protoVinLastBlock struct { + protoPivotBlock + vinIndexMap map[string]int +} + +func NewProtoVinLastBlock(signalNames []string, numVINs int, tripInfo bool) *protoVinLastBlock { + newBlock := protoVinLastBlock{} + newBlock.columnNames = append(newBlock.columnNames, signalNames...) + newBlock.vinIndexMap = make(map[string]int) + newBlock.appendTripInfo = tripInfo + + logger.Debug().Msgf("NEW VIN LAST BLOCK: %d %d", len(signalNames), maxBufferBytes) + + bytesPerVIN := 20 + 16 + 16 + 20 //VIN (str), Timestamp, TripStart (timestamp), TripID (str) + bytesPerVIN += 8 * len(signalNames) + + newBlock.Resize(signalNames, bytesPerVIN*numVINs) + return &newBlock +} + +func (block *protoVinLastBlock) Flush(useLock bool) { + if useLock { + block.Lock() + defer block.Unlock() + } + block.protoPivotBlock.Flush(false) + + block.vinIndexMap = make(map[string]int) +} + +// Appends a VehicleState as a row to this Block. +// Investigate this code vs protoPivotBlock. WHy the differences, why two of the same thing? +func (block *protoVinLastBlock) AppendRow(signalNames []string, row interface{}) error { + block.Lock() + defer block.Unlock() + + if block.length >= block.capacity { + return errors.WithStack(utils.ErrInsertFullBlock) + } + if len(signalNames) != len(block.data) { + return errors.WithStack(utils.ErrInsertWrongColumns) + } + state, valid := row.(*VehicleState) + if !valid { + return errors.WithStack(utils.ErrInvalidAppendType) + } + + vinIndex, ok := block.vinIndexMap[state.VIN] + + if block.startTime == nil { + time := time.Now() + block.startTime = &time + } + + if !ok { + // append state to the end of the buffer + block.length++ + + block.vin.Append(state.VIN) + block.timestamp.Append(state.Timestamp) + if block.appendTripInfo { + block.tripStart.Append(state.TripStart) + block.tripID.Append(state.TripID) + } + + // catchTime := state.Timestamp.Add(-time.Hour * 3) + for i, signal := range signalNames { + value, valid := state.StateValues[signal] + if !valid { + value = math.NaN() + } else { + // If the signal is not included, I'm not going to check its timing. + // should no linger see the !ok case + // signalTime, ok := state.StateTimes[signal] + // if !ok { + // logger.Warn().Str("Location", "protoVinLastBlock, !ok").Msgf("AppendRow no timestamp for %s", signal) + // } else { + // // If the signal is from 3 hours before the suggested time of the signal + // if signalTime.Before(catchTime) { + // logger.Warn().Str("VIN", state.VIN). + // Str("Signal", signal). + // Time("timestamp.state", state.Timestamp). + // Time("timestamp.signal", signalTime). + // Str("Location", "protoVinLastBlock, !ok"). + // Msg("AppendRow Timestamp Old") + // } + // } + } + + block.data[i].Append(value) + } + + block.vinIndexMap[state.VIN] = block.length - 1 + } else { + // override only the row mapped to the vin + block.timestamp.Data[vinIndex] = proto.ToDateTime64(state.Timestamp, block.timestamp.Precision) + block.tripStart.Data[vinIndex] = proto.ToDateTime64(state.TripStart, block.timestamp.Precision) + SetColStr(&block.tripID, state.TripID, vinIndex) + + //catchTime := state.Timestamp.Add(-time.Hour * 3) + for i, signal := range signalNames { + value, valid := state.StateValues[signal] + if !valid { + value = math.NaN() + } else { + // If the signal is not included, I'm not going to check its timing. + // should no linger see the !ok case + // signalTime, ok := state.StateTimes[signal] + // if !ok { + // logger.Warn().Str("Location", "protoVinLastBlock, ok").Msgf("AppendRow no timestamp for %s", signal) + // } else { + // // If the signal is from 3 hours before the suggested time of the signal + // if signalTime.Before(catchTime) { + // logger.Warn().Str("VIN", state.VIN). + // Str("Signal", signal). + // Time("timestamp.state", state.Timestamp). + // Time("timestamp.signal", signalTime). + // Str("Location", "protoVinLastBlock, ok"). + // Msg("AppendRow Timestamp Old") + // } + // } + } + + block.data[i][vinIndex] = value + } + } + return nil + +} + +// Block struct for vehicle_signal type of schema +type protoSignalBlock struct { + lock sync.Mutex + busy bool + + length int + capacity int + + maxBytes int + + startTime *time.Time + + vin proto.ColStr + timestamp proto.ColDateTime64 + id proto.ColInt16 + name proto.ColStr + value proto.ColFloat64 +} + +func NewProtoSignalBlock(signalNames []string, maxBufferBytes int) *protoSignalBlock { + newBlock := protoSignalBlock{} + newBlock.Resize(signalNames, maxBufferBytes) + return &newBlock +} + +func (block *protoSignalBlock) Lock() { + block.lock.Lock() + block.busy = true +} + +func (block *protoSignalBlock) Unlock() { + block.lock.Unlock() + block.busy = false +} + +func (block *protoSignalBlock) IsBusy() bool { + return block.busy +} + +func (block *protoSignalBlock) Len() int { + return block.length +} + +func (block *protoSignalBlock) Cap() int { + return block.capacity +} + +func (block *protoSignalBlock) TimeSinceThreshold(threshold time.Duration) bool { + return block.startTime == nil || time.Since(*block.startTime) > threshold +} + +// Resizes allocated memory for the Block, given maxBytes as the upper bound for memory size +func (block *protoSignalBlock) Resize(signals []string, maxBytes int) { + block.Lock() + defer block.Unlock() + + if maxBytes == block.maxBytes { + block.Flush(false) + return + } + + oldCapacity := block.capacity + + block.maxBytes = maxBytes + block.capacity = block.estimateMaxRows(maxBytes) + + logger.Debug().Msgf("resizing block to %d rows", block.capacity) + + //reallocate memory only if block dimensions have changed + if oldCapacity != block.capacity { + block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity) + block.vin.Pos = make([]proto.Position, 0, block.capacity) + + block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity) + block.timestamp.WithPrecision(proto.PrecisionMicro) + + block.id = make([]int16, 0, block.capacity) + + block.name.Buf = make([]byte, 0, 20*block.capacity) + block.name.Pos = make([]proto.Position, 0, block.capacity) + + block.value = make([]float64, 0, block.capacity) + } + + block.Flush(false) +} + +func (block *protoSignalBlock) GetInsertBlockType() InsertBlockType { + return SignalBlockType +} + +func (block *protoSignalBlock) IsFull() bool { + block.Lock() + defer block.Unlock() + return block.length >= block.capacity +} + +// Given upper bound for bytes, estimate the maximum number of rows to allocate +func (block *protoSignalBlock) estimateMaxRows(maxBytes int) int { + const rowBytes int = 17 + 8 + 2 + 30 + 8 + 2 + 2 //Each row is ~65 bytes. + return maxBytes / rowBytes +} + +// Clears and Resets the buffers in the Block. +func (block *protoSignalBlock) Flush(useLock bool) { + if useLock { + block.Lock() + defer block.Unlock() + } + block.vin.Reset() + block.timestamp.Reset() + block.id.Reset() + block.name.Reset() + block.value.Reset() + + block.startTime = nil + block.length = 0 +} + +// Appends a kafka_grpc.GRPC_CANSignal to this block as a row +func (block *protoSignalBlock) AppendRow(signalNames []string, row interface{}) error { + block.Lock() + defer block.Unlock() + + if block.length >= block.capacity { + return errors.WithStack(utils.ErrInsertFullBlock) + } + signal, valid := row.(*kafka_grpc.GRPC_CANSignal) + if !valid { + return errors.WithStack(utils.ErrInvalidAppendType) + + } + + if block.startTime == nil { + time := time.Now() + block.startTime = &time + } + + timestamp := utils.FloatToTime(signal.Timestamp) + + block.vin.Append(signal.Vin) + block.timestamp.Append(timestamp) + block.id.Append(int16(signal.Id)) + block.name.Append(signal.Name) + block.value.Append(signal.Value) + + block.length++ + + return nil +} + +// Gets protocol input struct for ch-go batch insertion +func (block *protoSignalBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input { + if useLock { + block.Lock() + defer block.Unlock() + } + + if block.length == 0 { + return nil + } + input := proto.Input{ + {Name: "VIN", Data: block.vin}, + {Name: "Timestamp", Data: block.timestamp}, + {Name: "Name", Data: block.name}, + {Name: "Value", Data: block.value}, + {Name: "ID", Data: block.id}, + } + return input +} + +/// helper methods /// + +// Sets the value of string in-place in a proto colstr. +// This will reallocate memory as needed. +func SetColStr(col *proto.ColStr, value string, index int) { + pos := col.Pos[index] + oldLen := pos.End - pos.Start + newLen := len(value) + offset := newLen - oldLen + + newEnd := pos.Start + len(value) + + oldBufLen := len(col.Buf) + + //need to resize buffers + if offset > 0 { + // grow buffer by appending zero bytes, then truncating the length (not the cap) of slice + col.Buf = append(col.Buf, make([]byte, offset)...)[:oldBufLen] + } + + // need to shift EVERYTHING after index. This can be slow. + if offset != 0 { + // copy buffer into itself with offset. + copy(col.Buf[newEnd:len(col.Buf)], col.Buf[pos.End:oldBufLen]) + + for i := index + 1; i < len(col.Pos); i++ { + col.Pos[i].Start += offset + col.Pos[i].End += offset + } + } + + //insert + copy(col.Buf[pos.Start:newEnd], value) + col.Pos[index].End = newEnd + +} diff --git a/services/jetfire/models/vehicle.go b/services/jetfire/models/vehicle.go new file mode 100644 index 0000000..18a7128 --- /dev/null +++ b/services/jetfire/models/vehicle.go @@ -0,0 +1,237 @@ +package models + +import ( + "fmt" + "time" + + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + maxVinCount = envtool.GetEnvInt("JETFIRE_MAX_VINS", 10000) + timestampThreshold = 500 * time.Millisecond +) + +// Vehicle State stores latest state for a particular VIN. +type VehicleState struct { + VIN string + Timestamp time.Time //data timestamp of last data sample + TripStart time.Time //data timestamp of start of trip + TripID string //trip id + StateValues map[string]float64 //map of values in state. Keys correspond to CAN signal name + StateTimes map[string]time.Time //map of timestamps in state. Keys correspond to CAN signal name + + InsertTime time.Time // local timestamp of last received data. Used only for removing old vehicle states from cache + + pollingMap map[uint]time.Time //map of last polling times. Key matches update flag + + // for linked list + Next *VehicleState + prev *VehicleState +} + +func (v *VehicleState) TimeSincePolled(updateIndex uint) time.Duration { + pollTime, ok := v.pollingMap[updateIndex] + if !ok { + pollTime = time.Unix(0, 0) + } + + return v.Timestamp.Sub(pollTime) +} + +func (v *VehicleState) SetPollTime(updateIndex uint) { + v.pollingMap[updateIndex] = v.Timestamp +} + +func (v *VehicleState) Clear(newVIN string) { + v.VIN = newVIN + cacheSize := len(v.StateValues) + + v.StateValues = make(map[string]float64, cacheSize) + v.StateTimes = make(map[string]time.Time, cacheSize) + v.Timestamp = time.Unix(0, 0) + v.TripStart = v.Timestamp + v.TripID = "" + v.InsertTime = time.Now().UTC() + v.pollingMap = make(map[uint]time.Time) +} + +// VehicleCache is a table of vehicle states. +type VehicleCache struct { + Cache map[string]*VehicleState + SignalsSet *map[string]bool + LastTimestamp time.Time + + // linked list LIFO of States in order of update. + StatesListHead *VehicleState //oldest update + StatesListTail *VehicleState //newest update +} + +func (cache *VehicleCache) Clear() { + clear(cache.Cache) + + // disconnect linked list + node := cache.StatesListHead + for node != nil { + next := node.Next + node.Next = nil + node.prev = nil + + node = next + } + cache.StatesListHead = nil + cache.StatesListTail = nil +} + +// removes the node from linked list and appends it to the right end +func (cache *VehicleCache) ReinsertRight(node *VehicleState) { + if cache.StatesListTail == node { + // node is already the tail. don't do anything + return + } + + // move head ptr if we are moving the first node + if cache.StatesListHead == node { + cache.StatesListHead = node.Next + } + + // remove from list + if node.prev != nil && node.Next != nil { + p := node.prev + n := node.Next + n.prev = p + p.Next = n + } else if node.prev != nil { + node.prev.Next = nil + } else if node.Next != nil { + node.Next.prev = nil + } + + node.prev = nil + node.Next = nil + + // append to tail + if cache.StatesListTail != nil { + cache.StatesListTail.Next = node + } + + node.prev = cache.StatesListTail + cache.StatesListTail = node + + if cache.StatesListHead == nil { + cache.StatesListHead = node + } +} + +// removes left node from the linked list AND from the cache map +func (cache *VehicleCache) PopLeft() *VehicleState { + if cache.StatesListHead == nil { + return nil + } + + node := cache.StatesListHead + cache.StatesListHead = node.Next + + delete(cache.Cache, node.VIN) + + return node +} + +// Insert CANSignal into vehicle cache. Allocates new vehicle state as necessary. +func (cache *VehicleCache) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) error { + if len(*cache.SignalsSet) > 0 { + // check if signal is in signals set, skip if not in signals set + _, contains := (*cache.SignalsSet)[signal.Name] + if !contains { + return nil + } + } + + // if VIN is not in cache, allocate new vehicle state for VIN and add to cache + _, contains := cache.Cache[signal.Vin] + if !contains { + if len(cache.Cache) > maxVinCount { + oldState := cache.PopLeft() + logger.Debug().Msgf("repurposing state %s -> %s", oldState.VIN, signal.Vin) + oldState.Clear(signal.Vin) + cache.Cache[signal.Vin] = oldState + } else { + cache.Cache[signal.Vin] = NewVehicleState(signal.Vin, len(*cache.SignalsSet)) + logger.Debug().Msgf("new vehicle state %s", signal.Vin) + } + } + + //update value + cache.Cache[signal.Vin].UpdateSignal(signal, updateFlag) + cache.ReinsertRight(cache.Cache[signal.Vin]) //move state to end of orderly linkedlist + cache.LastTimestamp = time.Now().UTC() + return nil +} + +// constructs a new VehicleState +func NewVehicleState(VIN string, cacheSize int) *VehicleState { + newState := new(VehicleState) + newState.VIN = VIN + newState.StateValues = make(map[string]float64, cacheSize) + newState.StateTimes = make(map[string]time.Time, cacheSize) + newState.Timestamp = time.Unix(0, 0) + newState.TripStart = newState.Timestamp + newState.TripID = "" + newState.InsertTime = time.Now().UTC() + newState.pollingMap = make(map[uint]time.Time) + return newState +} + +// constructs a new VehicleState +func NewVehicleStateDefault(VIN string) *VehicleState { + return NewVehicleState(VIN, 10) +} + +// UpdateSignal() updates the vehicle state cache +func (state *VehicleState) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) { + // Mark start of new trip if too much time has elapsed between updates + signalTime := utils.FloatToTime(signal.Timestamp) + + ignitionTriggered := false + // check for vehicle ignition rising edge as a trigger for a new trip. + if signal.Name == "BCM_PwrMod" && signal.Value >= 2 && signal.Value <= 4 && !signalTime.Before(state.Timestamp) { + pwrMod, ok := state.StateValues["BCM_PwrMod"] + ignitionTriggered = !ok || (ok && pwrMod < 2) + } + + if signalTime.Sub(state.Timestamp) >= utils.TripTimeout || ignitionTriggered { + logger.Debug().Msgf("%s New TripStart: %d, old %d, delta %d", state.VIN, signalTime.Unix(), state.Timestamp.Unix(), signalTime.Sub(state.Timestamp)) + state.TripStart = signalTime + state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix()) + } + + // Update the vehicle timestamp + oldTime, ok := state.StateTimes[signal.Name] + if !ok { + oldTime = state.Timestamp + } + + if !signalTime.Add(timestampThreshold).Before(oldTime) { + if !signalTime.Before(state.Timestamp) { + state.Timestamp = signalTime + } + // Insert new signal value + state.StateValues[signal.Name] = signal.Value + state.StateTimes[signal.Name] = signalTime + state.InsertTime = time.Now() + } else { + // if receiving message out of timestamp order, only accept new signal value if + // cached value is empty for signal name + _, hasSignal := state.StateValues[signal.Name] + if !hasSignal { + state.StateValues[signal.Name] = signal.Value + state.StateTimes[signal.Name] = signalTime + state.InsertTime = time.Now() + } + utils.LogOutOfOrderMsg(signal.Name, signal.Vin) + } +} diff --git a/services/jetfire/server/server_consumer.go b/services/jetfire/server/server_consumer.go new file mode 100644 index 0000000..5b27736 --- /dev/null +++ b/services/jetfire/server/server_consumer.go @@ -0,0 +1,164 @@ +package server + +import ( + "context" + "time" + + "github.com/fiskerinc/cloud-services/services/jetfire/handlers" + "github.com/fiskerinc/cloud-services/services/jetfire/models" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + + "github.com/intel-go/fastjson" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" +) + +// StartConsumer runs consumer and puts vehicle signals into a channel for router +func StartConsumer(ctx context.Context, topic string) { + logger.Debug().Str("StartConsumer", "").Send() + defer func() { + if err := recover(); err != nil { + logger.Error().Msgf("PanicConsumer %v", err) + } + }() + + producerChannel := make(chan models.InsertCommand, 100) + signalsChannel := make(chan *kafka.Message) + rebalanceChannel := make(chan struct{}) + + consumer, err := services.GetKafkaConsumer() + if err != nil { + logger.Fatal().Err(err) + panic(err) + } + + logger.Debug().Msgf("Starting routeEvents and InserterLoop...") + + go routeEvents(signalsChannel, rebalanceChannel, producerChannel) + + go InserterLoop(producerChannel, ctx) + + for { + logger.Debug().Msgf("Calling ConsumeOrRebalancedCatch...") + consumer.Subscribe([]string{topic}) + err = consumer.ConsumeOrRebalancedCatch([]string{topic}, signalsChannel, rebalanceChannel) + if err != nil { + logger.Error().Err(err).Send() + } + if !loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) { + check := consumer.Check(ctx) + if check != nil { + logger.Error().Err(check).Send() + } + } + time.Sleep(500 * time.Millisecond) + + // reset the kafka consumer and gc the old one + consumer = nil + for consumer == nil || err != nil { + consumer, err = services.ResetKafkaConsumer() + if err != nil { + logger.Error().Err(err).Send() + } + } + } +} + +// processes signals in batch, handles caching of vehicle state and appending to insertion batch caches +func routeEvents(signalsChannel chan *kafka.Message, rebalanceChannel <-chan struct{}, producerChannel chan models.InsertCommand) { + var jsonErr error + var protoErr error + var batchData []*kafka_grpc.GRPC_CANSignal + + initialized := false + + //Consumer loop + rebalanceFlag := true + for { + select { + case signal := <-signalsChannel: + if signal == nil { + continue + } + rebalanceFlag = true + + if !initialized { + // init cache after rebalancing + logger.Debug().Msgf("INITIALIZING CACHE...") + time.Sleep(time.Second) + services.InitCacheFromClickhouse() + + initialized = true + logger.Debug().Msgf("INITIALIZED...") + } + + batchData, protoErr = unmarshalProtobufSignal(signal) + if protoErr != nil { + batchData, jsonErr = unmarshalJSONSignal(signal) + if jsonErr != nil { + logger.Error().Err(protoErr).Msg("Failed to unmarshal signal as either Protobuf or JSON") + continue + } + } + + _, err := handlers.HandleSignalBatch(batchData, services.GetVehicleCache(), producerChannel) + if err != nil { + logger.Error().Err(errors.WithStack(err)).Send() + } + + case <-rebalanceChannel: + if rebalanceFlag { + rebalanceFlag = false + + logger.Info().Msgf("kafka rebalancing...") + initialized = false + } + } + } + +} + +func unmarshalProtobufSignal(event *kafka.Message) ([]*kafka_grpc.GRPC_CANSignal, error) { + if event == nil { + err := errors.Errorf("trying to unmarshall null event ptr") + return nil, err + } + batchData := kafka_grpc.GRPC_CANSignalBatchPayload{} + err := proto.Unmarshal(event.Value, &batchData) + if err != nil { + return nil, err + } + return batchData.Data.Cansignals, nil +} + +func unmarshalJSONSignal(event *kafka.Message) ([]*kafka_grpc.GRPC_CANSignal, error) { + if event == nil { + err := errors.Errorf("trying to unmarshall null event ptr") + return nil, err + } + //JSON handling is generally slower; + //not only is payload much larger but character insertions to fix json format from optimus is very slow. + + batchData := []kafka_grpc.GRPC_CANSignal{} + dataBuffer := make([]byte, len(event.Value)+2) + copy(dataBuffer[1:], event.Value) + dataBuffer[0] = '[' + dataBuffer[len(event.Value)+1] = ']' + + err := fastjson.Unmarshal(dataBuffer, &batchData) + if err != nil { + logger.Error().Err(err).Send() + return nil, err + } + + ptrs := make([]*kafka_grpc.GRPC_CANSignal, len(batchData)) + for i := range batchData { + ptrs[i] = &batchData[i] + } + return ptrs, nil +} diff --git a/services/jetfire/server/server_http.go b/services/jetfire/server/server_http.go new file mode 100644 index 0000000..85d3ef6 --- /dev/null +++ b/services/jetfire/server/server_http.go @@ -0,0 +1,25 @@ +package server + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/services/jetfire/handlers" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/logger" + + "github.com/julienschmidt/httprouter" +) + +const port string = ":8077" + +func StartHTTPServer() { + router := httprouter.New() + router.PanicHandler = httphandlers.HttpRouterPanicHandler + addHandler(router, http.MethodGet, "/reset", handlers.ResetSchemaDefinitions) + logger.Fatal().AnErr("http.ListenAndServe", http.ListenAndServe(port, router)).Send() +} + +func addHandler(router *httprouter.Router, method string, path string, handler http.HandlerFunc) { + router.HandlerFunc(method, httphandlers.HttpRouterHandleBaseURL(path), handler) +} diff --git a/services/jetfire/server/server_inserter.go b/services/jetfire/server/server_inserter.go new file mode 100644 index 0000000..e2e593b --- /dev/null +++ b/services/jetfire/server/server_inserter.go @@ -0,0 +1,99 @@ +package server + +import ( + "context" + "github.com/fiskerinc/cloud-services/services/jetfire/models" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "time" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/ClickHouse/ch-go" + "github.com/ClickHouse/ch-go/proto" + "github.com/pkg/errors" + + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + flushBufferCmd chan interface{} + + schemaResetPeriod = envtool.GetEnvDuration("JETFIRE_SCHEMA_RESET_PERIOD_MS", 3600000*12) * time.Millisecond +) + +// loop for Inserter goroutine. +// This goroutine is responsible for inserting data into clickhouse +func InserterLoop(producerChannel chan models.InsertCommand, ctx context.Context) { + resetSchemaTicker := time.NewTicker(schemaResetPeriod) + for { + select { + // reset cache vars every hour + case <-resetSchemaTicker.C: + logger.Debug().Msgf("<-resetSchemaTicker") + services.ResetCacheVars() + + // process flush buffer command + case <-flushBufferCmd: + logger.Debug().Msgf("<-flushBufferCmd") + + //get clickhouse client + client := services.GetShardClient() + for client == nil || client.IsClosed() { + logger.Error().Err(errors.Errorf("bad chgo client , retrying connection...")).Send() + time.Sleep(time.Second) + services.InitShardClients() + client = services.GetShardClient() + } + + logger.Debug().Msgf("InsertFlushAllBuffers??") + services.InsertFlushAllBuffers(client) + + // process data to be inserted to clickhouse + case command := <-producerChannel: + logger.Debug().Msgf("<-producerChannel") + // get clickhouse client + client := services.GetShardClient() + for client == nil || client.IsClosed() { + logger.Error().Err(errors.Errorf("bad chgo client , retrying connection...")).Send() + time.Sleep(time.Second) + services.InitShardClients() + client = services.GetShardClient() + } + + //insert the queued buffer and block in command + var err error + retryInsert := true + count := 0 + insertTime := time.Now() + for retryInsert { + count, err = models.InsertAndFlush(command, ctx, client.GetClient(), client.GetBreaker(), nil) + if err == nil { + break + } + + logger.Error().Err(errors.WithStack(err)).Send() + // if the error is a mismatching schema error, then pull new schemas + if ch.IsErr(err, + proto.ErrIncompatibleColumns, + proto.ErrNoSuchColumnInTable, + proto.ErrThereIsNoColumn, + proto.ErrIncorrectNumberOfColumns, + ) { + services.ResetCacheVars() + } else { + // client.Connect() //close and restart the clickhouse connection + client = services.GetShardClient() //grab a new shard client to retry insert + } + time.Sleep(time.Second) + } + if count == 0 { + continue + } + + //log messages relating to clickhouse insertion + logger.Debug().Msgf("done flush Buffer %s, %dms", command.Buffer.TableName, (time.Since(insertTime))/time.Millisecond) + if time.Since(insertTime) > 10*time.Second { + logger.Warn().Msgf("slow row insertions: took %s to insert %d rows for %s", time.Since(insertTime), count, command.Buffer.TableName) + } + } + } +} diff --git a/services/jetfire/services/cache.go b/services/jetfire/services/cache.go new file mode 100644 index 0000000..5f17880 --- /dev/null +++ b/services/jetfire/services/cache.go @@ -0,0 +1,303 @@ +package services + +import ( + "context" + "fmt" + "github.com/fiskerinc/cloud-services/services/jetfire/models" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "reflect" + "strings" + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" +) + +var ( + //singleton cache definitions for vehicle states + vehicleCache *models.VehicleCache + + // lists of variables needed by destination tables + featureVars []string + + // set of all variables needing to be tracked + featureSet map[string]bool + varsSet map[string]bool + + SchemaLock sync.Mutex +) + +// Initializes feature variables by pulling the table schema for destination tables +func ResetCacheVars() { + SchemaLock.Lock() + defer SchemaLock.Unlock() + + conn, err := GetClickhouseConnection() + if err != nil { + logger.Error().Err(err).Send() + return + } + featureVars = GetVarList(conn, FEATURE_TABLE, utils.FeatureVarsDefaults) + logger.Debug().Msgf("FEATURE VARS %d", len(featureVars)) + + featureSet = make(map[string]bool) + varsSet = make(map[string]bool) + + for _, key := range featureVars { + varsSet[key] = true + featureSet[key] = true + } + logger.Debug().Msgf("VARSSET %d", len(varsSet)) + + vCache := GetVehicleCache() + vCache.SignalsSet = &varsSet + + //insert and flush the insertion buffers + insertClient := GetShardClient() + + //guarantee initialization of singleton buffer + //GetVehicleSignalBatch() + + //only insert signals in the intersection of insert buffer columns and updated sink table columns + + fBuffer := GetFeatureBatch() + flBuffer := GetFeatureLastBatch() + + if insertClient != nil { + ctx := context.Background() + models.InsertAndFlushHead(fBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet) + models.InsertAndFlushHead(flBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet) + } + //now resize the buffers and reset the column names + fBuffer.Resize(featureVars) + + //now resize the buffers and reset the column names + flBuffer.Resize(featureVars) +} + +// flushes the head node for all buffers. +func InsertFlushAllBuffers(client *ChGoClient) { + ctx := context.Background() + + //models.InsertAndFlushHead(GetVehicleSignalBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet) + models.InsertAndFlushHead(GetFeatureBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet) + models.InsertAndFlushHead(GetFeatureLastBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet) +} + +func GetFeatureVars() []string { + return featureVars +} + +// Initializes the VIN cache from clickhouse feature table. This is used during Kafka rebalance events. +func InitCacheFromClickhouse() { + SchemaLock.Lock() + defer SchemaLock.Unlock() + + logger.Debug().Msgf("InitCacheFromClickhouse") + //query feature table + + featureColumns := append([]string{"VIN", "Timestamp", "TripStart", "TripID"}, featureVars...) + logger.Debug().Msgf("Querying feature table, %d columns", len(featureColumns)) + + queryString := fmt.Sprintf("SELECT * FROM %s LIMIT 1 BY VIN", FEATURE_LAST_TABLE) + populateCacheFromTable(GetVehicleCache(), queryString, &featureColumns) + + logger.Debug().Msgf("Pulled cache from clickhouse! %d vehicles!", len(GetVehicleCache().Cache)) +} + +// Performs a query, expecting a dynamic schema from the table. Query results are used to populate cache. +// Columns describes the columns expected in the table +func populateCacheFromTable(cache *models.VehicleCache, query string, columns *[]string) { + ctx := context.Background() + logger.Debug().Msgf("query: %s", query) + conn, err := GetClickhouseConnection() + + if err != nil { + logger.Error().Err(err) + return + } + + if !HasClickhouseParams() { + logger.Error().Msgf("Could not open clickhouse connection. skipping initialization of vehicle caches") + return + } + + rows, err := QueryWithBreaker(ctx, conn, &query) + + if err != nil { + logger.Error().Err(err).Send() + panic(errors.WithStack(err)) + } + + if rows == nil { + return + } + + 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) + } + } + + defer rows.Close() + for rows.Next() { + err := rows.Scan(row...) + if err != nil { + panic(errors.WithStack(err)) + } + + vin := *row[0].(*string) + timestamp := *row[1].(*time.Time) + + _, contains := cache.Cache[vin] + // initialize new state cache as needed + if !contains { + cache.Cache[vin] = models.NewVehicleState(vin, len(varsSet)) + } + state := cache.Cache[vin] + + if timestamp.After(state.Timestamp) { + for i, valPtr := range row { + key := (*columns)[i] + if key == "VIN" { + continue + } + if key == "Timestamp" { + continue + } + if key == "TripStart" { + state.TripStart = *valPtr.(*time.Time) + state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix()) + continue + } + if key == "TripID" { + state.TripID = *valPtr.(*string) + continue + } + myValue, hasValue := state.StateValues[key] + if !hasValue || myValue == 0 || timestamp.After(state.Timestamp) { + state.StateValues[key] = *valPtr.(*float64) + } + } + state.Timestamp = timestamp + } + } + // append states to right + for _, state := range cache.Cache { + cache.ReinsertRight(state) + } +} + +// Queries clickhouse and reads into the dest struct +func LoadChState(VIN string, dest *models.VehicleState, columns *[]string) error { + ctx := context.Background() + queryString := fmt.Sprintf("SELECT * FROM %s WHERE VIN='%s' LIMIT 1", FEATURE_LAST_TABLE, VIN) + logger.Debug().Msgf("query: %s", queryString) + conn, err := GetClickhouseConnection() + + if !HasClickhouseParams() || err != nil { + return errors.Errorf("Could not open clickhouse connection. Failed to load VIN state %s", VIN) + } + + rows, err := QueryWithBreaker(ctx, conn, &queryString) + + if err != nil { + logger.Error().Err(err).Send() + panic(errors.WithStack(err)) + } + + if rows == nil { + return nil + } + + 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) + } + } + + defer rows.Close() + + for rows.Next() { + err := rows.Scan(row...) + if err != nil { + return err + } + + timestamp := *row[1].(*time.Time) + + for i, valPtr := range row { + key := (*columns)[i] + if key == "VIN" { + dest.VIN = *row[0].(*string) + continue + } + if key == "Timestamp" { + dest.Timestamp = timestamp + continue + } + if key == "TripStart" { + dest.TripStart = *valPtr.(*time.Time) + dest.TripID = fmt.Sprintf("%s_%d", dest.VIN, dest.TripStart.Unix()) + continue + } + if key == "TripID" { + dest.TripID = *valPtr.(*string) + continue + } + _, ok := dest.StateValues[key] + if ok { + dest.StateValues[key] = *valPtr.(*float64) + } + } + } + + GetVehicleCache().ReinsertRight(dest) + + return nil +} + +// Returns Singleton Vehicle Cache; only one is needed for Jetfire service +func GetVehicleCache() *models.VehicleCache { + if vehicleCache != nil { + return vehicleCache + } + vehicleCache = new(models.VehicleCache) + + vehicleCache.SignalsSet = &varsSet + vehicleCache.Cache = make(map[string]*models.VehicleState) + vehicleCache.LastTimestamp = utils.FloatToTime(0.0) + + return vehicleCache +} diff --git a/services/jetfire/services/chgo.go b/services/jetfire/services/chgo.go new file mode 100644 index 0000000..c349d28 --- /dev/null +++ b/services/jetfire/services/chgo.go @@ -0,0 +1,201 @@ +package services + +import ( + "context" + "fmt" + "math/rand" + "time" + + fClickhouse "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/ClickHouse/ch-go" + "github.com/pkg/errors" + "github.com/sony/gobreaker" + "gopkg.in/retry.v1" +) + +/* +This file implements an interface for the chgo library. +*/ + +var ( + shardClients []*ChGoClient +) + +// Gets a shard client at random +func GetShardClient() *ChGoClient { + if len(shardClients) == 0 { + InitShardClients() + } + + if len(shardClients) == 0 { + return nil + } + + index := rand.Intn(len(shardClients)) + client := shardClients[index] + + isInvalid := client.GetBreaker().State() == gobreaker.StateOpen && !client.GetClient().IsClosed() + count := 5 + + //resample shards N times to look for a shard client that is active and circruit breaker is not open + for isInvalid && count > 0 { + count-- + index = rand.Intn(len(shardClients)) + client = shardClients[index] + isInvalid = client.GetBreaker().State() == gobreaker.StateOpen && !client.GetClient().IsClosed() + } + if isInvalid { + return nil + } + + return client +} + +func InitShardClients() { + + clear(shardClients) + shardClients = shardClients[:0] + + client, err := GetClickhouseConnection() + if err != nil { + logger.Error().Err(err).Send() + } + + shards := []ShardInfo{} + err = client.Select( + context.Background(), + &shards, + "SELECT shard_num, replica_num, host_address FROM system.clusters WHERE cluster='default'", + ) + if err != nil { + err = errors.WithStack(err) + logger.Error().Err(err).Send() + } + + for _, s := range shards { + shardName := fmt.Sprintf("%d-%d", s.ShardNum, s.ReplicaNum) + logger.Debug().Msgf("Creating new shard connection %s, %s", s.HostAddress, shardName) + + client, _ := NewChgoClient( + s.HostAddress, + fClickhouse.CLICKHOUSE_PORT, + shardName, + fClickhouse.CLICKHOUSE_DB, + fClickhouse.CLICKHOUSE_USER, + fClickhouse.CLICKHOUSE_PASS, + ) + shardClients = append(shardClients, client) + } + + logger.Info().Msgf("Connected to %d shards", len(shardClients)) +} + +type ChGoClient struct { + client *ch.Client + shardName string + + retry retry.Strategy //retry is used for connecting and reconnecting + breaker *gobreaker.CircuitBreaker //circuit breaker is only used for insertions + + ch_host string + ch_port string + ch_db string + ch_user string + ch_pass string +} + +func NewChgoClient(ch_host string, ch_port string, ch_shard string, ch_db string, ch_user string, ch_pass string) (*ChGoClient, error) { + newClient := ChGoClient{ + shardName: ch_shard, + ch_host: ch_host, + ch_port: ch_port, + ch_db: ch_db, + ch_user: ch_user, + ch_pass: ch_pass, + } + + newClient.retry = retry.LimitTime( + 120*time.Second, + retry.Exponential{ + Initial: 100 * time.Millisecond, + }, + ) + err := newClient.Connect() + + if err != nil { + return nil, errors.WithStack(err) + } + + return &newClient, nil +} + +func (client *ChGoClient) Connect() error { + if client.client != nil && !client.client.IsClosed() { + client.client.Close() + } + + var err error + var newConn *ch.Client + + client.breaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: "clickhouse", + MaxRequests: 1, + Interval: time.Minute * 1, + Timeout: time.Minute * 10, + }) + + for a := retry.Start(client.retry, nil); a.Next(); { + newConn, err = ch.Dial( + context.Background(), + ch.Options{ + Address: fmt.Sprintf("%s:%s", client.ch_host, client.ch_port), + Database: client.ch_db, + User: client.ch_user, + Password: client.ch_pass, + DialTimeout: 1 * time.Minute, + }, + ) + + if err == nil { + client.client = newConn + return nil + } + } + + err = errors.WithStack(err) + return err +} + +func (c *ChGoClient) IsClosed() bool { + if c.client == nil { + return true + } + return c.client.IsClosed() +} + +func (c *ChGoClient) Close() error { + if c.client == nil { + return nil + } + return c.client.Close() +} + +func (c *ChGoClient) GetClient() *ch.Client { + return c.client +} + +func (c *ChGoClient) GetShardName() string { + return c.shardName +} + +func (c *ChGoClient) GetBreaker() *gobreaker.CircuitBreaker { + return c.breaker +} + +// helper struct for querying clickhouse shard info +type ShardInfo struct { + ShardNum uint32 `ch:"shard_num"` + ReplicaNum uint32 `ch:"replica_num"` + HostAddress string `ch:"host_address"` +} diff --git a/services/jetfire/services/clickhouse.go b/services/jetfire/services/clickhouse.go new file mode 100644 index 0000000..3676531 --- /dev/null +++ b/services/jetfire/services/clickhouse.go @@ -0,0 +1,213 @@ +package services + +import ( + "context" + "fmt" + "github.com/fiskerinc/cloud-services/services/jetfire/models" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "sync" + "time" + + fClickhouse "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + "github.com/pkg/errors" + "github.com/sony/gobreaker" + "gopkg.in/retry.v1" +) + +var ( + conn fClickhouse.ConnInterface + clickLock sync.Mutex + FEATURE_TABLE = envtool.GetEnv("CLICKHOUSE_FEATURE_TABLE", "feature_table_shard") + FEATURE_LAST_TABLE = envtool.GetEnv("CLICKHOUSE_FEATURE_LAST_TABLE", "feature_table_last_shard") + VEHICLE_SIGNAL_TABLE = envtool.GetEnv("CLICKHOUSE_VEHICLE_SIGNAL_TABLE", "vehicle_signal_shard") + + clickhouseBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: "clickhouse", + MaxRequests: 1, + Interval: time.Minute * 1, + Timeout: time.Second * 60, + OnStateChange: utils.BreakerStateChange, + ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.Requests > 0 }, + }) + + featureBuffer *models.InsertBuffer + featureLastBuffer *models.InsertBuffer + vehicleSignalBuffer *models.InsertBuffer + + retryStrategy = retry.LimitTime( + 120*time.Second, + retry.Exponential{ + Initial: 100 * time.Millisecond, + }, + ) +) + +func HasClickhouseParams() bool { + return len(envtool.GetEnv("CLICKHOUSE_USER", "")) > 0 +} + +// Returns singleton instance of clickhouse connection +func GetClickhouseConnection() (fClickhouse.ConnInterface, error) { + + clickLock.Lock() + defer clickLock.Unlock() + if conn != nil { + return conn, nil + } + + executeWrapper := func() (interface{}, error) { + return fClickhouse.NewConn() + } + + var err error + if conn == nil { + //instantiate singleton + newConn, err := clickhouseBreaker.Execute(executeWrapper) + if err != nil { + panic(errors.WithStack(err)) + } + conn = newConn.(clickhouse.Conn) + } + + return conn, err +} + +func SetClickhouseConn(newConn fClickhouse.ConnInterface) { + clickLock.Lock() + defer clickLock.Unlock() + + conn = newConn +} + +// Returns the buffer struct for batched inserts into Vehicle Signal table +func GetVehicleSignalBatch() *models.InsertBuffer { + if vehicleSignalBuffer != nil { + return vehicleSignalBuffer + } + + vehicleSignalBuffer = models.NewInsertBuffer( + false, + nil, + VEHICLE_SIGNAL_TABLE, + envtool.GetEnvDuration("JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS", 10000)*time.Millisecond, + models.SignalBlockType, + ) + return vehicleSignalBuffer +} + +// Returns the buffer struct for batched inserts into Feature table +func GetFeatureBatch() *models.InsertBuffer { + if featureBuffer != nil { + return featureBuffer + } + + featureBuffer = models.NewInsertBuffer( + true, + featureVars, + FEATURE_TABLE, + envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond, + models.PivotBlockType, + ) + return featureBuffer +} + +func GetFeatureLastBatch() *models.InsertBuffer { + if featureLastBuffer != nil { + return featureLastBuffer + } + + featureLastBuffer = models.NewInsertBuffer( + true, + featureVars, + FEATURE_LAST_TABLE, + envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond, + models.VinLastBlockType, + ) + return featureLastBuffer +} + +// Queries table schema to get ordered list of can signals +func GetVarList(conn fClickhouse.ConnInterface, table string, defaultVarsFile string) []string { + var result []string + ctx := context.Background() + if !HasClickhouseParams() { + logger.Warn().Msgf("No clickhouse params found, reading default vars instead.") + return utils.ReadVarListFromFile(defaultVarsFile) + } + + query := fmt.Sprintf("DESCRIBE %s", table) + + logger.Debug().Msgf("%s", query) + + var rows []descRow + err := SelectWithBreaker(ctx, conn, &query, &rows) + if err != nil { + logger.Warn().Err(err).Msgf("Failed to select data from clickhouse, reading default vars instead...") + return utils.ReadVarListFromFile(defaultVarsFile) + } + + for _, row := range rows { + if row.Name == "VIN" || row.Name == "Timestamp" { + continue + } + if row.Name == "TripID" || row.Name == "TripStart" { + continue + } + result = append(result, row.Name) + } + return result +} + +func SelectWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string, buffer interface{}) error { + var err error + + selectWrapper := func() (interface{}, error) { + return nil, conn.Select(ctx, buffer, *query) + } + + for a := retry.Start(retryStrategy, nil); a.Next(); { + _, err = clickhouseBreaker.Execute(selectWrapper) + if err == nil { + break + } + logger.Error().Err(err).Send() + } + return err +} + +func QueryWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string) (driver.Rows, error) { + var rows interface{} + var err error + + queryWrapper := func() (interface{}, error) { + return conn.Query(ctx, *query) + } + + for a := retry.Start(retryStrategy, nil); a.Next(); { + rows, err = clickhouseBreaker.Execute(queryWrapper) + if err == nil && rows != nil { + break + } + } + + //exit if rows is nil; do not attempt to convert to driver.Rows + if rows == nil { + return nil, err + } + + return rows.(driver.Rows), err +} + +type descRow struct { + Name string `ch:"name"` + Type string `ch:"type"` + Default_type string `ch:"default_type"` + Default_expression string `ch:"default_expression"` + Comment string `ch:"comment"` + Codec_expression string `ch:"codec_expression"` + Ttl_expression string `ch:"ttl_expression"` +} diff --git a/services/jetfire/services/kafka.go b/services/jetfire/services/kafka.go new file mode 100644 index 0000000..8673b7d --- /dev/null +++ b/services/jetfire/services/kafka.go @@ -0,0 +1,72 @@ +package services + +import ( + "sync" + "time" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" + "gopkg.in/retry.v1" +) + +const ServiceName = "jetfire" + +var ( + consumer kafka.BaseConsumerInterface = nil + + consumerLock sync.Mutex + + kafkaRetry = retry.LimitTime( + 120*time.Second, + retry.Exponential{ + Initial: 100 * time.Millisecond, + }) +) + +// GetKafkaConsumer returns singleton instance of kafka consumer +func GetKafkaConsumer() (kafka.BaseConsumerInterface, error) { + consumerLock.Lock() + defer consumerLock.Unlock() + + if consumer != nil { + return consumer, nil + } + + var err error + for a := retry.Start(kafkaRetry, nil); a.Next(); { + newConsumer, err := kafka.NewBaseConsumer(ServiceName, -1, -1, true) + if err == nil { + consumer = newConsumer + return consumer, nil + } + logger.Error().Err(err).Send() + } + err = errors.WithStack(err) + return nil, err +} + +// stops the kafka consumer, instantiates new one. gc should clean up old one +func ResetKafkaConsumer() (kafka.BaseConsumerInterface, error) { + consumerLock.Lock() + defer consumerLock.Unlock() + + logger.Info().Msgf("Resetting Kafka Consumer...") + + if consumer != nil { + consumer.Stop() + consumer = nil + } + + var err error + for a := retry.Start(kafkaRetry, nil); a.Next(); { + newConsumer, err := kafka.NewBaseConsumer(ServiceName, -1, -1, true) + if err == nil { + consumer = newConsumer + return consumer, nil + } + logger.Error().Err(err).Send() + } + err = errors.WithStack(err) + return nil, err +} diff --git a/services/jetfire/set_envs.sh b/services/jetfire/set_envs.sh new file mode 100755 index 0000000..684784d --- /dev/null +++ b/services/jetfire/set_envs.sh @@ -0,0 +1,16 @@ +#!/usr/bin/bash + +# This is a tool to just set env vars from .env +# This is used instead of a golang dotenv package, +# because there are some variables in fiskerinc.com/modules/clickhouse +# that are initialized before init() is called. +# Usage: +# Run this first before running go module locally: +# +# source ./set_envs.sh +# go run main.go +# + +set -a +source .env +set +a \ No newline at end of file diff --git a/services/jetfire/tests/bench_test.go b/services/jetfire/tests/bench_test.go new file mode 100644 index 0000000..1a4be80 --- /dev/null +++ b/services/jetfire/tests/bench_test.go @@ -0,0 +1,103 @@ +package tests + +/* +This file implements benchmarks for +1. Batch handling of messages. + The intention is to measure performance of appending rows to InsertBuffer +2. Batch serialization of messages + The intention is to measure performance of preparing data for clickhouse insertion + +Because of the size of the input data for this benchmark, it is recommended to set + JETFIRE_BUFFER_MAX_BYTES=1073741824 + +Or more; otherwise the benchmark may hang while waiting for the nonexistant inserter thread to flush the buffer. +*/ + +import ( + "github.com/fiskerinc/cloud-services/services/jetfire/handlers" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "os" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/ClickHouse/ch-go/proto" + "github.com/intel-go/fastjson" +) + +var benchmarkJSONPayload = []byte{} +var benchmarkBatchPayload = []kafka_grpc.GRPC_CANSignal{} +var benchmarkBatchPtrs = []*kafka_grpc.GRPC_CANSignal{} + +func benchInit() { + //1176 messages long + jsonPath := "test-batch-msg.json" + os.Chdir("./tests/") + data, err := os.ReadFile(jsonPath) + if err != nil { + panic(err) + } + + benchmarkJSONPayload = data + fastjson.Unmarshal(data, &benchmarkBatchPayload) + + benchmarkBatchPtrs = make([]*kafka_grpc.GRPC_CANSignal, len(benchmarkBatchPayload)) + for i := range benchmarkBatchPayload { + benchmarkBatchPtrs[i] = &benchmarkBatchPayload[i] + } + services.ResetCacheVars() +} + +func benchmarkMessageHandler(batchData []*kafka_grpc.GRPC_CANSignal, b *testing.B) { + cache := services.GetVehicleCache() + for i := 0; i < b.N; i++ { + handlers.HandleSignalBatch(batchData, cache, nil) + } + + b.StopTimer() +} + +func BenchmarkMessageHandler(b *testing.B) { + benchInit() + b.ResetTimer() + benchmarkMessageHandler(benchmarkBatchPtrs, b) +} + +func BenchmarkSignalSerialization(b *testing.B) { + benchInit() + + cache := services.GetVehicleCache() + handlers.HandleSignalBatch(benchmarkBatchPtrs, cache, nil) + + b.ResetTimer() + dummy := proto.Input{} + serializedLength := 0 + rowsLength := 0 + + for i := 0; i < b.N; i++ { + dummy = services.GetVehicleSignalBatch().GetInput() + serializedLength += len(dummy) + rowsLength += services.GetVehicleSignalBatch().Len() + } + + b.StopTimer() +} + +func BenchmarkFeatureSerialization(b *testing.B) { + benchInit() + + cache := services.GetVehicleCache() + handlers.HandleSignalBatch(benchmarkBatchPtrs, cache, nil) + + b.ResetTimer() + dummy := proto.Input{} + serializedLength := 0 + rowsLength := 0 + + for i := 0; i < b.N; i++ { + dummy = services.GetFeatureBatch().GetInput() + serializedLength += len(dummy) + rowsLength += services.GetFeatureBatch().Len() + } + + b.StopTimer() +} diff --git a/services/jetfire/tests/cache_test.go b/services/jetfire/tests/cache_test.go new file mode 100644 index 0000000..01b8bbc --- /dev/null +++ b/services/jetfire/tests/cache_test.go @@ -0,0 +1,251 @@ +package tests + +import ( + "fmt" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/stretchr/testify/assert" +) + +const testVIN = "TESTVIN1234567890" + +var testCanSignalBatch = []kafka_grpc.GRPC_CANSignal{ + {Vin: testVIN, Timestamp: 600.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 100}, + {Vin: testVIN, Timestamp: 601.0, Id: 792, Name: "ESP_VehSpd", Value: 0}, + {Vin: testVIN, Timestamp: 602.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 90}, + {Vin: testVIN, Timestamp: 603.0, Id: 792, Name: "ESP_VehSpd", Value: 10}, + {Vin: testVIN, Timestamp: 604.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80}, + {Vin: testVIN, Timestamp: 605.0, Id: 792, Name: "ESP_VehSpd", Value: 20}, + {Vin: testVIN, Timestamp: 606.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80}, + {Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 30}, + {Vin: testVIN, Timestamp: 608.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 70}, + {Vin: testVIN, Timestamp: 609.0, Id: 792, Name: "ESP_VehSpd", Value: 40}, + {Vin: testVIN, Timestamp: 608.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60}, + {Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 50}, + {Vin: testVIN, Timestamp: 610.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 50}, + {Vin: testVIN, Timestamp: 611.0, Id: 792, Name: "ESP_VehSpd", Value: 60}, + {Vin: testVIN, Timestamp: 612.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 40}, + {Vin: testVIN, Timestamp: 613.0, Id: 792, Name: "ESP_VehSpd", Value: 70}, + {Vin: testVIN, Timestamp: 614.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 30}, + {Vin: testVIN, Timestamp: 615.0, Id: 792, Name: "ESP_VehSpd", Value: 80}, + {Vin: testVIN, Timestamp: 616.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 20}, + {Vin: testVIN, Timestamp: 617.0, Id: 792, Name: "ESP_VehSpd", Value: 90}, + {Vin: testVIN, Timestamp: 618.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60}, + {Vin: testVIN, Timestamp: 619.0, Id: 792, Name: "ESP_VehSpd", Value: 100}, + {Vin: testVIN, Timestamp: 650.0, Id: 792, Name: "ESP_VehSpd", Value: 100}, + + {Vin: testVIN, Timestamp: 800.0, Id: 792, Name: "ESP_VehSpd", Value: 100}, + + {Vin: testVIN, Timestamp: 1500.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 20}, + {Vin: testVIN, Timestamp: 1501.0, Id: 792, Name: "ESP_VehSpd", Value: 90}, + {Vin: testVIN, Timestamp: 1502.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 45}, + {Vin: testVIN, Timestamp: 1503.0, Id: 792, Name: "ESP_VehSpd", Value: 13}, + + {Vin: testVIN, Timestamp: 1510.0, Id: 819, Name: "BCM_PwrMod", Value: 0}, + {Vin: testVIN, Timestamp: 1511.0, Id: 819, Name: "BCM_PwrMod", Value: 2}, + {Vin: testVIN, Timestamp: 1512.0, Id: 792, Name: "ESP_VehSpd", Value: 11}, +} + +var testCanSignalOrderBatch = []kafka_grpc.GRPC_CANSignal{ + {Vin: testVIN, Timestamp: 700.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 100}, + {Vin: testVIN, Timestamp: 601.0, Id: 792, Name: "ESP_VehSpd", Value: 0}, + {Vin: testVIN, Timestamp: 603.0, Id: 792, Name: "ESP_VehSpd", Value: 10}, + {Vin: testVIN, Timestamp: 704.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80}, + {Vin: testVIN, Timestamp: 605.0, Id: 792, Name: "ESP_VehSpd", Value: 20}, + {Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 30}, + {Vin: testVIN, Timestamp: 708.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 70}, + {Vin: testVIN, Timestamp: 609.0, Id: 792, Name: "ESP_VehSpd", Value: 40}, + {Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 50}, + {Vin: testVIN, Timestamp: 710.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 50}, + {Vin: testVIN, Timestamp: 611.0, Id: 792, Name: "ESP_VehSpd", Value: 60}, + {Vin: testVIN, Timestamp: 613.0, Id: 792, Name: "ESP_VehSpd", Value: 70}, + {Vin: testVIN, Timestamp: 714.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 30}, + {Vin: testVIN, Timestamp: 615.0, Id: 792, Name: "ESP_VehSpd", Value: 80}, + {Vin: testVIN, Timestamp: 617.0, Id: 792, Name: "ESP_VehSpd", Value: 90}, + {Vin: testVIN, Timestamp: 718.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60}, + {Vin: testVIN, Timestamp: 619.0, Id: 792, Name: "ESP_VehSpd", Value: 100}, +} + +func TestVehicleCache(t *testing.T) { + cache := services.GetVehicleCache() + + cache.Clear() + + for i, signal := range testCanSignalBatch { + cache.UpdateSignal(&signal, 0x0) + + if i == 4 { + // testing signal aggregation + state, containsState := cache.Cache[testVIN] + assert.True(t, containsState) + assert.NotNil(t, state) + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(604.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 80.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 10.0) + } + + if i == 11 { + // testing out of order messages + state := cache.Cache[testVIN] + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(609.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 40.0) + } + + if i == 17 { + // testing signal aggregation after message order is restored + state := cache.Cache[testVIN] + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(615.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 30.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 80.0) + } + + if i == 22 { + state := cache.Cache[testVIN] + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(650.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0) + } + + if i == 23 { + state := cache.Cache[testVIN] + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(800.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0) + } + + if i == 25 { + state := cache.Cache[testVIN] + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1501.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(1500.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 20.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 90.0) + } + } + + assert.Equal(t, len(cache.Cache), 1) + + // testing large timestamp gap; trigger new trip + state := cache.Cache[testVIN] + tripStartTime := utils.FloatToTime(1511.0) + assert.WithinDuration(t, state.TripStart, tripStartTime, 1e8) + assert.Equal(t, state.TripID, fmt.Sprintf("%s_%d", testVIN, 1511)) + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1512.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 45.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 11.0) +} + +func TestVehicleCacheOrderly(t *testing.T) { + cache := services.GetVehicleCache() + + cache.Clear() + + for i, signal := range testCanSignalOrderBatch { + cache.UpdateSignal(&signal, 0x0) + + if i == 8 { + state := cache.Cache[testVIN] + + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(708.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(700.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 70.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 40.0) + + } + } + state := cache.Cache[testVIN] + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(718.0), 1e8) + assert.WithinDuration(t, state.TripStart, utils.FloatToTime(700.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0) + +} + +func TestVehicleCacheList(t *testing.T) { + cache := services.GetVehicleCache() + + cache.Clear() + + signal := kafka_grpc.GRPC_CANSignal{ + Vin: "TESTVIN1", + Timestamp: 0.0, + Id: 816, + Name: "ESP_VehSpd", + Value: 792, + } + cache.UpdateSignal(&signal, 0x0) + + signal = kafka_grpc.GRPC_CANSignal{ + Vin: "TESTVIN2", + Timestamp: 0.0, + Id: 816, + Name: "ESP_VehSpd", + Value: 792, + } + cache.UpdateSignal(&signal, 0x0) + + signal = kafka_grpc.GRPC_CANSignal{ + Vin: "TESTVIN3", + Timestamp: 0.0, + Id: 816, + Name: "ESP_VehSpd", + Value: 792, + } + cache.UpdateSignal(&signal, 0x0) + + expectedVins := []string{"TESTVIN1", "TESTVIN2", "TESTVIN3"} + node := cache.StatesListHead + i := 0 + for node != nil { + assert.Equal(t, expectedVins[i], node.VIN) + node = node.Next + i++ + } + + node2 := cache.PopLeft() + + expectedVins = []string{"TESTVIN2", "TESTVIN3"} + node = cache.StatesListHead + i = 0 + for node != nil { + assert.Equal(t, expectedVins[i], node.VIN) + node = node.Next + i++ + } + + cache.ReinsertRight(node2) + + expectedVins = []string{"TESTVIN2", "TESTVIN3", "TESTVIN1"} + node = cache.StatesListHead + i = 0 + for node != nil { + assert.Equal(t, expectedVins[i], node.VIN) + node = node.Next + i++ + } + + cache.ReinsertRight(cache.Cache["TESTVIN3"]) + + expectedVins = []string{"TESTVIN2", "TESTVIN1", "TESTVIN3"} + node = cache.StatesListHead + i = 0 + for node != nil { + assert.Equal(t, expectedVins[i], node.VIN) + node = node.Next + i++ + } + +} diff --git a/services/jetfire/tests/integration_test.go b/services/jetfire/tests/integration_test.go new file mode 100644 index 0000000..5e6cf46 --- /dev/null +++ b/services/jetfire/tests/integration_test.go @@ -0,0 +1,151 @@ +package tests + +import ( + "context" + "fmt" + "github.com/fiskerinc/cloud-services/services/jetfire/server" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "os" + "testing" + "time" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/intel-go/fastjson" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" +) + +var batchDataSignal = []kafka_grpc.GRPC_CANSignal{} +var dataToPublish kafka_grpc.GRPC_CANSignalBatchPayload +var startTimestamp = time.Now().UTC() + +func TestIntegration(t *testing.T) { + + t.Skip() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + //cleaning up previous run + + conn, err := services.GetClickhouseConnection() + if err != nil { + logger.Error().Err(err) + } + + startTimestamp = time.Now().UTC() + + timestampString := fmt.Sprintf( + "%d-%d-%d %d:%d:%d", + startTimestamp.Year(), + startTimestamp.Month(), + startTimestamp.Day(), + startTimestamp.Hour(), + startTimestamp.Minute(), + startTimestamp.Second(), + ) + + println("cleaning up previous run") + conn.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DELETE WHERE VIN=='%s' AND Timestamp<'%s'", services.FEATURE_TABLE, testVIN, timestampString)) + conn.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DELETE WHERE VIN=='%s' AND Timestamp<'%s'", services.VEHICLE_SIGNAL_TABLE, testVIN, timestampString)) + + // conn.Ping(ctx) + + //initialization + cache := services.GetVehicleCache() + cache.Clear() + + readTestData() + producer, err := kafka.NewAsyncProducer(ctx) + assert.Nil(t, err) + // runJetfire(ctx) + + publishDuration := int64(10) + testDuration := time.Duration(publishDuration*2) * time.Second //batch inserts take some time to run. give it some time... + + // begin publishing kafka data. Update message timestamps first. + grpcData, err := proto.Marshal(&dataToPublish) + assert.Nil(t, err) + + err = producer.ProduceBinary(kafka.VehicleSignal, testVIN, grpcData, nil) + assert.Nil(t, err) + + //wait a while since kafka has to rebalance, wait for clickhouse inserts to trigger + time.Sleep(testDuration) + + //check clickhouse, count rows + println("querying clickhouse feature...") + query := fmt.Sprintf("SELECT VIN, Timestamp FROM %s WHERE VIN=='%s' AND Timestamp>='%s'", + services.FEATURE_TABLE, + testVIN, + timestampString, + ) + checkRows(query, 1, t) + + println("querying clickhouse vehicle_signal...") + query = fmt.Sprintf("SELECT VIN, Timestamp FROM %s WHERE VIN=='%s' AND Timestamp>='%s'", + services.VEHICLE_SIGNAL_TABLE, + testVIN, + timestampString, + ) + checkRows(query, 1000, t) +} + +func checkRows(query string, expected int, t *testing.T) { + conn, err := services.GetClickhouseConnection() + + if err != nil { + logger.Error().Err(err) + return + } + + fmt.Println(query) + rows, err := conn.Query(context.Background(), query) + assert.Nil(t, err) + + count := int(0) + defer rows.Close() + for rows.Next() { + count++ + } + assert.GreaterOrEqual(t, count, expected) +} + +func readTestData() { + //1176 messages long + jsonPath := "./test-batch-msg.json" + data, err := os.ReadFile(jsonPath) + if err != nil { + panic(err) + } + + fastjson.Unmarshal(data, &batchDataSignal) + + dataPtr := make([]*kafka_grpc.GRPC_CANSignal, len(batchDataSignal)) + offset := -1.0 + for i := range batchDataSignal { + dataPtr[i] = &batchDataSignal[i] + + // find min timestamp in batch data + if offset < 0 || offset > batchDataSignal[i].Timestamp { + offset = dataPtr[i].Timestamp + } + + dataPtr[i].Vin = testVIN // in case we need to chagne the test vin to follow proper pattern + } + for i := range batchDataSignal { + dataPtr[i].Timestamp += utils.TimeToFloat(startTimestamp) - offset + } + dataToPublish = kafka_grpc.GRPC_CANSignalBatchPayload{Data: &kafka_grpc.GRPC_CANSignalData{ + Cansignals: dataPtr, + }} +} + +func runJetfire(ctx context.Context) { + // first initialize and run jetfire application loops + services.ResetCacheVars() + go server.StartConsumer(ctx, kafka.VehicleSignal) +} diff --git a/services/jetfire/tests/message_test.go b/services/jetfire/tests/message_test.go new file mode 100644 index 0000000..18f1d6c --- /dev/null +++ b/services/jetfire/tests/message_test.go @@ -0,0 +1,39 @@ +package tests + +import ( + "fmt" + "github.com/fiskerinc/cloud-services/services/jetfire/handlers" + "github.com/fiskerinc/cloud-services/services/jetfire/services" + "github.com/fiskerinc/cloud-services/services/jetfire/utils" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/stretchr/testify/assert" +) + +func TestHandleSignalMessage(t *testing.T) { + cache := services.GetVehicleCache() + + ptrArray := make([]*kafka_grpc.GRPC_CANSignal, len(testCanSignalBatch)) + for i := range testCanSignalBatch { + ptrArray[i] = &testCanSignalBatch[i] + } + + services.ResetCacheVars() + + cache = services.GetVehicleCache() + cache.Clear() + + handlers.HandleSignalBatch(ptrArray, cache, nil) + + assert.Equal(t, len(cache.Cache), 1) + + // testing large timestamp gap; trigger new trip + state := cache.Cache[testVIN] + tripStartTime := utils.FloatToTime(1511.0) + assert.WithinDuration(t, state.TripStart, tripStartTime, 1e8) + assert.Equal(t, state.TripID, fmt.Sprintf("%s_%d", testVIN, 1511)) + assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1512.0), 1e8) + assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 45.0) + assert.Equal(t, state.StateValues["ESP_VehSpd"], 11.0) +} diff --git a/services/jetfire/tests/reset_test.go b/services/jetfire/tests/reset_test.go new file mode 100644 index 0000000..b50f0a2 --- /dev/null +++ b/services/jetfire/tests/reset_test.go @@ -0,0 +1,26 @@ +//go:build reset +// +build reset + +package tests + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/services/jetfire/handlers" + + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestResetSchema(t *testing.T) { + tests := []th.BasicHttpTest{ + { + Name: "Reset", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/reset", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: "", + }, + } + + th.RunBasicHttpTests(t, tests, handlers.ResetSchemaDefinitions) +} diff --git a/services/jetfire/tests/test-batch-msg.json b/services/jetfire/tests/test-batch-msg.json new file mode 100644 index 0000000..f79fccb --- /dev/null +++ b/services/jetfire/tests/test-batch-msg.json @@ -0,0 +1 @@ +[{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_151_CheckSum","value":190},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_151_AliveCounter","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtTqVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtRotDir","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtSpd","value":-1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtTq","value":-511},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtSpdSigVld","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_CrtMod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_Decoup_State","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_HVActvDchaSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_SlipCtrlActMode","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_AlrmLamp_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7753758,"id":337,"name":"MCU_R_HVChkEndFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775426,"id":586,"name":"BMS_24A_CheckSum","value":182},{"vin":"BENCHTEST1234567","timestamp":1668413114.775426,"id":586,"name":"BMS_24A_AliveCounter","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775426,"id":586,"name":"BMS_PwrBattChrgDchaCrt1","value":1.3999999999999773},{"vin":"BENCHTEST1234567","timestamp":1668413114.775426,"id":586,"name":"BMS_PwrBattChrgDchaCrt2","value":2.080000000000041},{"vin":"BENCHTEST1234567","timestamp":1668413114.775426,"id":586,"name":"BMS_PwrBattIntTotVoltV1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_1B6_CheckSum","value":174},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_1B6_AliveCounter","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_TpMnvrCancel","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_TpActDeactInp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_RemPwrOnOffReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_RemDrvgReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_RemChrgDchaGunUnlckCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_RmteDviceBattLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_BLE_Online","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_DviceRngeDist","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_DviceAliveCnt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_MnvreEnblInput","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_RapActDeactInp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_MnvrRemsumeKey","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_MnvrRtnStartKey","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_AuthUsr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_AlcInterlockReqPromt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7754378,"id":438,"name":"PKC_NFCReminderPromt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattOverChrgAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattUderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_CellUnderVoltFailFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_CellUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BattBalActv","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BattPack1OverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BattPack1UnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_DCChrgSoktOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComLostFlt_BOBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComLostFlt_CCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ACChrgSoktOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ACCrtFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_InsulChkFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattOverTAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattHLVIntlkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattExtShoCirc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattIntShoCirc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattTUnbal","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattTRiseFast","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattIntTotVoltChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattExtTotVoltChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattNegRlyAdh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPrecRlyAdh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPosRlyAdh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattNegRlyBreak","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPrecRlyBreak","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPosRlyBreak","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPrecRBreak","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattMSDBreak","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattExtTotVoltChkFltV2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BCUEEPROMReadWrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPosRlyDrvChFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattPrecRlyDrvChFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattInsulChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattNegRlyDrvChFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattCellVoltUnbal","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_EEPROMReadWrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattSysIntComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_InsulRLo","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_CopRowLooseFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubEEPROMReadWrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubCellVoltCollctnCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubModleVoltCollctnCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubTCollctnCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubActvBalChFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubPasBalChFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubOperModRespFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BattCellVoltUnbalAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComLostFlt_ECC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_SubComLostFlt_VBUBMS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PrecFailFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattCellOverVolt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_TotVoltOver","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_PwrBattIntTotVoltChkFltV1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_TotCrtChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComLostFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_BattReDrvHLVIntlkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComOverTiFlt_ECC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_LVPwrSplyFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775447,"id":880,"name":"BMS_ComOverTiFlt_BOBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_0x553_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_0x553_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_Splr","value":13},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_ACCrtPhase1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_ACCrtPhase2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_ACCrtPhase3","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_PhaseFreq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775457,"id":1363,"name":"OBC_EVChrgElectcLockStsFbSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_343_CheckSum","value":184},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_343_AliveCounter","value":13},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_RemReqFb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_APASwSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_FrntHoodLidSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SunroofAntipinchSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_FrntDrDoorLockSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_TrRelsSwtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_LockAllDoorCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_DrFrntDoorSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_PasFrntDoorSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_AntithftSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_CenLockSwtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_DoorUnlockSetFb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_RiReDoorSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_LeReDoorSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_LeFrntWinSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_RiFrntWinSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_LeReWinSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_RiReWinSt","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SunroofSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_FolwMeSetStsFb","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_DrvrDoorUnlckOutpCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_PassDoorUnlckOutpCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_LeDRLOutpCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_RiDRLOutpCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_ArmedClsWinSetSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_OffAutoUnlckSetSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_WarnVbrnSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SunroofPosnInfo","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SunroofOpenAr","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SteerWhlVbrnAlrmAvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_SunroofRunngSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775467,"id":835,"name":"BCM_MirrCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_345_CheckSum","value":200},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_345_AliveCounter","value":14},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_OutpValResl_HUD","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_SunshadeRunngSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_RiSolaValr_RLS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_LeSolaValr_RLS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_WindshdHum_RLS","value":40},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_WindshdT_RLS","value":27.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_MeasdVal_HUD","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.775476,"id":837,"name":"BCM_SunshadePosnInfo","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_110_CheckSum","value":232},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_110_AliveCounter","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotTarTqCmdVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_BoostCmd_F","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotTarSpdCmdVld","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotDampgFobdFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotTarSpdCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotTarTqCmd","value":-511},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_FrntMotModCmd","value":4},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775485,"id":256,"name":"VCU_100_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_111_CheckSum","value":214},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_111_AliveCounter","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotTarTqCmdVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_BoostCmd_R","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotTarSpdCmdVld","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotDampgFobdFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotTarSpdCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotTarTqCmd","value":-511},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_ReMotModCmd","value":4},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775493,"id":257,"name":"VCU_101_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_234_CheckSum","value":192},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_234_AliveCounter","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_RdyLamp","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_BrkLampCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_RemWakeUpEndFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_StgyGearSig","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_ShiftMisoper","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_GearSigVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_GearSig","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_ACCRdy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_ParkRdy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_APSPercVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_VehSt","value":30},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_APSPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_BrkSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_BrkSigVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_DrvModSigFb","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_DrvModShiftMisoper","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_SpclTerrainModEnaSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775502,"id":532,"name":"VCU_214_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_235_CheckSum","value":120},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_235_AliveCounter","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_DCPosRlyCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_DCNegRlyCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_DCCrt","value":-255},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_DCVolt","value":1.52},{"vin":"BENCHTEST1234567","timestamp":1668413114.775512,"id":565,"name":"OBC_DCChrgnRlyCrt","value":95.72000000000003},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_269_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_269_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_DCVoltCmd_OBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_DCCrtCmd_OBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_VehChrgDchgMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_DCRlyCtrlCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_ChrgSysOperCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_ChrgSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755198,"id":608,"name":"VCU_260_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_150_CheckSum","value":85},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_150_AliveCounter","value":4},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtTqVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtRotDir","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtSpd","value":-1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtTq","value":-511},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtSpdSigVld","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_CrtMod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_HVActvDchaSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_SlipCtrlActMode","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_AlrmLamp_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775529,"id":336,"name":"MCU_F_HVChkEndFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775539,"id":544,"name":"MCU_F_220_CheckSum","value":97},{"vin":"BENCHTEST1234567","timestamp":1668413114.775539,"id":544,"name":"MCU_F_220_AliveCounter","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.775539,"id":544,"name":"MCU_F_DCBusCrt","value":0.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775539,"id":544,"name":"MCU_F_DCBusVolt","value":306},{"vin":"BENCHTEST1234567","timestamp":1668413114.775555,"id":545,"name":"MCU_R_221_CheckSum","value":139},{"vin":"BENCHTEST1234567","timestamp":1668413114.775555,"id":545,"name":"MCU_R_221_AliveCounter","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.775555,"id":545,"name":"MCU_R_DCBusCrt","value":0.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775555,"id":545,"name":"MCU_R_DCBusVolt","value":306},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_382_CheckSum","value":67},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_382_AliveCounter","value":6},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_OTARdy_Fb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_CellMinT","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_CellDiffT","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_CellMaxT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_BattAvrgT","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_PwrBattHeatgMngDmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_ECCSysAllwDwnPwrCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_BattInlTDmd","value":25},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755718,"id":882,"name":"BMS_372_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775581,"id":1291,"name":"VCU_SOCHiWakeUpThd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775581,"id":1291,"name":"VCU_SOCHiWakeUpEnaCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775581,"id":1291,"name":"VCU_BattSOCHiWakeUpSetFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_0x550_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_0x550_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_PDU_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_Pwr","value":6},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_OutpMaxPwr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_V2L_RdySts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_V2L_ActvStsFb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_T","value":38},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_CrtSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_DCNegRlyAdhFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_DCPosRlyAdhFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_OperMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_PwrSplyHLVIntlkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_ACSCIntlkSigChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_DCFCIntlkSigChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_PTCIntlkSigChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_PwrBattIntlkSigChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_EASIntlkSigChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_OTARdy_Fb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_550_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_550_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_550_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77559,"id":1360,"name":"OBC_550_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_189_CheckSum","value":206},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_189_AliveCounter","value":9},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_PBtnSts1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_PBtnSts2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_PBtnVld","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_GearLvrCrtPosnInfo","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_GearleverFltSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7755978,"id":393,"name":"CIM_OTARdy_Fb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_215_CheckSum","value":195},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_215_AliveCounter","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattOverTFlt_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattOverCrtFlt_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattNegRlyAdh_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattPosRlyAdh_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_CellUnderrVoltFlt_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattCellOverVolt_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_BattInlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_BattOutlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattPrecRlyAdh_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_AlrmLamp_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_PwrBattLoadTotVoltV3_FS","value":391.72},{"vin":"BENCHTEST1234567","timestamp":1668413114.7756379,"id":533,"name":"BMS_FctSftyErrCod","value":151},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_236_CheckSum","value":4},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_236_AliveCounter","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_DCChrgDchaGunCnctnSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_OutpACWakeupSigSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_OutpFCWakeupSigSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_ACChrgDchaGunCnctnSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_VcuErrCtgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_VcuErrBitVect","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_VcuState","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775647,"id":566,"name":"VCU_PwrSplyEquipSts","value":4},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_105_CheckSum","value":225},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_105_AliveCounter","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_DmdTqFltFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_AllwMntnIndcrLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_AlrmLamp_FS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775722,"id":261,"name":"VCU_VehTqDistbnRat","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_225_CheckSum","value":29},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_225_AliveCounter","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_BusCrtEstim","value":0.6800000000000637},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_VehFltProcLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_OverMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_MotChkACCFltFbFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_VehTqFltSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_PwrAntitheftAuthentReqFlg","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_PwrModSwtReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_KL15Req","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775743,"id":549,"name":"VCU_VehMaxWhlTq","value":755},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_318_CheckSum","value":250},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_318_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_BrkPedlStsVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_BrkPedlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_VehSpdVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_SysActv","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_LampSwtOffIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_FltIndcn_EBD","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_FltIndcn_ABS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_MilgRollgCntr_ODO","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_VehSpd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_IninModSigIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_FltIndcn_TCS","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_CtrlSts_HDC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_AvlIndcn_HDC","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_NoBrkP","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775766,"id":792,"name":"ESP_RDAEnaFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_0x371_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_0x371_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_PCBT","value":28},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_Crt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_HVIntlkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_ComLost_ECC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_OverCrtFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_HVUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_HVOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_ChkBattOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_ChkBattUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_InlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_OutlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_InlOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_OutlOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_PCBOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_PCBTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775811,"id":881,"name":"WTC_B_PwrOutpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_0x374_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_0x374_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_PCBT","value":29},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_Crt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_HVIntlkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_ComLost_ECC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_OverCrtFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_HVUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_HVOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_ChkBattOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_ChkBattUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_InlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_OutlTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_InlOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_OutlOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_PCBOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_PCBTSnsrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758439,"id":884,"name":"WTC_H_PwrOutpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_InlT","value":-48},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_OutlT","value":28},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_CrtPwr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_CrtT","value":28},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_IninSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_HVPwrSplySts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_ActGear","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.775887,"id":886,"name":"WTC_B_Mod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_InpT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_OutpT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_CrtPwr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_CrtT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_IninSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_HVPwrSplySts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_ActGear","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.7758958,"id":889,"name":"WTC_H_Mod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_391_CheckSum","value":251},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_391_AliveCounter","value":13},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_VehInsulFltDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattSysFltDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattRmngCpSOC","value":95.60000000000001},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattAvlCp","value":245.33},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_CellMinTAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattThermRunawayAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattOverT","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_PwrBattInsulSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776015,"id":816,"name":"BMS_330_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_0x521_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_0x521_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_T","value":38},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_CrtSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_InpOverVoltSwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_InpUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_LVOutpOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_LVOutpUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_OverTTurnDwnLoadFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_InpCurOverHwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_IntFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_ExtFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_OutpShoCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_OverTShutDwnFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_StsAlrmFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_TAlrmFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7760248,"id":1313,"name":"DCDC_Utlzn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776032,"id":1440,"name":"VCU_DCChrgVoltCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776032,"id":1440,"name":"VCU_DCChrgCrtCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776032,"id":1440,"name":"VCU_DCChrgOutpVolt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776032,"id":1440,"name":"VCU_DCChrgOutpCrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT49","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT50","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT51","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT52","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT53","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT54","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT55","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.776041,"id":1750,"name":"BMS_CellT56","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_0x520_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_0x520_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_OutpCrt","value":20.900000000000002},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_InpVolt","value":391.86},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_InpCrt","value":0.7000000000000001},{"vin":"BENCHTEST1234567","timestamp":1668413114.7762308,"id":1312,"name":"DCDC_OutpVolt","value":13.4},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_OTA_ownCondchk","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_TerminalRemWakeupSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemSCModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemPwrBattOpenHeatgReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemPwrBattResvHeatgReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemInsdInfoDispEna","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemECCDefrstModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemECCResvDefrstReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemECCSetT","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemResvECCSetT","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemSOCMax","value":20},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_4GSigIND","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_Conn_Typ","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_TSPNotConn","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_EHUNotCONN","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_WIFIError","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_InnerBattError","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_VoltLow","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_VoltHigh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_4GLoss","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_RemSWH_Req","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_OTAInhbReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_5C4_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_5C4_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_5C4_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776278,"id":1476,"name":"TBOX_5C4_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_4F8_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_4F8_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_Sw_upd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_Flsflg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_RemFlsh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_FobdGWPartBUSUDSCom","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_RemChrgInsulFctReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_EHUSetTrvlMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_ResvACChrgOpenSts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_RemKL15PwrOnReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_SetChrgEndSOC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_PreCondTriggerReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_BlowLvlReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_ICC_UpdStrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_VehiMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_Crash_cfm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_SchedChrgnReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7763078,"id":1268,"name":"TBOX_4F4_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_333_CheckSum","value":244},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_333_AliveCounter","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_KL15_Off_Announcement","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_IG1Sts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_PwrMod","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_PwrModVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_BrkSwtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_IG1StsVld_CAPE","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_IG2StsVld_CAPE","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_KL30s_L","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_IG2Sts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.776566,"id":819,"name":"BCM_InteriorSwtSt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_PwrGridBreakFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_ACChrgDchaIndcrLampSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_PrplsnEPwrReq","value":-600},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_VCUWakeUpSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_DCChrgDchaIndcrLampSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_HVBattActPwr","value":-600},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_PreCdnTi1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7769148,"id":1364,"name":"VCU_PreCdnTi2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_357_CheckSum","value":60},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_357_AliveCounter","value":10},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_ChrgSts_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_MCUFltSum_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_BrkPedlSts_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_SOCLoWakeUpThd","value":70},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_SOCLoWakeUpEnaCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_DrvModDiRmn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_SpclTerrainModUsrRmn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_BattIntegtChrgEndFlg","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_BattIntegtChrgModPwrOnReq","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_BattIntegtChrgWakeUpSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_RemUgrdFltFbSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777132,"id":855,"name":"VCU_OTAInhbRdy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_IninSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_PwrSysHeatMngtSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_ACSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_AirClnSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_WindSpdSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_BackRowAirOutlModSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_DrvrTSetSts","value":17.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_PassTSetSts","value":17.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_HeatSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_AUTOSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_SYNCSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_InfoSysWrmColdDisp","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_CircSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_ParticleConcVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_OutdT","value":26.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_OutdTVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_MaxFrntDefrst","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_HeatMngtSysFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_HeatMngtFctLim","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_DrvrAirOutlMod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_PassAirOutlMod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_RemSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7771719,"id":883,"name":"ECC_OTAInhbRdy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_OnOff","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_ReOperPanSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_ReOperPanLockSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_BackRowAirOutlModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_VolInc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_VolDec","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_VolCtrl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_TempCtrlnc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_TempCtrDec","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_BlowCtrInc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_BlowCtrDec","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_AirOutlLeftBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_AirOutlRightBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_SeatHRearLeftBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777209,"id":897,"name":"RAC_SeatHRearRightBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_DrvPwrLimPerc","value":100},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_EgyFbPwrLimPerc","value":100},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_MemChrgRmnMilgThd","value":48},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_FltAlrmCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_VehMaxAlrmLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_BrkStrtReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_PerKmFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_TrvlModSetRes","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_SlopCalcnVal","value":0.10969999999998947},{"vin":"BENCHTEST1234567","timestamp":1668413114.777217,"id":1283,"name":"VCU_InsntEgyCnseHr","value":0.5400000000000063},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MCUFCooltFlow","value":4.1000000000000005},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MCURCooltFlow","value":4.4},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_LeOutlUpDwnMotActvSts","value":9},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_LeOutlLeRiMotActvSts","value":6},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_LeOutlDamprMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidLeOutlUpDwnMotActvSts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidLeOutlLeRiMotActvSts","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidLeOutlDamprMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidRiOutlUpDwnMotActvSts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidRiOutlLeRiMotActvSts","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_MidRiOutlDamprMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_RiOutlUpDwnMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_RiOutlLeRiMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777257,"id":888,"name":"ECC_RiOutlDamprMotActvSts","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MonrPwrBattThermRunawayAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_ShiftOperRmn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MCUFOverSpdAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MCUROverSpdAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MCUFSysOverTDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MCURSysOverTDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_VehSysFltLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_MotSysFltLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_RmnUsrClsECCDispCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_PPrkgFltLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_RmnUsrECCFctLmtDispCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_DrvgRngEPROMVal","value":577.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_DrvgMilg","value":577.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_DrvRngVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_EgyFlowLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777268,"id":1284,"name":"VCU_VirtEgyRecovInten","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_SubtolEgyCnse","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_InsntEgyCnse","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_AccueEgyCnse","value":17.32},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_EgyRecovForbnFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_DrvPwrLimIndcrLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_PPrkgSysFltIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_GearSysFltIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_SocLoRmn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_BattFltIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_ChrgIndcrLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_VehEgyFlow","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_ChrgDchaGunCnctnIndcrLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_InsntEgyCnseUnitSwtFlg","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773,"id":1285,"name":"VCU_ChrgGunStrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_TarPwr_WTC_H","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_TarPwr_WTC_B","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_TarSpdCmd_EAS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_EnaCmd_WTC_H","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_EnaCmd_EAS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_EnaCmd_WTC_B","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_TarGear_WTC_B","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777318,"id":1248,"name":"ECC_TarGear_WTC_H","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_OTAVehInhb_QM_Fb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_OTAVehCdnChk_QM_sts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_DrvAvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_CllsnSigSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_VehSts_GB","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_MCUOverTFlt_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_DrvrMotOverTFlt_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_SOCTooLoAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_SOCJumpAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_PwrBattPackMismatAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_SOCTooHiAlrm","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_DCDCEnaCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_PwrBattECCEnaCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_MstRlyCtrlCmd","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_DCDCOutpVoltCmd","value":13.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_CCCnctnVolt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_GearSig_GB","value":15},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_DrvrMotNr_GB","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773259,"id":1287,"name":"VCU_BattVolt","value":13.44},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_BMS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_MCU_F","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_DCDC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_BOBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_ABS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_EHU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_ICM","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_BCM","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_TBOX","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ComLostFlt_NBS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_PwrAntitheftAuthentOverT","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_PwrAntitheftAuthentKey","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_HVIntlkFlt_GB","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_PwrAntitheftAuthentNoLrng","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_CllsnSigHardWireCollctnFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_APSSigErr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_APSSigChkErr","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_APSPwrSplyFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ChkBattOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ChkBattUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_ChkDCDCFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_BCUBMSSelfChkTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_BCUBMSSelfChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_FMCUSelfChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_DchaCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_FMCUHVSelfChkTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_AdpvFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_DCDCOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_VehCtrlrRAMFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_VehCtrlrROMFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_VehCtrlrEEPROMFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_BrkPedlPwrSplyFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_BattVoltCollctnCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_MstRlyBreakFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_MstRlyInpPwrSplyShoCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_MstRlyInpGndShoCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_MstRlyInpBreakFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_BattDamageFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_RMCUSelfChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_RMCUHVSelfChkTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x120","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x150","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x254","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x264","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x268","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x189","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x261","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x319","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x332","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x1B0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x306","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7773628,"id":1289,"name":"VCU_E2EFlt_0x151","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_PwrBattHVChkTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_HVSysPrecTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_HVSysChkTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_PwrBattPosBreakStsTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_HVDchaStsTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_PartSaveStsTiOut","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_CellVoltLoHVPwrOffFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_EEPROMCellVoltLoHVPwrOffFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_VehBootLoaderFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_FobdHVPwrOnFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_HVPwrOffReq","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_VehRemSCModPwrOnReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_VehRemOperModPwrOnReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_DDmdTq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_TqLim","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_MotDampgFobdSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77737,"id":1290,"name":"VCU_EEPROMSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_DrvMotOutpPwrPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_APS2Volt","value":0.42},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_APS1Volt","value":0.86},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_SlipRate","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_DrvTqLimSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777379,"id":1292,"name":"VCU_EgyRecovTqLimSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_SysAllwMaxPwrCnse","value":363.7},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_SysAllwMaxFbPwr","value":78.58},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_ECCSysAllwMaxPwrCnse","value":17.5},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_ECCEgyRecovModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_ECCEnaCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_CrashDIRSigPWMPeriod","value":119},{"vin":"BENCHTEST1234567","timestamp":1668413114.777426,"id":1293,"name":"VCU_CrashDIRSigPWMPct","value":83},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_BMSLVWakeUpSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_ONWakeUpSig","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_FCWakeUpSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_SCWakeUpSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemWakeUpSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_NetWakeUpSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_LVWakeUpSts_PDU","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_VehEOLFctFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_LVWakeUpSts_MCU","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_PwrAntithefAllwtRdyFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_LVWakeUpSts_CCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemECCEndCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_EvChrgElectcLockCtrlCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemPwrBattHeatgEndCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemChrgEndCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_V2L_TrAvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemBattHeatgFailReason","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemECCEndReason","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_V2L_ChrgPortAvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemChrgEndReason","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_SchedChrgnStsFb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_V2V_Avl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_V2H_Avl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_V2G_Avl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_VehMod","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_VehCrtChrgEndSOC","value":100},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_CalSwVers","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemPwrBattPreheatgEndFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_VehOperMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_VehRemRefrshModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777451,"id":1297,"name":"VCU_RemDTCPwrOnReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_ActvModDiEna","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_ApplianceClsLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_RainSnsrSysFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_SolarSnsrSysFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_FogSnsrSysFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_LightSnsrHwFlt_RLS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_SolarSnsrHwFlt_RLS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7774758,"id":1310,"name":"BCM_GlassSurfaceOcclusion","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgElectcLockDrvFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACDCChrgIndcrLamp12VFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComChkFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComChkFlt_BMS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComLostFlt_ICC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComLostFlt_PEPS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgNTC1AndNTC2TFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComChkFlt_ESP","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComLostFlt_OBC","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_CrtSts","value":5},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgTAlrmFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgNTC1AndNTC2TFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_KL30OperFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgElectcLockDrvFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ComLostFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgDchaCCSigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgElectcUnlockFailFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgElectcLockFailFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgTAlrmFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ChrgCPSigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgNTC1InpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgNTC2InpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgNTC1InpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgNTC2InpFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DchgCPSigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_CC2SigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_CC2Volt","value":12},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_ACChrgIndcrLampDrvFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgIndcrLampDrvFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775,"id":1365,"name":"VCU_DCChrgASigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777524,"id":1366,"name":"VCU_CP_Amp","value":0.7000000000000001},{"vin":"BENCHTEST1234567","timestamp":1668413114.777524,"id":1366,"name":"VCU_CP_Frq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777524,"id":1366,"name":"VCU_CP_DutyRat","value":100},{"vin":"BENCHTEST1234567","timestamp":1668413114.777524,"id":1366,"name":"VCU_CC_R","value":4095},{"vin":"BENCHTEST1234567","timestamp":1668413114.777549,"id":1400,"name":"VCU_NotChrgModECCCnseEgy","value":3.2},{"vin":"BENCHTEST1234567","timestamp":1668413114.777549,"id":1400,"name":"VCU_NotChrgModVehDrvCnseEgy","value":43.6},{"vin":"BENCHTEST1234567","timestamp":1668413114.777549,"id":1400,"name":"VCU_NotChrgModBHMCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777549,"id":1400,"name":"VCU_NotChrgModExtDchaCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775738,"id":1401,"name":"VCU_NotChrgModAcsyCnseEgy","value":0.2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775738,"id":1401,"name":"VCU_NotChrgModVehTotCnseEgy","value":47},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775738,"id":1401,"name":"VCU_ChrgModECCCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775738,"id":1401,"name":"VCU_ChrgModVehChrgEgy","value":44.300000000000004},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775989,"id":1402,"name":"VCU_ChrgModBattHeatgMngCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775989,"id":1402,"name":"VCU_ChrgModAcsyCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775989,"id":1402,"name":"VCU_ChrgModChrgEffCnseEgy","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7775989,"id":1402,"name":"VCU_ChrgModVehTotChrgEgy","value":44.300000000000004},{"vin":"BENCHTEST1234567","timestamp":1668413114.777624,"id":1403,"name":"VCU_NotChrgModECCCnseEgyPerc","value":6.800000000000001},{"vin":"BENCHTEST1234567","timestamp":1668413114.777624,"id":1403,"name":"VCU_NotChrgModVehDrvCnseEgyPerc","value":92.80000000000001},{"vin":"BENCHTEST1234567","timestamp":1668413114.777624,"id":1403,"name":"VCU_NotChrgModBHMCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777624,"id":1403,"name":"VCU_NotChrgModExtDchaCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777624,"id":1403,"name":"VCU_NotChrgModAcsyCnseEgyPerc","value":0.4},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ChrgModECCCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ChrgModVehChrgEgyPerc","value":100},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ChrgModBHMCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ChrgModAcsyCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ChrgModChrgEffCnseEgyPerc","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777648,"id":1404,"name":"VCU_ActEgyCnsTyp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_InsdT","value":29},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_RemPwrCnsEstim","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_RemTSetSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_RemDefrstSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_BattFldLvlSnsrLamp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_ResvOperTi","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777673,"id":1410,"name":"ECC_ParticleConc","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_PwrAntitheftAuthentLrngSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_PwrAntitheftErrTyp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_FreestartMod","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_DataPartVers","value":94200001},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_CalSwVersM","value":9},{"vin":"BENCHTEST1234567","timestamp":1668413114.7776968,"id":1558,"name":"VCU_CalSwVersS","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_TarPwrReq_WTC_H","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_TarPwrReq_WTC_B","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_DchaT","value":-40},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_FourWayVlvOutlT","value":-40},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_TarGearReq_WTC_B","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777721,"id":1412,"name":"ECC_TarGearReq_WTC_H","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_E2PROMAPICallFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_E2PROMReadFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_E2PROMWrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_TMonFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_EVCANBusFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_CCANBusFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_BREAKCANBusFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ATIC232ComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ATIC280ComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ATIC239LowSideOutpComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ATIC239VotSplyMngComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ATIC239UnderOverVotFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ADCnvrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_SftyFctMonrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_SftyFctMonrMemFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_LVbattCtrlSigOpenFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_LVbattCtrlSigSTBFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_LVbattCtrlSigSTGFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_BMSWakeUpSigOpenFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_BMSWakeUpSigSTBFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_BMSWakeUpSigSTGFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_PDUWakeUpSigSTBFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_PDUWakeUpSigOpenFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_PDUWakeUpSigSTGFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_CCUCMUWakeUpSigOpenFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_CCUCMUWakeUpSigSTBFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_CCUCMUWakeUpSigSTGFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_MCUWakeUpSigOpenFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_MCUWakeUpSigSTBFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_MCUWakeUpSigSTGFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7777438,"id":1559,"name":"VCU_ComLostFlt_CCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_EvaprT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_LeBlowFaceAirOutlT","value":28},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_RiBlowFaceAirOutlT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_LeBlowFootAirOutlT","value":30},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_RiBlowFootAirOutlT","value":30},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_HPTSnsrT","value":-40},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_LPTSnsrT","value":28},{"vin":"BENCHTEST1234567","timestamp":1668413114.777865,"id":1282,"name":"ECC_DefrstT","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_355_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_355_AliveCounter","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_ReqZeroTqFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_MaxPwrGennTq","value":-56},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_MaxElecTq","value":-471},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_MOT_T","value":407},{"vin":"BENCHTEST1234567","timestamp":1668413114.777947,"id":853,"name":"MCU_F_IGBTT","value":295},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_354_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_354_AliveCounter","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_ReqZeroTqFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_CrtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_MaxPwrGennTq","value":-56},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_MaxElecTq","value":-471},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_MOT_T","value":407},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780168,"id":852,"name":"MCU_R_IGBTT","value":294},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_SecuStsChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_HVActvDchaFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_CtrlPanTChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_ComLostFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_E2EFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_I2TFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_OperModFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_CooltOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSampleCircFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSampleCircFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSampleCircFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_ThreePhaCrtChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_DCBusUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_DCBusOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PosnSnsrChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_DCBusVoltSampleCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_OverSpdFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTOverTFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTTChkCircFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTTChkCircFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTTChkCircFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_OverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_TChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_CtrlPanOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_LVPwrSplyUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_LVPwrSplyOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_FbTqStsChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSwOverCrtFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_TqCmdOverLimFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PwrSplyModFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTThreePhaTChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_RotCalFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IntComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IntTqChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTDesatFltTop","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTDesatFltBot","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSwOverCrtFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_PhaCrtSwOverCrtFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTOverTFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780418,"id":869,"name":"MCU_F_IGBTOverTFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_SecuStsChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_HVActvDchaFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_CtrlPanTChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_ComLostFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_E2EFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_I2TFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_OperModFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_CooltOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSampleCircFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSampleCircFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSampleCircFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_ThreePhaCrtChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_DCBusUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_DCBusOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PosnSnsrChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_DCBusVoltSampleCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_OverSpdFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTOverTFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTTChkCircFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTTChkCircFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTTChkCircFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_OverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_TChkCircFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_CtrlPanOverTFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_LVPwrSplyUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_LVPwrSplyOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_FbTqStsChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSwOverCrtFltU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_TqCmdOverLimFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PwrSplyModFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTThreePhaTChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_RotCalFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IntComFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IntTqChkFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTDesatFltTop","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTDesatFltBot","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSwOverCrtFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_PhaCrtSwOverCrtFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTOverTFltV","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7780669,"id":870,"name":"MCU_R_IGBTOverTFltW","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_FltProcLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_MaxAlrmLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_SysFltDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_SysFltAlrmCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_DrvrMotCrtSts_GB","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778092,"id":885,"name":"MCU_F_CooltEstimnT","value":40},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_FltProcLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_MaxAlrmLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_SysFltDisp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_SysFltAlrmCtrlSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_DrvrMotCrtSts_GB","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778117,"id":887,"name":"MCU_R_CooltEstimnT","value":39},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"EWP_H_Sts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"EWP_B_Sts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"EWP_FD_Sts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"EWP_RD_Sts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_ThreeWtrVlvSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_AvtvGrilleSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_CoolgFanSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_ParticleSnsrCrtSts","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_CoolgFanSpdRatSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_EXV_B_Sts","value":52},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_HeatPumpHeatgEXVSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_ActvGrilleAg","value":33},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_ThreeWtrVlvPerc","value":50},{"vin":"BENCHTEST1234567","timestamp":1668413114.778142,"id":1409,"name":"ECC_ParticleSnsrFltSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778168,"id":1588,"name":"VCU_ACChrgShttrSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778168,"id":1588,"name":"VCU_DCChrgShttrSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778168,"id":1588,"name":"VCU_SCPwrSplyEquipFactry","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778168,"id":1588,"name":"VCU_ACChrgCrtUpprLmt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7781749,"id":1616,"name":"VCU_PartVers","value":107171},{"vin":"BENCHTEST1234567","timestamp":1668413114.7781749,"id":1616,"name":"VCU_SwVersPatch","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.7781749,"id":1616,"name":"VCU_HwVers","value":41},{"vin":"BENCHTEST1234567","timestamp":1668413114.7781749,"id":1616,"name":"VCU_SwVersMain","value":9},{"vin":"BENCHTEST1234567","timestamp":1668413114.7781749,"id":1616,"name":"VCU_SwVersMinor","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_PwrGridAllwMaxPwrSplyCrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_ChrgLineAllwtCrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_DchaLineAllwCrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_DchaVehSts","value":6},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_DCChrgSoktT1","value":25},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_DCChrgSoktT2","value":25},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_ACChrgDchaSoktT1","value":24},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_6DA_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_6DA_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_6DA_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778184,"id":1754,"name":"VCU_6DA_SSecOC_MAC_Byte2","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT57","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT58","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT59","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT60","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT61","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT62","value":27},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT63","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.77826,"id":1751,"name":"BMS_CellT64","value":26},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_LeOutlUpDwnMotActvCmd","value":9},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_LeOutlLeRiMotActvCmd","value":6},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_LeOutlDamprMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidLeOutlUpDwnMotActvCmd","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidLeOutlLeRiMotActvCmd","value":7},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidLeOutlDamprMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidRiOutlUpDwnMotActvCmd","value":2},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidRiOutlLeRiMotActvCmd","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_MidRiOutlDamprMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_RiOutlUpDwnMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_RiOutlLeRiMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.7784839,"id":890,"name":"ICC_RiOutlDamprMotActvCmd","value":11},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_DrvrTSet","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_PassTSet","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_VSPVoiceTypSwt","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_ECCAUTOReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_DrvrSYNCReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_ACSwtReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_AirVolSet","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_ECCIntExtCircReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_AirClnSwtReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_MaxFrntDefrstSet","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_VSPCtrlCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavCtryCod","value":255},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavRoadTyp","value":15},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavSpdLimUnit","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavSpdLimSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavSpdLim","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_BlowWinBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778553,"id":1328,"name":"ICC_NavSpdLimExitDst","value":2550},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_MinChrgSoc_Setting","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_PwrBattDrvgHeatSwtSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_DrvrBlowFaceBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_PassBlowFacetBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_DrvrBlowFootBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_PassBlowFootBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_BackRowAirOutlModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_DchaBtnAvlFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_ECCSysSwtCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_DrvrBlowModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778563,"id":1332,"name":"ICC_PassBlowModReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778601,"id":864,"name":"BMS_PwrBattPosGNDInsulRrel","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778601,"id":864,"name":"BMS_PwrBattNegGNDInsulRrel","value":256},{"vin":"BENCHTEST1234567","timestamp":1668413114.778601,"id":864,"name":"BMS_PwrBattPosGNDInsulR","value":3000},{"vin":"BENCHTEST1234567","timestamp":1668413114.778601,"id":864,"name":"BMS_PwrBattNegGNDInsulR","value":3000},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_0x55A_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_0x55A_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_HVDCOutpOverCrtSwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_HVDCOutpOverCrtHwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ComLostFlt_BMS","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ACVoltOverVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ACVoltUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_HVDCOutpOverVoltHwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_HVDCOutpUnderVoltFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_HVDCOutpOverVoltSwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_InpCrtOverCrtHwSwFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_OverTTurnDwnPwrFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_OverTShutDwnFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ExtFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_IntFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ComLostFlt_VCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_ComLostFlt_CCU","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_12VPwrSplyVoltUnder","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.7786179,"id":1370,"name":"OBC_12VPwrSplyVoltOver","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_UsrSetChrgRmnMilg","value":48},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_EPedlSts","value":3},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_AccelMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_BoostMode","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_EgyCnseClrFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_UsrSetDchaEndDrvgMilg","value":50},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_VehiMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_EgyCnseStcClrFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_PollingFctOpenSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_KickFctOpenSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_UsrSetChrgGunAntithft","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_ChrgInsulFctReq","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_StopChrgBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_DrvModSig","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_SetACChrgLmtCrt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_SetTrvlMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_RegenLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_SetChrgEndSOC","value":127},{"vin":"BENCHTEST1234567","timestamp":1668413114.778647,"id":1552,"name":"ICC_StartChrgBtn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_TotMilgVld_ODO","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_TotMilg_ODO","value":3764.7000000000003},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_DispVehSpd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_DispVehSpdUnit","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_ADASSigFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_DispFlt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778657,"id":1329,"name":"ICC_WorkshopMode","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ACDchaStrtFailReason","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_AllwRemHeatgSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_FbRemHeatgOperSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ChrgEndSOCCrtCtrlFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_Lvl2ProtStrtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_TotOverVoltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_UnderVoltFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_OverTFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_OverVoltFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_CellIntShoFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_InsulFltLvl2ProtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_VehCfgExchgFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ReadEEFailFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_WrEEFailFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_FirstUseEEFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_SwUpdataFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_NewOldEESwtFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_FSCFillEndFlg","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_MaxAlrmLvl","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_UsrStopChrgReqSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_DchaIndcrLampCtrlCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_VehExtDchaSts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_S1SwtCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_DchaFltStsIndcn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_DchaUsrSetThd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_RemHeatgDmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_PwrBattHeatgTyp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_RemPwrBattPreheatgDmdTi","value":255},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ChrgInsulFctOpenSts","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ChrgEquipPwrNotEngh","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778884,"id":867,"name":"VCU_ACChrgStrtFailReason","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_0x559_CheckSum","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_0x559_AliveCounter","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_InpVoltRng","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_MinOutpVolt","value":220},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_MaxOutpVolt","value":455},{"vin":"BENCHTEST1234567","timestamp":1668413114.778913,"id":1369,"name":"OBC_DCChrggEvseInlVolt","value":15.36},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_SysAlrmVld","value":1},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_RemCtrlCmdFb","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_RemLockCmd_Key","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_KeyStsMod","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_KeyID","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_KeyInCarRmn","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_RemLockCmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_BatterySts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_KeyOutdCarPromt","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_DoorOpenRmd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_StrtFailTyp","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_LockKeyInCarInd","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_KeySts","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_37B_SSecOC_Fresh_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_37B_SSecOC_MAC_Byte0","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_37B_SSecOC_MAC_Byte1","value":0},{"vin":"BENCHTEST1234567","timestamp":1668413114.779032,"id":891,"name":"PKC_37B_SSecOC_MAC_Byte2","value":0}] \ No newline at end of file diff --git a/services/jetfire/utils/const.go b/services/jetfire/utils/const.go new file mode 100644 index 0000000..067d49b --- /dev/null +++ b/services/jetfire/utils/const.go @@ -0,0 +1,25 @@ +package utils + +import ( + "math" + "time" + + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + //other cache constants + TripTimeout = time.Duration(envtool.GetEnvInt64("JETFIRE_TRIP_TIMEOUT_MS", 600000)) * time.Millisecond + + EmptySignal = math.NaN() + + //bitwise flags for state updates. + //Additional flags can be added for additional sink tables later on + FeatureUpdateFlag uint = 0x1 + LatestUpdateFlag uint = 0x2 + + MaxVinLength = 20 + MaxTimestampLength = 12 + + FeatureVarsDefaults = "default-feature-vars.json" +) diff --git a/services/jetfire/utils/errors.go b/services/jetfire/utils/errors.go new file mode 100644 index 0000000..21dde8e --- /dev/null +++ b/services/jetfire/utils/errors.go @@ -0,0 +1,11 @@ +package utils + +import "errors" + +var ( + ErrInsertFullBlock = errors.New("appending row into full block") + ErrInsertWrongColumns = errors.New("appending buffer with incorrect number of columns") + ErrInvalidAppendType = errors.New("appending invalid type to block") + ErrNilClickhouseClient = errors.New("nil clickhouse client") + ErrNilKafkaConsumer = errors.New("nil kafka consumer") +) diff --git a/services/jetfire/utils/functions.go b/services/jetfire/utils/functions.go new file mode 100644 index 0000000..ab48e2a --- /dev/null +++ b/services/jetfire/utils/functions.go @@ -0,0 +1,63 @@ +package utils + +import ( + "math" + "os" + "time" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/intel-go/fastjson" + "github.com/sony/gobreaker" +) + +// Converts float64 (as decimal seconds in unix epoch) to Time.Time struct +func FloatToTime(float float64) time.Time { + s := int64(math.Floor(float)) + ns := int64((float - math.Floor(float)) * 1e9) + return time.Unix( + s, + ns, + ) +} + +func TimeToFloat(timestamp time.Time) float64 { + return float64(timestamp.UnixNano()) / 1e9 +} + +func FixFloatTimestampScale(float float64) float64 { + for float > 9999999999 { + float /= 1000 + } + return float +} + +// marchTimer increments the value at timer until newTime has been reached. +// This is used to try to maintain consistent downsample and insertion periods. +func MarchTimer(timer *time.Time, newTime *time.Time, delay time.Duration) { + for timer.Before(*newTime) { + *timer = timer.Add(delay) + } +} + +func ReadVarListFromFile(varsFile string) []string { + data, err := os.ReadFile(varsFile) + if err != nil { + // when running tests, pwd is in the wrong directory to find the default json files. + os.Chdir("..") + data, err = os.ReadFile(varsFile) + if err != nil { + panic(err) + } + } + + var result []string + err = fastjson.Unmarshal(data, &result) + if err != nil { + panic(err) + } + return result +} + +func BreakerStateChange(name string, from gobreaker.State, to gobreaker.State) { + logger.Warn().Stack().Msgf("%s breaker change from %d to %d", name, from, to) +} diff --git a/services/jetfire/utils/logging.go b/services/jetfire/utils/logging.go new file mode 100644 index 0000000..060930b --- /dev/null +++ b/services/jetfire/utils/logging.go @@ -0,0 +1,55 @@ +package utils + +import ( + "time" + + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/rs/zerolog" +) + +var ( + outOfOrderSignalMap = make(map[string]uint) + outOfOrderVINMap = make(map[string]uint) + outOfOrderCount uint = 0 + outOfOrderLogDelay = time.Hour + outOfOrderTime = time.Now().UTC() +) + +// This function is for aggregating and logging out of order incoming messages +func LogOutOfOrderMsg(signal string, VIN string) { + _, ok := outOfOrderSignalMap[signal] + if !ok { + outOfOrderSignalMap[signal] = 0 + } + outOfOrderSignalMap[signal] += 1 + + _, ok = outOfOrderVINMap[VIN] + if !ok { + outOfOrderVINMap[VIN] = 0 + } + outOfOrderVINMap[VIN] += 1 + outOfOrderCount += 1 + + if time.Since(outOfOrderTime) > outOfOrderLogDelay { + + signalDict := zerolog.Dict() + for k, v := range outOfOrderSignalMap { + signalDict.Uint(k, v) + } + vinDict := zerolog.Dict() + for k, v := range outOfOrderVINMap { + vinDict.Uint(k, v) + } + + logger.Warn().Dict( + "Signals", signalDict, + ).Dict( + "VINs", vinDict, + ).Msgf("Received Out of Order Data! %d out-of-order messages", outOfOrderCount) + + outOfOrderCount = 0 + outOfOrderTime = time.Now().UTC() + clear(outOfOrderSignalMap) + clear(outOfOrderVINMap) + } +} diff --git a/services/optimus/Dockerfile b/services/optimus/Dockerfile new file mode 100644 index 0000000..9bea8f7 --- /dev/null +++ b/services/optimus/Dockerfile @@ -0,0 +1,25 @@ +ARG BASE_IMAGE=cloud_base_go +FROM ${BASE_IMAGE} as builder-go + +WORKDIR /build/optimus +COPY ./optimus/go.mod ./optimus/go.sum ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go mod download + +COPY ./optimus ./ +RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \ + && go build -v fiskerinc.com/modules/dbc/fm29_frsd0 && 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/optimus/optimus . + +ENV LOG_CONFIG=log_config +EXPOSE 8077 + +CMD ./optimus diff --git a/services/optimus/controllers/health_check.go b/services/optimus/controllers/health_check.go new file mode 100644 index 0000000..1a1aff8 --- /dev/null +++ b/services/optimus/controllers/health_check.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "time" + + "github.com/fiskerinc/cloud-services/services/optimus/services" + + "github.com/fiskerinc/cloud-services/pkg/health" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" +) + +var mismatchTypeError = errors.New("mismatch type error") + +func HealthCheck() { + server := health.HealthCheckServer{} + err := server.Serve([]health.Config{ + { + Name: "kafka", + Check: health.NewKafkaCheck(getKafkaConsumer), + Timeout: time.Second * 1, + Vital: true, + }, + }) + if err != nil { + logger.Error().Err(err).Send() + } +} + +func getKafkaConsumer() (health.KafkaConnCheckInterface, error) { + client, err := services.GetKafkaConsumer() + if err != nil { + return nil, err + } + + conn, ok := client.(health.KafkaConnCheckInterface) + if !ok { + return nil, errors.WithStack(mismatchTypeError) + } + + return conn, nil +} diff --git a/services/optimus/go.mod b/services/optimus/go.mod new file mode 100644 index 0000000..e1c2ff1 --- /dev/null +++ b/services/optimus/go.mod @@ -0,0 +1,132 @@ +module github.com/fiskerinc/cloud-services/services/optimus + +go 1.25 + +toolchain go1.25.0 + +require github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000 + +require ( + github.com/julienschmidt/httprouter v1.3.0 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 + google.golang.org/protobuf v1.36.1 +) + +require ( + github.com/ClickHouse/ch-go v0.58.2 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.6.0 // 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/cespare/xxhash/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-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/pg/v10 v10.11.1 // 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/gomodule/redigo v1.8.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/schema v1.2.0 // 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/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/inflection v1.0.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-isatty v0.0.20 // 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/swaggo/swag v1.8.8 // 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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.mongodb.org/mongo-driver v1.14.0 // 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/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 + gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 // 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 +) diff --git a/services/optimus/go.sum b/services/optimus/go.sum new file mode 100644 index 0000000..c9f73cd --- /dev/null +++ b/services/optimus/go.sum @@ -0,0 +1,513 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= +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/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/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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/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-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-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw= +github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= +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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.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/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.5/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +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/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/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/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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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.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.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/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/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-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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +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/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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +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/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/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/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/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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/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/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.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.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-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-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-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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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-20190313153728-d0100b6bd8b3/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-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +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-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/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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-20191120155948-bd437916bb0e/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-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-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.3.0/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.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.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-20200130002326-2f3ba24bd6e7/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.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= +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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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= diff --git a/services/optimus/handlers/batch.go b/services/optimus/handlers/batch.go new file mode 100644 index 0000000..7e9a6f7 --- /dev/null +++ b/services/optimus/handlers/batch.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "github.com/fiskerinc/cloud-services/services/optimus/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/pkg/errors" +) + +func ProduceSignalsFromMessageBatch(vin string, payload *kafka_grpc.GRPC_BatchPayload) ([]common.CANSignal, error) { + var batch []common.CANSignal + if payload == nil { + return nil, errors.New("payload is nil") + } + if payload.Data == nil { + return nil, errors.New("payload.Data is nil") + } + // TODO Where to report epoch_usec, dropped, and filtered stats + dbcVersion := payload.Version + dbc, err := services.GetDBCCollection().Get(dbcVersion) + if err != nil { + return nil, err + } + // r := services.RedisClientPool().GetFromPool() + // defer r.Close() + + clickClient, err := services.GetClickhouseClient() + if err != nil { + return nil, err + } + + filters := services.GetVehicleMessageFilters().GetFiltersForVehicle(clickClient, vin) + for _, msg := range payload.Data.Frames { + if err := validator.ValidateStruct(msg); err != nil { + logger.Warn().Str("id", vin).Err(err).Msgf("%+v", msg) + continue + } + + if !filters.AllowMessage(int(msg.ID), int(msg.GetEpoch())) { + continue + } + + signals, err := dbc.GenerateCANSignals(vin, msg) + if err == nil && len(signals) > 0 { + batch = append(batch, signals...) + } else if err != nil { + logger.At(logger.Warn(), vin, "dbc").Err(err).Send() + } + } + + return batch, nil +} diff --git a/services/optimus/handlers/batch_test.go b/services/optimus/handlers/batch_test.go new file mode 100644 index 0000000..4534bcc --- /dev/null +++ b/services/optimus/handlers/batch_test.go @@ -0,0 +1,211 @@ +package handlers_test + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/fiskerinc/cloud-services/services/optimus/handlers" + + "github.com/fiskerinc/cloud-services/pkg/common" + fm29 "github.com/fiskerinc/cloud-services/pkg/dbc/fm29_frsd0" + fm390 "github.com/fiskerinc/cloud-services/pkg/dbc/fm29_frsd390" + dbc "github.com/fiskerinc/cloud-services/pkg/dbc/models" + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + rutils "github.com/fiskerinc/cloud-services/pkg/redis/redisutils" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +var produceSignalsCorrectPayload = kafka_grpc.GRPC_BatchPayload{ + Handler: "canbus", + Data: &kafka_grpc.GRPC_CANData{ + EpochUsec: 1653255445, + Dropped: 10, + Filtered: 20, + Frames: []*kafka_grpc.GRPC_CANFrame{ + {ID: 5, Value: []byte("AAAGAB6cCBY=")}, + {ID: 624, Value: []byte("AAAGAB6cCBY=")}, + {ID: 1290, Value: []byte("CACAAIAAAAA=")}, + {ID: 1287, Value: []byte("ACBAAAoANLI=")}, + {ID: 1311, Value: []byte("afaA9wBkkgA=")}, + {ID: 1317, Value: []byte("MAAwIAAAAAA=")}, + {ID: 1318, Value: []byte("D9M1YAF78aA=")}, + {ID: 5, Value: []byte("AAAGAB6cCBY=")}, + }, + }, + Version: fm29.Hash, +} + +var produceInvalidSignalsPayload = kafka_grpc.GRPC_BatchPayload{ + Handler: "canbus", + Data: &kafka_grpc.GRPC_CANData{ + EpochUsec: 1653255445, + Dropped: 10, + Filtered: 20, + Frames: []*kafka_grpc.GRPC_CANFrame{ + {ID: 1169, Value: []byte("MDAwMDgwMDA=")}, + {ID: 576, Value: []byte("3wBMc0yCAAA=")}, + {ID: 624, Value: []byte("MDAwMDA2MDA=")}, + {ID: 640, Value: []byte("MDAwMDI4RTY=")}, + {ID: 1168, Value: []byte("MDAwMDRFRTg=")}, + {ID: 1169, Value: []byte("MDAwMDgwMDA=")}, + {ID: 576, Value: []byte("3wBMc0yCAAA=")}, + {ID: 624, Value: []byte("MDAwMDA2MDA=")}, + {ID: 640, Value: []byte("MDAwMDI4RTY=")}, + {ID: 1168, Value: []byte("MDAwMDRFRTg=")}, + }, + }, + Version: fm390.Hash, +} + +var produceOutOfRangeSignalsPayload = kafka_grpc.GRPC_BatchPayload{ + Handler: "canbus", + Data: &kafka_grpc.GRPC_CANData{ + EpochUsec: 1653255445, + Dropped: 10, + Filtered: 20, + Frames: []*kafka_grpc.GRPC_CANFrame{ + {ID: 1073741824, Value: []byte("MDAwMDgwMDA=")}, + {ID: 1073756897, Value: []byte("3wBMc0yCAAA=")}, + {ID: 5242880, Value: []byte("MDAwMDA2MDA=")}, + {ID: 308346880, Value: []byte("MDAwMDI4RTY=")}, + {ID: 1188096, Value: []byte("MDAwMDRFRTg=")}, + {ID: -318242816, Value: []byte("MDAwMDgwMDA=")}, + {ID: 1073741824, Value: []byte("MDAwMDgwMDA=")}, + {ID: 1073756897, Value: []byte("3wBMc0yCAAA=")}, + {ID: 5242880, Value: []byte("MDAwMDA2MDA=")}, + {ID: 308346880, Value: []byte("MDAwMDI4RTY=")}, + {ID: 1188096, Value: []byte("MDAwMDRFRTg=")}, + {ID: -318242816, Value: []byte("MDAwMDgwMDA=")}, + }, + }, + Version: fm390.Hash, +} + +var produceSignalsAllValidPayload = kafka_grpc.GRPC_BatchPayload{ + Handler: "canbus", + Data: &kafka_grpc.GRPC_CANData{ + EpochUsec: 1653255445, + Dropped: 10, + Filtered: 20, + Frames: []*kafka_grpc.GRPC_CANFrame{ + {ID: 1290, Value: []byte("CACAAIAAAAA=")}, + {ID: 1287, Value: []byte("ACBAAAoANLI=")}, + {ID: 1317, Value: []byte("MAAwIAAAAAA=")}, + {ID: 1318, Value: []byte("D9M1YAF78aA=")}, + }, + }, + Version: fm29.Hash, +} + +var correctExpBatch = []common.CANSignal{ + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_PwrBattHVChkTiOut", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_HVSysPrecTiOut", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_HVSysChkTiOut", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_PwrBattPosBreakStsTiOut", Value: 1, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_HVDchaStsTiOut", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_PartSaveStsTiOut", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_CellVoltLoHVPwrOffFlg", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_EEPROMCellVoltLoHVPwrOffFlg", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_VehBootLoaderFlg", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_FobdHVPwrOnFlg", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_HVPwrOffReq", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_VehRemSCModPwrOnReq", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_VehRemOperModPwrOnReq", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_DDmdTq", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_TqLim", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_MotDampgFobdSts", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1290, Name: "VCU_EEPROMSts", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_OTAVehInhb_QM_Fb", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_OTAVehCdnChk_QM_sts", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_DrvAvl", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_CllsnSigSts", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_VehSts_GB", Value: 2, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_MCUOverTFlt_GB", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_DrvrMotOverTFlt_GB", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_SOCTooLoAlrm", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_SOCJumpAlrm", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_PwrBattPackMismatAlrm", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_SOCTooHiAlrm", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_DCDCEnaCmd", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_PwrBattECCEnaCmd", Value: 1, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_MstRlyCtrlCmd", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_DCDCOutpVoltCmd", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_CCCnctnVolt", Value: 1, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_GearSig_GB", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_DrvrMotNr_GB", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1287, Name: "VCU_BattVolt", Value: 13.49, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1317, Name: "TBOX_525_CheckSum", Value: 48, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1317, Name: "TBOX_525_AliveCounter", Value: 0, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1317, Name: "TBOX_GPSHei", Value: -115, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1318, Name: "TBOX_GPSLongi", Value: 85.5, Description: ""}, + {VIN: "TESTVIN123", Timestamp: 0, ID: 1318, Name: "TBOX_GPSLati", Value: -65.1, Description: ""}, +} + +func TestProduceSignalsFromMessageBatch(t *testing.T) { + invalidVersionPayload := produceSignalsCorrectPayload + invalidVersionPayload.Version = "12" + + tests := map[string]struct { + payload *kafka_grpc.GRPC_BatchPayload + cacheSetMock rutils.CacheSetMock + batchError error + expBatch []common.CANSignal + expError error + }{ + "correct": { + payload: &produceSignalsCorrectPayload, + expBatch: correctExpBatch, + }, + "all valid": { + payload: &produceSignalsAllValidPayload, + expBatch: correctExpBatch, + }, + "invalid_payload": { + payload: nil, + expError: errors.WithStack(errors.New("payload is nil")), + }, + "invalid_version": { + payload: &invalidVersionPayload, + expError: errors.New("DBC 12 does not exists"), + }, + "invalid_signals": { + payload: &produceInvalidSignalsPayload, + }, + "out_of_range_signals": { + payload: &produceOutOfRangeSignalsPayload, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + dbc.SetInvalidCANIDCache(dbc.NewInvalidCANIDCache()) + batch, err := handlers.ProduceSignalsFromMessageBatch("TESTVIN123", tt.payload) + if err != nil && tt.expError != nil { + assert.Equal(t, tt.expError.Error(), err.Error()) + return + } + + assert.Equal(t, tt.expError, err) + assert.Equal(t, tt.expBatch, batch) + }) + } +} + +func TestGenB64(t *testing.T) { + t.Skip() + + // example of generating binary CAN signal data + + msg := fm29.NewESP_0x114() + msg.SetESP_ActvSig_DTC(true) + + data := msg.Frame().Data + b64 := base64.StdEncoding.EncodeToString([]byte(data[:])) + fmt.Println(msg.Descriptor().ID) + fmt.Println(b64) + + t.Error("done") +} diff --git a/services/optimus/handlers/reset.go b/services/optimus/handlers/reset.go new file mode 100644 index 0000000..2642536 --- /dev/null +++ b/services/optimus/handlers/reset.go @@ -0,0 +1,14 @@ +package handlers + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/services/optimus/services" + + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +func ResetFilters(w http.ResponseWriter, r *http.Request) { + logger.Info().Msg("resetting filters") + services.ResetVehicleMessageFilters() +} diff --git a/services/optimus/handlers/reset_test.go b/services/optimus/handlers/reset_test.go new file mode 100644 index 0000000..9a615e2 --- /dev/null +++ b/services/optimus/handlers/reset_test.go @@ -0,0 +1,23 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/services/optimus/handlers" + + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestResetFilters(t *testing.T) { + tests := []th.BasicHttpTest{ + { + Name: "Reset", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/reset", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: "", + }, + } + + th.RunBasicHttpTests(t, tests, handlers.ResetFilters) +} diff --git a/services/optimus/main.go b/services/optimus/main.go new file mode 100644 index 0000000..ccf3c27 --- /dev/null +++ b/services/optimus/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + + "github.com/fiskerinc/cloud-services/services/optimus/controllers" + "github.com/fiskerinc/cloud-services/services/optimus/server" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/tracer" + "github.com/fiskerinc/cloud-services/pkg/utils/app" +) + +var ( + SERVICE_NAME = "optimus" +) + +func init() { + app.Setup(SERVICE_NAME, cleanup) +} + +func main() { + defer cleanup() + tracer.Start() + defer tracer.Stop() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go controllers.HealthCheck() + go server.StartHTTPServer() + go server.StartConsumer(ctx, []string{kafka.VehicleData}) + select {} +} + +func cleanup() { + logger.Close() +} diff --git a/services/optimus/server/server_consumer.go b/services/optimus/server/server_consumer.go new file mode 100644 index 0000000..4db2400 --- /dev/null +++ b/services/optimus/server/server_consumer.go @@ -0,0 +1,96 @@ +package server + +import ( + "context" + "runtime" + "time" + + "github.com/fiskerinc/cloud-services/services/optimus/handlers" + "github.com/fiskerinc/cloud-services/services/optimus/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +var freeze = envtool.GetEnvInt("OPTIMUS_KAFKA_BATCH_SIZE_FREEZE", 200000) +var OPTIMUS_CONCURRENCY = envtool.GetEnvInt("OPTIMUS_CONCURRENCY", 20) + +// StartConsumer runs consumer and puts events into a channel for router +func StartConsumer(ctx context.Context, topics []string) { + defer func() { + if err := recover(); err != nil { + logger.Error().Msgf("PanicConsumer %v", err) + } + }() + + events := make(chan *kafka.Message) + go routeEvents(ctx, events) + + logger.Info().Msgf("consumer intialized for topic: %v", topics) + consumer, err := services.GetKafkaConsumer() + if err != nil { + panic(errors.WithStack(err)) + } + for { + err = consumer.ConsumeToChannel(topics, events) + if loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) { + time.Sleep(500 * time.Millisecond) + } else { + break + } + } +} + +func routeEvents(ctx context.Context, events chan *kafka.Message) { + for event := range events { + start := time.Now() + canData := kafka_grpc.GRPC_BatchPayload{} + vin := string(event.Key) + err := proto.Unmarshal(event.Value, &canData) + if loggerdataresp.BadDataError(errors.WithStack(err)) { + continue + } + batch, err := handlers.ProduceSignalsFromMessageBatch(vin, &canData) + if loggerdataresp.BadDataError(err) || len(batch) == 0 { + log(start, vin) + continue + } + + prod, err := services.GetKafkaProducer() + if loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) { + continue + } + + if prod.Len() > freeze { + prod.Flush(int(time.Second.Milliseconds()) * 5) + } + + cansignal := common.CANSignalBatchPayload{} + grpcCanSignal := cansignal.ToGrpc(batch) + grpcData, err := proto.Marshal(grpcCanSignal) + + if err != nil { + logger.Error().Str("id", vin).Err(err).Send() + continue + } + + go func(prodVal kafka.ProducerInterface, vinVal string, batchVal []byte) { + err = prodVal.ProduceBinary(kafka.VehicleSignal, vinVal, batchVal, nil) + loggerdataresp.BadDataError(err) + }(prod, vin, grpcData) + + log(start, vin) + } + +} + +func log(start time.Time, vin string) { + finish := time.Now() + logger.Debug().Str("id", vin).Msgf("vehicle batch data time elapsed %v, goroutines count: %v", finish.Sub(start), runtime.NumGoroutine()) +} diff --git a/services/optimus/server/server_http.go b/services/optimus/server/server_http.go new file mode 100644 index 0000000..50195b8 --- /dev/null +++ b/services/optimus/server/server_http.go @@ -0,0 +1,23 @@ +package server + +import ( + "net/http" + "github.com/fiskerinc/cloud-services/services/optimus/handlers" + + h "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/julienschmidt/httprouter" +) + +const port string = ":8077" + +func StartHTTPServer() { + router := httprouter.New() + router.PanicHandler = h.HttpRouterPanicHandler + addHandler(router, http.MethodGet, "/reset", handlers.ResetFilters) + logger.Fatal().AnErr("http.ListenAndServe", http.ListenAndServe(port, router)).Send() +} + +func addHandler(router *httprouter.Router, method string, path string, handler http.HandlerFunc) { + router.HandlerFunc(method, h.HttpRouterHandleBaseURL(path), handler) +} diff --git a/services/optimus/services/clickhouse.go b/services/optimus/services/clickhouse.go new file mode 100644 index 0000000..0accc5a --- /dev/null +++ b/services/optimus/services/clickhouse.go @@ -0,0 +1,35 @@ +package services + +import ( + "github.com/pkg/errors" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/clickhouse" +) + +var ( + client clickhouse.ClientInterface + clickLock sync.Mutex +) + +// GetClickhouseClient returns singleton instance of clickhouse client +func GetClickhouseClient() (clickhouse.ClientInterface, error) { + if client != nil { + return client, nil + } + + clickLock.Lock() + defer clickLock.Unlock() + + var err error + if client == nil { + conn, err := clickhouse.NewConn() + if err != nil { + return nil, errors.WithStack(err) + } + + client, err = clickhouse.NewClient(conn) + } + + return client, err +} diff --git a/services/optimus/services/dbc.go b/services/optimus/services/dbc.go new file mode 100644 index 0000000..9653c2a --- /dev/null +++ b/services/optimus/services/dbc.go @@ -0,0 +1,19 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/dbc" + "github.com/fiskerinc/cloud-services/pkg/dbc/models" +) + +var collection models.DBCCollectionInterface +var collectionOnce sync.Once + +// GetDBCCollection returns singleton instance of collection of DBCs +func GetDBCCollection() models.DBCCollectionInterface { + collectionOnce.Do(func() { + collection = dbc.NewDBCCollection() + }) + return collection +} diff --git a/services/optimus/services/filters.go b/services/optimus/services/filters.go new file mode 100644 index 0000000..d775a79 --- /dev/null +++ b/services/optimus/services/filters.go @@ -0,0 +1,39 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/services/optimus/utils" + + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var filters *utils.VehicleMessageFilters +var filtersOnce sync.Once + +// GetKafkaConsumer returns singleton instance of kafka consumer +func GetVehicleMessageFilters() *utils.VehicleMessageFilters { + filtersOnce.Do(func() { + ResetVehicleMessageFilters() + }) + + return filters +} + +func ResetVehicleMessageFilters() { + filters = utils.NewVehicleMessageFilters() + + c, err := GetClickhouseClient() + if err != nil { + logger.Error().Err(err).Send() + return + } + + defaults, err := c.RetrieveDefaultFilters() + if err != nil { + logger.Error().Err(err).Send() + return + } + + filters.SetDefaultFilters(defaults) +} diff --git a/services/optimus/services/kafka.go b/services/optimus/services/kafka.go new file mode 100644 index 0000000..6d42c4a --- /dev/null +++ b/services/optimus/services/kafka.go @@ -0,0 +1,53 @@ +package services + +import ( + "context" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +const serviceName = "optimus" + +var consumer kafka.ConsumerInterface +var consumerOnce sync.Once + +// GetKafkaConsumer returns singleton instance of kafka consumer +func GetKafkaConsumer() (kafka.ConsumerInterface, error) { + var err error + + consumerOnce.Do(func() { + consumer, err = kafka.NewConsumer(serviceName) + if err != nil { + logger.Error().Err(err).Send() + } + }) + if err != nil { + return nil, err + } + + return consumer, nil +} + +var producer kafka.ProducerInterface +var producerOnce sync.Once + +// GetKafkaProducer returns singleton instance of kafka producer +func GetKafkaProducer() (kafka.ProducerInterface, error) { + var err error + + producerOnce.Do(func() { + ctx := context.Background() + producer, err = kafka.NewAsyncProducer(ctx) + if err != nil { + logger.Error().Err(err).Send() + } + go producer.ReadEvents() + }) + if err != nil { + return nil, err + } + + return producer, nil +} diff --git a/services/optimus/services/redis.go b/services/optimus/services/redis.go new file mode 100644 index 0000000..3caa69d --- /dev/null +++ b/services/optimus/services/redis.go @@ -0,0 +1,37 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/redis" + rm "github.com/fiskerinc/cloud-services/pkg/redis/tester" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var ( + clientPoolOnce sync.Once + clientPool redis.ClientPoolInterface + FilteringON = envtool.GetEnv("OPTIMUS_FILTER_WARNINGS", "1") +) + +func RedisClientPool() redis.ClientPoolInterface { + clientPoolOnce.Do(func() { + if clientPool != nil { + return + } + + if FilteringON == "0" { + mockRedis := rm.MockRedis{} + clientPool = rm.NewMockClientPool(&mockRedis) + return + } + + clientPool = redis.NewClientPool() + }) + + return clientPool +} + +func SetRedisClientPool(cp redis.ClientPoolInterface) { + clientPool = cp +} diff --git a/services/optimus/utils/filters.go b/services/optimus/utils/filters.go new file mode 100644 index 0000000..64cd24f --- /dev/null +++ b/services/optimus/utils/filters.go @@ -0,0 +1,130 @@ +package utils + +import ( + "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +// NewVehicleMessageFilters constructs a new VehicleMessageFilters +// should only be called once during lifetime of a service +func NewVehicleMessageFilters() *VehicleMessageFilters { + return &VehicleMessageFilters{ + filters: make(map[string]*MessageFilters), + } +} + +// VehicleMessageFilters maps vehicle vins to their set of filters +type VehicleMessageFilters struct { + filters map[string]*MessageFilters +} + +// NewFiltersForVehicle adds a new vehicle to the sets of filters +func (v *VehicleMessageFilters) NewFiltersForVehicle(client clickhouse.ClientInterface, vin string) *MessageFilters { + var f *MessageFilters + + d, ok := v.filters[DEFAULT] + if ok { + f = NewMessageFiltersFromDefault(d) + } else { + f = NewMessageFilters() + } + + cFilters, err := client.RetrieveFiltersForVehicle(vin) + if err != nil { + logger.Warn().Err(err).Send() + } + + f.PopulateFromClickhouse(cFilters) + v.filters[vin] = f + return f +} + +// GetFiltersForVehicle obtains filters for a vehicle +func (v *VehicleMessageFilters) GetFiltersForVehicle(client clickhouse.ClientInterface, vin string) *MessageFilters { + var f *MessageFilters + var ok bool + + if f, ok = v.filters[vin]; !ok { + f = v.NewFiltersForVehicle(client, vin) + } + + return f +} + +// SetDefaultFilters sets the default filters for vehicles to use as a base +func (v *VehicleMessageFilters) SetDefaultFilters(filters []clickhouse.CANFilterSchema) { + + d := NewMessageFilters() + d.PopulateFromClickhouse(filters) + v.filters[DEFAULT] = d +} + +// NewMessageFilters is a convenience function to setup a map of filters +func NewMessageFilters() *MessageFilters { + return &MessageFilters{ + filters: make(map[int]*MessageFilter), + } +} + +// NewMessageFiltersFromDefault copies default filters to a new struct +func NewMessageFiltersFromDefault(d *MessageFilters) *MessageFilters { + f := NewMessageFilters() + for id, key := range d.filters { + f.SetMessageFilter(id, key.cycle) + } + + return f +} + +// MessageFilters maps CAN IDs to MessageFilter structs +type MessageFilters struct { + filters map[int]*MessageFilter +} + +// SetMessageFilter adds an ID to the filter maps +// cycle expects an integer in ms +func (m *MessageFilters) SetMessageFilter(id int, cycle int) { + m.filters[id] = &MessageFilter{cycle: cycle} +} + +// PopulateFromClickhouse is a helper method to populate +// MessageFilters with filters queried from clickhouse +func (m *MessageFilters) PopulateFromClickhouse(filters []clickhouse.CANFilterSchema) { + for _, filter := range filters { + m.SetMessageFilter(int(filter.ID), int(filter.Period)*1000) + } +} + +// AllowMessage verifies if a message can pass through based on timestamp +// if the message passes through calculates the timestamp for the next +// possible message of that ID +// if MessageFilter.Cycle is 0 it means the message should not pass through +// if MessageFilter.Cycle is -1 it means the message should always pass through +// if no filter is present for the ID - it will always pass through (BLACKLIST) +func (m *MessageFilters) AllowMessage(id int, timestamp int) bool { + filter, ok := m.filters[id] + if !ok { + return true + } + + if filter.cycle == 0 { + return false + } else if filter.cycle == -1 { + return true + } + + if timestamp > filter.next { + filter.next = timestamp + filter.cycle + return true + } + + return false +} + +// MessageFilter keeps track of message cycle rules and the next possible timestamp for a message +// cycle and next are integers in microseconds (CANFilter data that comes in is in microseconds) +type MessageFilter struct { + cycle, next int +} + +const DEFAULT = "DEFAULT" diff --git a/services/optimus/utils/filters_test.go b/services/optimus/utils/filters_test.go new file mode 100644 index 0000000..a5746e7 --- /dev/null +++ b/services/optimus/utils/filters_test.go @@ -0,0 +1,113 @@ +package utils_test + +import ( + "github.com/fiskerinc/cloud-services/services/optimus/utils" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestNewVehicleMessageFilters(t *testing.T) { + _ = utils.NewVehicleMessageFilters() +} + +func TestNewFiltersForVehicle(t *testing.T) { + conn := &clickhouse.MockConn{ExpectedResult: testFilters} + client := clickhouse.NewMockClient(conn) + + vehicleFilters := utils.NewVehicleMessageFilters() + filters := vehicleFilters.NewFiltersForVehicle(client, "TESTVIN123") + + if filters == nil { + t.Errorf(testhelper.TestErrorTemplate, "TestNewFiltersForVehicle", "*MessageFilters", filters) + } +} + +func TestGetFiltersForVehicle(t *testing.T) { + conn := &clickhouse.MockConn{ExpectedResult: testFilters} + client := clickhouse.NewMockClient(conn) + + vehicleFilters := utils.NewVehicleMessageFilters() + filters := vehicleFilters.GetFiltersForVehicle(client, "TESTVIN123") + + if filters == nil { + t.Errorf(testhelper.TestErrorTemplate, "TestGetFiltersForVehicle", "*MessageFilters", filters) + } +} + +func TestSetDefaultFilters(t *testing.T) { + vehicleFilters := utils.NewVehicleMessageFilters() + vehicleFilters.SetDefaultFilters(testFilters) +} + +func TestNewMessageFilters(t *testing.T) { + _ = utils.NewMessageFilters() +} + +func TestNewMessageFiltersFromDefault(t *testing.T) { + d := utils.NewMessageFilters() + d.SetMessageFilter(1, 1) + + f := utils.NewMessageFiltersFromDefault(d) + if f.AllowMessage(1, 1) != true { + t.Errorf(testhelper.TestErrorTemplate, "TestNewMessageFiltersFromDefault", true, false) + } +} + +func TestSetMessageFilter(t *testing.T) { + f := utils.NewMessageFilters() + f.SetMessageFilter(2, 2) +} + +func TestPopulateFromClickhouse(t *testing.T) { + f := utils.NewMessageFilters() + f.PopulateFromClickhouse(testFilters) + if f.AllowMessage(1, 1) != true { + t.Errorf(testhelper.TestErrorTemplate, "TestPopulateFromClickhouse", true, false) + } +} + +func TestAllowMessage(t *testing.T) { + f := utils.NewMessageFilters() + f.PopulateFromClickhouse(testFilters) + + if f.AllowMessage(1, 1) != true { + t.Errorf(testhelper.TestErrorTemplate, "TestAllowMessage", true, false) + } + + if f.AllowMessage(3, 2) != true { + t.Errorf(testhelper.TestErrorTemplate, "TestAllowMessage", true, false) + } + + if f.AllowMessage(3, 1) == true { + t.Errorf(testhelper.TestErrorTemplate, "TestAllowMessage", false, true) + } + + if f.AllowMessage(5, 1) == true { + t.Errorf(testhelper.TestErrorTemplate, "TestAllowMessage", false, true) + } + + if f.AllowMessage(10, 1) != true { + t.Errorf(testhelper.TestErrorTemplate, "TestAllowMessage", true, false) + } +} + +var testFilters = []clickhouse.CANFilterSchema{ + { + ID: 1, + Period: 2, + }, + { + ID: 3, + Period: 4, + }, + { + ID: 5, + Period: 0, + }, + { + ID: 10, + Period: -1, + }, +} diff --git a/services/ota_update_go/Dockerfile b/services/ota_update_go/Dockerfile new file mode 100644 index 0000000..0c89768 --- /dev/null +++ b/services/ota_update_go/Dockerfile @@ -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 diff --git a/services/ota_update_go/background/carImmobilizer.go b/services/ota_update_go/background/carImmobilizer.go new file mode 100644 index 0000000..46fd18f --- /dev/null +++ b/services/ota_update_go/background/carImmobilizer.go @@ -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() { + +} diff --git a/services/ota_update_go/controllers/can_signals.go b/services/ota_update_go/controllers/can_signals.go new file mode 100644 index 0000000..4338606 --- /dev/null +++ b/services/ota_update_go/controllers/can_signals.go @@ -0,0 +1,8 @@ +package controllers + +import "time" + +type CarCANSignal struct { + VIN string + Last time.Time +} diff --git a/services/ota_update_go/controllers/errors.go b/services/ota_update_go/controllers/errors.go new file mode 100644 index 0000000..e9d8a21 --- /dev/null +++ b/services/ota_update_go/controllers/errors.go @@ -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") diff --git a/services/ota_update_go/controllers/handle_create.go b/services/ota_update_go/controllers/handle_create.go new file mode 100644 index 0000000..c14d3b5 --- /dev/null +++ b/services/ota_update_go/controllers/handle_create.go @@ -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) +} diff --git a/services/ota_update_go/controllers/handle_delete.go b/services/ota_update_go/controllers/handle_delete.go new file mode 100644 index 0000000..3b6c9fe --- /dev/null +++ b/services/ota_update_go/controllers/handle_delete.go @@ -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", + }) +} diff --git a/services/ota_update_go/controllers/handle_get_list.go b/services/ota_update_go/controllers/handle_get_list.go new file mode 100644 index 0000000..4dd4683 --- /dev/null +++ b/services/ota_update_go/controllers/handle_get_list.go @@ -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, + }) +} diff --git a/services/ota_update_go/controllers/handle_get_model.go b/services/ota_update_go/controllers/handle_get_model.go new file mode 100644 index 0000000..2c1e0e5 --- /dev/null +++ b/services/ota_update_go/controllers/handle_get_model.go @@ -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) +} diff --git a/services/ota_update_go/controllers/handle_update.go b/services/ota_update_go/controllers/handle_update.go new file mode 100644 index 0000000..00ed731 --- /dev/null +++ b/services/ota_update_go/controllers/handle_update.go @@ -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) +} diff --git a/services/ota_update_go/controllers/health_check.go b/services/ota_update_go/controllers/health_check.go new file mode 100644 index 0000000..358a31a --- /dev/null +++ b/services/ota_update_go/controllers/health_check.go @@ -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 +} diff --git a/services/ota_update_go/controllers/helper_base.go b/services/ota_update_go/controllers/helper_base.go new file mode 100644 index 0000000..81ac3a4 --- /dev/null +++ b/services/ota_update_go/controllers/helper_base.go @@ -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) +} diff --git a/services/ota_update_go/controllers/mongo_handle_create.go b/services/ota_update_go/controllers/mongo_handle_create.go new file mode 100644 index 0000000..76b937a --- /dev/null +++ b/services/ota_update_go/controllers/mongo_handle_create.go @@ -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) +} diff --git a/services/ota_update_go/controllers/mongo_handle_delete.go b/services/ota_update_go/controllers/mongo_handle_delete.go new file mode 100644 index 0000000..aa6d021 --- /dev/null +++ b/services/ota_update_go/controllers/mongo_handle_delete.go @@ -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", + }) +} diff --git a/services/ota_update_go/controllers/mongo_handle_get_list.go b/services/ota_update_go/controllers/mongo_handle_get_list.go new file mode 100644 index 0000000..f8e74fb --- /dev/null +++ b/services/ota_update_go/controllers/mongo_handle_get_list.go @@ -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), + }) +} diff --git a/services/ota_update_go/controllers/mongo_handle_get_model.go b/services/ota_update_go/controllers/mongo_handle_get_model.go new file mode 100644 index 0000000..60f23d6 --- /dev/null +++ b/services/ota_update_go/controllers/mongo_handle_get_model.go @@ -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) +} diff --git a/services/ota_update_go/controllers/mongo_handle_update.go b/services/ota_update_go/controllers/mongo_handle_update.go new file mode 100644 index 0000000..a070f00 --- /dev/null +++ b/services/ota_update_go/controllers/mongo_handle_update.go @@ -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) +} diff --git a/services/ota_update_go/docs/docs.go b/services/ota_update_go/docs/docs.go new file mode 100644 index 0000000..76e6adc --- /dev/null +++ b/services/ota_update_go/docs/docs.go @@ -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{}) +} \ No newline at end of file diff --git a/services/ota_update_go/go.mod b/services/ota_update_go/go.mod new file mode 100644 index 0000000..72bc8da --- /dev/null +++ b/services/ota_update_go/go.mod @@ -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 +) diff --git a/services/ota_update_go/go.sum b/services/ota_update_go/go.sum new file mode 100644 index 0000000..120c5ef --- /dev/null +++ b/services/ota_update_go/go.sum @@ -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= diff --git a/services/ota_update_go/handlers/HandleGetCarsHMIKey.go b/services/ota_update_go/handlers/HandleGetCarsHMIKey.go new file mode 100644 index 0000000..c04b4db --- /dev/null +++ b/services/ota_update_go/handlers/HandleGetCarsHMIKey.go @@ -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 " +// @Param Api-Key header string false "" +// @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)) +} \ No newline at end of file diff --git a/services/ota_update_go/handlers/apicalls_get.go b/services/ota_update_go/handlers/apicalls_get.go new file mode 100644 index 0000000..cc367b6 --- /dev/null +++ b/services/ota_update_go/handlers/apicalls_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/apicalls_get_test.go b/services/ota_update_go/handlers/apicalls_get_test.go new file mode 100644 index 0000000..4271211 --- /dev/null +++ b/services/ota_update_go/handlers/apicalls_get_test.go @@ -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()) + }) + } +} diff --git a/services/ota_update_go/handlers/apitoken_add.go b/services/ota_update_go/handlers/apitoken_add.go new file mode 100644 index 0000000..5dca415 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_add.go @@ -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 " +// @Param Api-Key header string false "" +// @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)) +} diff --git a/services/ota_update_go/handlers/apitoken_add_test.go b/services/ota_update_go/handlers/apitoken_add_test.go new file mode 100644 index 0000000..cfcba57 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_add_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/apitoken_delete.go b/services/ota_update_go/handlers/apitoken_delete.go new file mode 100644 index 0000000..f0b9669 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_delete.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/apitoken_delete_test.go b/services/ota_update_go/handlers/apitoken_delete_test.go new file mode 100644 index 0000000..dee29f6 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_delete_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/apitoken_update.go b/services/ota_update_go/handlers/apitoken_update.go new file mode 100644 index 0000000..9f8b462 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_update.go @@ -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 " +// @Param Api-Key header string false "" +// @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)) +} diff --git a/services/ota_update_go/handlers/apitoken_update_test.go b/services/ota_update_go/handlers/apitoken_update_test.go new file mode 100644 index 0000000..a661464 --- /dev/null +++ b/services/ota_update_go/handlers/apitoken_update_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/apitokens_get.go b/services/ota_update_go/handlers/apitokens_get.go new file mode 100644 index 0000000..1dbb042 --- /dev/null +++ b/services/ota_update_go/handlers/apitokens_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/apitokens_get_test.go b/services/ota_update_go/handlers/apitokens_get_test.go new file mode 100644 index 0000000..c23d37e --- /dev/null +++ b/services/ota_update_go/handlers/apitokens_get_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/can_signal_list_get.go b/services/ota_update_go/handlers/can_signal_list_get.go new file mode 100644 index 0000000..489be60 --- /dev/null +++ b/services/ota_update_go/handlers/can_signal_list_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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}) +} diff --git a/services/ota_update_go/handlers/can_signal_list_get_test.go b/services/ota_update_go/handlers/can_signal_list_get_test.go new file mode 100644 index 0000000..38ca38f --- /dev/null +++ b/services/ota_update_go/handlers/can_signal_list_get_test.go @@ -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()) + }) + } +} diff --git a/services/ota_update_go/handlers/can_signal_vin_get.go b/services/ota_update_go/handlers/can_signal_vin_get.go new file mode 100644 index 0000000..ba188a7 --- /dev/null +++ b/services/ota_update_go/handlers/can_signal_vin_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/can_signal_vin_get_test.go b/services/ota_update_go/handlers/can_signal_vin_get_test.go new file mode 100644 index 0000000..9e12dd2 --- /dev/null +++ b/services/ota_update_go/handlers/can_signal_vin_get_test.go @@ -0,0 +1,73 @@ +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/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" +) + +func TestHandleCANSignalGetList(t *testing.T) { + validQuery := "?timestamp_start=1675111000×tamp_end=1675733172&vin=1G1FP87S1GN000445&limit=1000&can_signals=BCM_FrntDrDoorLockSts,BCM_TotMilg_ODO" + validQuerySelectAll := "?timestamp_start=1675111000×tamp_end=1675733172&vin=1G1FP87S1GN000445&limit=1000&select_all=true" + invalidQuery := "?timestamp_start=1675111000×tamp_end=1675733172&vin=1G1FP87S1GN000445&limit=1000&select_all=false" + validVin := "1G1FP87S1GN000445" + tests := map[string]struct { + q string + conn clickhouse.ConnInterface + expStatus int + expBody string + }{ + "correct": { + q: validQuery, + conn: &clickhouse.MockConn{}, + expStatus: http.StatusOK, + expBody: `VIN,Timestamp,BCM_FrntDrDoorLockSts,BCM_TotMilg_ODO +`, + }, + "correct select all": { + q: validQuerySelectAll, + conn: &clickhouse.MockConn{ExpectedResult: canSignalList}, + expStatus: http.StatusOK, + expBody: `VIN,Timestamp,ACU_AirBagSysAlrmLampSts,ACU_Crash_cfm,ACU_Drvr_Occpt_St,ACU_FrntCrashOutpSts,ACU_LeSideCrashOutpSts,ACU_Pass_OCS_St,ACU_RearCrashOutpSts,ACU_RiSideCrashOutpSts,ACU_RollovrCrashOutpSts,ADAS_AEB_ActvTyp,ADAS_AEB_DecelReq,ADAS_BSDSts,ADAS_BSD_CID_LeDispReq,ADAS_BSD_CID_RiDispReq,ADAS_ChimeReq,ADAS_DCAASts,ADAS_DOW_Sts,ADAS_DoorLockReq,ADAS_ELKASts,ADAS_ELKA_TelltaleReq,ADAS_ESA_TelltaleReq,ADAS_FACM_Sts,ADAS_FACM_TelltaleReq,ADAS_IDS_WarnTxtReq,ADAS_ISAOverSpeedWarning,ADAS_ISASts,ADAS_ISA_CutOffReq,ADAS_ISA_SpdLmt,ADAS_LKASts,ADAS_LatCtrl_Req,ADAS_LatCtrl_SteerAnReq,ADAS_LatCtrl_Sts,ADAS_LatCtrl_Typ,ADAS_LeMirrWarnReq,ADAS_LeMirrWarnSrc,ADAS_LgtCtrl_ESP_Sts,ADAS_ParkGearReq,ADAS_RiMirrWarnReq,ADAS_RiMirrWarnSrc,ADAS_Sts_TLR,ADAS_SysFltWarnReq,ADAS_TLR_WarnReq,ADAS_TSRSpeedLimit,ADAS_TSRSts,AEB_ReAEB_Sts,BCM_BrkLampOutpCmd,BCM_BrkLampSwtSts,BCM_BrkSwtSts,BCM_DoorUnlockSetFb,BCM_DrFrntDoorSts,BCM_DrvrDoorUnlckOutpCmd,BCM_FrntDrDoorLockSts,BCM_LeReDoorSts,BCM_LeSolaValr_RLS,BCM_LockAllDoorCmd,BCM_PasFrntDoorSts,BCM_PassDoorUnlckOutpCmd,BCM_PwrMod,BCM_PwrModVld,BCM_RiReDoorSts,BCM_RiSolaValr_RLS,BCM_SBRMatSt2ndRowLeft,BCM_SBRMatSt2ndRowMid,BCM_SBRMatSt2ndRowRight,BCM_TotMilg_ODO,BMS_ACChrgSoktOverTFlt,BMS_ACCrtFlt,BMS_ACRmngChrgTi,BMS_AccueChrgTotAh,BMS_AccueDchaTotAh,BMS_Bat_Actual_Pack_Capacity,BMS_Bat_Bus_measure_Temperature,BMS_Bat_C_MAXcurr_PLT,BMS_Bat_C_MAXcurr_PST,BMS_Bat_C_MAXcurr_PmLT,BMS_Bat_C_MaxPack_I,BMS_Bat_C_MaxPack_U,BMS_Bat_Coolant_in,BMS_Bat_Coolant_out,BMS_Bat_D_MAXcurr_PLT,BMS_Bat_D_MAXcurr_PST,BMS_Bat_D_MAXcurr_PmLT,BMS_Bat_D_MAXvolt_PST,BMS_Bat_D_MaxPack_I,BMS_Bat_D_MinCell_U,BMS_Bat_D_MinPack_U,BMS_Bat_HVMeasure_Current,BMS_Bat_HVMeasure_V_Pack,BMS_Bat_HVmeasure_Current,BMS_Bat_Hvmeasure_V_Pack,BMS_Bat_SOC_Real,BMS_Bat_SOH,BMS_Bat_SoC_usable,BMS_Bat_measure_Energy,BMS_BattAvrgT,BMS_BattBalActv,BMS_BattCellVoltUnbalAlrm,BMS_BattInlTDmd,BMS_BattInlT_Max,BMS_BattInlT_Min,BMS_BattInlT_Target,BMS_BattPack1OverVoltFlt,BMS_BattPack1UnderVoltFlt,BMS_BatteryGeneralStatus_OK,BMS_CL15_Sts,BMS_CL30C_Sts,BMS_CellDiffT,BMS_CellMaxT,BMS_CellMinT,BMS_CellMinTAlrm,BMS_CellVolt12,BMS_CellVolt120,BMS_Cell_Volt_max,BMS_Cell_Volt_min,BMS_Cell_Volt_nom,BMS_Cont_Act_St_HV_Neg,BMS_Cont_Act_St_HV_Pos,BMS_Cont_Act_St_HV_Prec,BMS_Cont_CurrentState_PV_Neg,BMS_Cont_CurrentState_PV_Pos,BMS_Cont_Error_Ch1_HV_Neg1,BMS_Cont_Error_Ch1_HV_PV_Neg1,BMS_Cont_Error_Ch1_HV_PV_Pos1,BMS_Cont_Error_Ch1_HV_Pos1,BMS_Cont_Error_Ch1_HV_Prec1,BMS_Cont_Error_DisCh1_HV_PV_Neg1,BMS_Cont_Error_DisCh1_HV_PV_Pos1,BMS_Cont_Error_DisCh1_HV_Pos1,BMS_Cont_Error_DisCh1_HV_Prec,BMS_DCChrgRmngTi,BMS_DCChrgSoktOverTFlt,BMS_DTC_status,BMS_ErrorActive_AbsChargeCurrent,BMS_ErrorActive_AbsChargePowerLi,BMS_ErrorActive_AbsDischargeCurr,BMS_ErrorActive_AbsDischargePowe,BMS_ErrorActive_CellOverVoltage,BMS_ErrorActive_CellTempHigh,BMS_ErrorActive_CellTempLow,BMS_ErrorActive_CellUnderVoltage,BMS_ErrorActive_CellVoltageDiffO,BMS_ErrorActive_ChargePowerLimit,BMS_ErrorActive_Diff_U1_UCellSum,BMS_ErrorActive_DischargePowerLi,BMS_ErrorActive_HV_OverVoltage,BMS_ErrorActive_HV_UnderVoltage,BMS_ErrorActive_Iso_Crit,BMS_ErrorActive_OverChargeCurren,BMS_ErrorActive_OverDischargeCur,BMS_ErrorActive_SOC_Low,BMS_ErrorActive_SOH_Low,BMS_ErrorWarning_ChargePowerLimi,BMS_ErrorWarning_DischargePowerL,BMS_ErrorWarning_OverChargeCurre,BMS_ErrorWarning_OverDischargeCu,BMS_ErrorWarning_SOH_Low,BMS_ErrorWarning_overcurrent_Pre,BMS_HVIL_OK,BMS_HV_ContactorsStatus_OK,BMS_HVlinkError_ContactorAuxFail,BMS_HVlinkError_ContactorGeneric,BMS_HVlinkError_ContactorOpen,BMS_HVlinkError_ContactorPwmEcon,BMS_HWerror_Cell_Temp_Sensor_Fai,BMS_HWerror_KL30_Voltage,BMS_LVPwrSplyFlt,BMS_OperationStates,BMS_PVIU_EnergyOverall,BMS_PVIU_F_12V_OVP,BMS_PVIU_F_12V_UVP,BMS_PVIU_F_CANRx_Timeout,BMS_PVIU_F_HVIL,BMS_PVIU_F_Input_OVP,BMS_PVIU_F_Input_UVP,BMS_PVIU_F_OTP,BMS_PVIU_F_Output_OCP,BMS_PVIU_F_Output_OVP,BMS_PVIU_F_Output_SCP,BMS_PVIU_F_Output_UVP,BMS_PVIU_FailSt,BMS_PVIU_ModeSt,BMS_PwrBattCellOverVolt,BMS_PwrBattCellOverVolt_FS,BMS_PwrBattCellVoltUnbal,BMS_PwrBattChrgDchaCrt1,BMS_PwrBattChrgDchaCrt2,BMS_PwrBattExtShoCirc,BMS_PwrBattHeatgMngDmd,BMS_PwrBattIntShoCirc,BMS_PwrBattMSDBreak,BMS_PwrBattNegGNDInsulR,BMS_PwrBattNegRlyAdh,BMS_PwrBattNegRlyBreak,BMS_PwrBattOverChrgAlrm,BMS_PwrBattOverT,BMS_PwrBattOverTAlrm,BMS_PwrBattPackNr,BMS_PwrBattPosGNDInsulR,BMS_PwrBattPosRlyAdh,BMS_PwrBattPosRlyBreak,BMS_PwrBattPrecRBreak,BMS_PwrBattPrecRlyAdh,BMS_PwrBattPrecRlyBreak,BMS_PwrBattPwrStsSOH,BMS_PwrBattRmngCpSOC,BMS_PwrBattTRiseFast,BMS_PwrBattTUnbal,BMS_PwrBattTyp,BMS_SDIL_Sts,BMS_Splr,BMS_StateMAchine_Status,BMS_SwVers,BMS_SwVersM,BMS_SwVersS,BMS_SystemError_HV_CCT_NoBatVolt,BMS_SystemError_HV_CCT_WrongPola,BMS_SystemError_HV_Fuse,BMS_SystemError_HV_Interlock,BMS_SystemError_KL30C,BMS_SystemError_OmmissionOfKL15,BMS_SystemError_Precharge,BMS_TotVoltOver,CIM_GearLvrCrtPosnInfo,CMRR_FL_WhlDrvDirSigInvld,CMRR_FL_WhlSpdSigInvld,CMRR_FR_WhlDrvDirSigInvld,CMRR_FR_WhlSpdSigInvld,CMRR_RL_WhlDrvDirSigInvld,CMRR_RL_WhlSpdSigInvld,CMRR_RR_WhlSpdSigInvld,DCDC_CrtSts,DCDC_InpCrt,DCDC_InpVolt,DCDC_LVOutpOverVoltFlt,DCDC_LVOutpUnderVoltFlt,DCDC_OutpCrt,DCDC_OutpVolt,DCDC_OverTTurnDwnLoadFlt,DCDC_T,DSMC_DrvrSeatT1,DSMC_DrvrSeatT2,EAS_CrtPwr,EAS_MotSpd,ECC_ACSts,ECC_AUTOSts,ECC_ActvGrilleAg,ECC_AirClnSts,ECC_BattFldLvlSnsrLamp,ECC_CircSts,ECC_CoolgFanSpdRatSts,ECC_CoolgFanSts,ECC_DchaT,ECC_DrvrAirOutlMod,ECC_DrvrTSetSts,ECC_EvaprT,ECC_FrntMotOutlT,ECC_HPTSnsrT,ECC_HeatPumpHeatgEXVSts,ECC_HeatSts,ECC_InsdT,ECC_LPTSnsrT,ECC_LeBlowFaceAirOutlT,ECC_LeBlowFootAirOutlT,ECC_MCUFCooltFlow,ECC_MCURCooltFlow,ECC_MCURInlT,ECC_MCURInlTSnsrFlt,ECC_OutdT,ECC_OutdTSnsrFlt,ECC_OutdTVld,ECC_PDUInlT,ECC_PDUInlTSnsrFlt,ECC_PassTSetSts,ECC_ReMotOutlT,ECC_RiBlowFaceAirOutlT,ECC_RiBlowFootAirOutlT,ECC_SYNCSts,ECC_SwVersM,ECC_SwVersS,ECC_TarGear_WTC_B,ECC_TarGear_WTC_H,ECC_TarPwrReq_WTC_B,ECC_TarPwrReq_WTC_H,ECC_TarSpdCmd_EAS,EPS_AbortFb,EPS_AdasLatCtrlSts,EPS_AdasLatCtrlStsVld,EPS_AsscMotCrtPwr,EPS_AsscMotCrtTq,EPS_ComLostFlt_ESP,EPS_DrvrIntvSteerWhlDetd,EPS_DrvrIntvSteerWhlVld,EPS_DrvrSteerTq,EPS_FltIndcn,EPS_MaxSftyTq,EPS_SpdDataErr,EPS_SteerAgSnsrCalSts,EPS_SteerWhlAgSig,EPS_TSnsrFlt,ESP_BrkFStsFlg,ESP_BrkLiReq,ESP_BrkOverTIndcn,ESP_BrkPadWearWarn,ESP_BrkPedlSts,ESP_BrkPedlStsVld,ESP_EmgyBrkLi,ESP_MstCylP,ESP_MstCylPOffs,ESP_NoBrkP,ESP_ParkBrkSt,ESP_SysSts_EPB,ESP_TotBrkTqReq,ESP_VLC_APCtrlSaturated,ESP_VehSpd,ESP_WhlMovgDir_LF,ESP_WhlMovgDir_RF,ESP_WhlMovgDir_RL,ESP_WhlMovgDir_RR,ESP_WhlOdoEdgesVld_FL,ESP_WhlOdoEdgesVld_FR,ESP_WhlOdoEdgesVld_RL,ESP_WhlOdoEdgesVld_RR,ESP_WhlOdoEdges_FL,ESP_WhlOdoEdges_FR,ESP_WhlOdoEdges_RL,ESP_WhlOdoEdges_RR,ESP_WhlSpdVld_LF,ESP_WhlSpdVld_RF,ESP_WhlSpdVld_RL,ESP_WhlSpdVld_RR,ESP_WhlSpd_LF,ESP_WhlSpd_RF,ESP_WhlSpd_RL,ESP_WhlSpd_RR,EWP_B_SpdRatSts,EWP_FD_SpdRatSts,EWP_H_SpdRatSts,EWP_RD_SpdRatSts,FCM_BattVoltBelowThd,FCM_BattVoltOverThd,IBS_AvailableCapacity,IBS_AvgRi,IBS_BatteryCurrent,IBS_BatteryDefect,IBS_BatteryTemperature,IBS_BatteryVoltage,IBS_CapacityLossBottom,IBS_CapacityLossTop,IBS_CurrentAutorange,IBS_DischargeableAh,IBS_EngineStart,IBS_Error,IBS_EstVoltaDrop,IBS_LowVoltage,IBS_NominalCapacity,IBS_OptChargeVolt,IBS_PowerOn,IBS_Recalibrated,IBS_SOCLowerTolerance,IBS_SOCUpperTolerance,IBS_StateOfCharge,IBS_StateOfHealth,IBS_Sulfation,IBS_WakeupStatus,ICC_AirVolSet,ICC_DispVehSpd,ICC_DoorCtrlSwtSts,ICC_DoorUnlockSet,ICC_DrvrTSet,ICC_ECCAUTOReq,ICC_ECCSysSwtCmd,ICC_FrntWiprCtrl,ICC_Ota_Inst,ICC_PassTSet,ICC_RiOutlDamprMotActvCmd,ICC_RiOutlLeRiMotActvCmd,ICC_RiOutlUpDwnMotActvCmd,ICC_SchedUpdate_Selection,ICC_SetChrgEndSOC,ICC_StartChrgBtn,ICC_StopChrgBtn,ICC_TotMilg_ODO,MCU_F_ActDampTq,MCU_F_ActSafeSt,MCU_F_AlrmLamp_FS,MCU_F_BoostEnStat,MCU_F_BoostModActCounter,MCU_F_CooltEstimnT,MCU_F_CooltOverTFlt,MCU_F_CrtMod,MCU_F_CrtSpd,MCU_F_CrtSts,MCU_F_CrtTq,MCU_F_DCBusCrt,MCU_F_DCBusOverVoltFlt,MCU_F_DCBusUnderVoltFlt,MCU_F_DCBusVolt,MCU_F_EmRotorT,MCU_F_EmStatorT,MCU_F_HVActvDchaSts,MCU_F_IGBTOverTFltU,MCU_F_IGBTT,MCU_F_LVPwrSplyOverVoltFlt,MCU_F_LVPwrSplyUnderVoltFlt,MCU_F_MOT_T,MCU_F_MaxElecTq,MCU_F_MaxPwrGennTq,MCU_F_MaxTqLim,MCU_F_MinTqLim,MCU_F_OverTFlt,MCU_F_ParkLockStatus_EV,MCU_F_PrkLckStatNrmlOp_EV,MCU_F_Sum,MCU_R_ActDampTq,MCU_R_ActSafeSt,MCU_R_AlrmLamp_FS,MCU_R_BoostEnStat,MCU_R_BoostModActCounter,MCU_R_CooltEstimnT,MCU_R_CooltOverTFlt,MCU_R_CrtMod,MCU_R_CrtSpd,MCU_R_CrtSts,MCU_R_CrtTq,MCU_R_DCBusCrt,MCU_R_DCBusVolt,MCU_R_Decoup_State,MCU_R_EmRotorT,MCU_R_EmStatorT,MCU_R_HVActvDchaSts,MCU_R_IGBTOverTFltU,MCU_R_IGBTT,MCU_R_LVPwrSplyOverVoltFlt,MCU_R_LVPwrSplyUnderVoltFlt,MCU_R_MOT_T,MCU_R_MaxElecTq,MCU_R_MaxPwrGennTq,MCU_R_MaxTqLim,MCU_R_MinTqLim,MCU_R_OverTFlt,MCU_R_Sum,MRR_VCUWhlTqActVldSigInvld,MRR_WhlSpdAmpInvld,MRR_WhlSpdDirInvld,OBC_12VPwrSplyVoltOver,OBC_12VPwrSplyVoltUnder,OBC_ACCrt,OBC_ACCrtPhase1,OBC_ACCrtPhase2,OBC_ACCrtPhase3,OBC_ACVolt,OBC_ComLostFlt_VCU,OBC_CrtSts,OBC_DCChrggEvseInlVolt,OBC_DCCrt,OBC_DCNegRlyAdhFlt,OBC_DCNegRlyCtrlSts,OBC_DCPosRlyAdhFlt,OBC_DCPosRlyCtrlSts,OBC_DCVolt,OBC_EASIntlkSigChkCircFlt,OBC_EVChrgElectcLockStsFbSig,OBC_InpVoltRng,OBC_MaxOutpVolt,OBC_MinOutpVolt,OBC_OperMod,OBC_OutpMaxPwr,OBC_Pwr,OBC_T,PKC_DoorOpenRmd,PSM_PassSeatT1,PSM_PassSeatT2,PVIU_DCDC_ActHVCurr,PVIU_DCDC_ActHVVolt,PVIU_DCDC_ActLVCurr,PVIU_DCDC_ActLVVolt,PWC_ChrgSts,PWC_R_ChrgSts,TBOX_California_mode,TBOX_Crash_cfm,TBOX_CtrlBitVector_Bit4_ActWake,TBOX_FlsFlg,TBOX_GPSHei,TBOX_GPSLati,TBOX_GPSLongi,TBOX_ICC_FlashPct,TBOX_ICC_UpdStrt,TBOX_OTAInhbReq,TBOX_OTAReq_ECUx,TBOX_OTAResFb,TBOX_OTA_TiRng,TBOX_OTA_ownCondchk,TBOX_RemClsTrCmd,TBOX_RemCtrlLockCmd,TBOX_RemKL15PwrOnReq,TBOX_RemSeekCarCtrlCmd,TBOX_Remflsh,TBOX_ScheduleCharg_Status,TBOX_Sw_upd,TBOX_VoltHigh,TBOX_VoltLow,TBOX_eCallSt,TRM_IndcrLeSts,TRM_IndcrRiSts,TRM_TrailerSts,VCU_ACChrgCrtUpprLmt,VCU_ACChrgDchaGunCnctnSts,VCU_ACChrgDchaIndcrLampSts,VCU_ACChrgDchaSoktT1,VCU_ACChrgShttrSts,VCU_ACChrgStrtFailReason,VCU_ACDCChrgIndcrLamp12VFlt,VCU_ACDchaStrtFailReason,VCU_APSPerc,VCU_AccelModFb,VCU_ActChrgTotAh,VCU_ActDchgTotAh,VCU_ActRgnTq_CRB,VCU_ActvDchaEnaFlg,VCU_BMSLVWakeUpSts,VCU_BattChrgSOC,VCU_BattChrgSOCEEPROM,VCU_BattFltIndcn,VCU_BattVolt,VCU_BoostCmd_F,VCU_BoostCmd_R,VCU_BoostEnStat,VCU_BoostEnStatCoded,VCU_BrkLampCtrlSts,VCU_BrkPedlPwrSplyFlt,VCU_BrkPedlSts_GB,VCU_BrkSig,VCU_BrkSigVld,VCU_BrkStrtReq,VCU_CcTrgSpdDisp,VCU_CellMaxVolt,VCU_CellMinVolt,VCU_CellVoltLoHVPwrOffFlg,VCU_ChrgDchaGunCnctnIndcrLamp,VCU_ChrgEndSOCCrtCtrlFlg,VCU_ChrgIndcrLamp,VCU_ChrgModBHMCnseEgyPerc,VCU_ChrgModBattHeatgMngCnseEgy,VCU_ChrgModChrgEffCnseEgy,VCU_ChrgModChrgEffCnseEgyPerc,VCU_ChrgModVehChrgEgy,VCU_ChrgPwrLim,VCU_ChrgSts,VCU_ChrgSts_GB,VCU_ChrgSysOperCmd,VCU_CoastTqReq,VCU_ComChkFlt_BMS,VCU_ComChkFlt_ESP,VCU_ComChkFlt_VCU,VCU_ComLostFlt_ABS,VCU_ComLostFlt_BCM,VCU_ComLostFlt_BMS,VCU_ComLostFlt_BOBC,VCU_ComLostFlt_DCDC,VCU_ComLostFlt_EHU,VCU_ComLostFlt_ESP,VCU_ComLostFlt_ICC,VCU_ComLostFlt_ICM,VCU_ComLostFlt_MCU_F,VCU_ComLostFlt_NBS,VCU_ComLostFlt_OBC,VCU_ComLostFlt_PEPS,VCU_ComLostFlt_TBOX,VCU_ComLostFlt_VCU,VCU_CrashDIRSigPWMPct,VCU_CrtSts,VCU_DCChrgCrtCmd,VCU_DCChrgDchaGunCnctnSts,VCU_DCChrgDchaIndcrLampSts,VCU_DCChrgOutpCrt,VCU_DCChrgOutpVolt,VCU_DCChrgRmngTi,VCU_DCChrgShttrSts,VCU_DCChrgSoktT1,VCU_DCChrgSoktT2,VCU_DCCrtCmd_OBC,VCU_DCDCEnaCmd,VCU_DCDCOutpVoltCmd,VCU_DCPosRlyCtrlCmd,VCU_DCRlyCtrlCmd,VCU_DCVoltCmd_OBC,VCU_DDmdTq,VCU_Decel_ReqType,VCU_Decoup_Rq_Mod,VCU_DrvModShiftMisoper,VCU_DrvModSigFb,VCU_DrvMotOutpPwrPerc,VCU_DrvTqLimSts,VCU_DrvgMilg,VCU_DrvrFrntMotTqReq,VCU_DrvrMotNr_GB,VCU_DrvrReMotTqReq,VCU_ECCEnaCmd,VCU_ECCSysAllwMaxPwrCnse,VCU_ECC_PwrMax,VCU_ECC_PwrMin,VCU_EPBReq,VCU_EPB_Req,VCU_EVCANBusFlt,VCU_EgyCnsHist01,VCU_EmgyShutOff,VCU_EvChrgElectcLockCtrlCmd,VCU_FCChrgCnt,VCU_FMCUHVSelfChkTiOut,VCU_F_MnTqLim,VCU_F_MxTqGrad,VCU_F_MxTqLim,VCU_F_cmdSlipCtrlComp,VCU_F_rspdEmMax,VCU_F_rspdEmMin,VCU_FrntCrtWhlTq,VCU_FrntMotModCmd,VCU_FrntMotTarTqCmd,VCU_FrntMotTarTqCmdVld,VCU_GearSig,VCU_GearSig_GB,VCU_HVBActivateDeactivateRq,VCU_HVBattActPwr,VCU_HVCustSOC,VCU_HVEMBoostModSt,VCU_HVPwrOffReq,VCU_IsolationMonitor_Rq,VCU_KL15Req,VCU_KickdwnFlg,VCU_LVWakeUpSts_CCU,VCU_LVWakeUpSts_MCU,VCU_LVWakeUpSts_PDU,VCU_LVbattCtrlSigOpenFlt,VCU_LVbattCtrlSigSTBFlt,VCU_LVbattCtrlSigSTGFlt,VCU_Lvl2ProtStrtFlg,VCU_MaxBoostCntr,VCU_NetWakeUpSig,VCU_NotChrgModAcsyCnseEgy,VCU_NotChrgModAcsyCnseEgyPerc,VCU_NotChrgModBHMCnseEgy,VCU_NotChrgModBHMCnseEgyPerc,VCU_NotChrgModECCCnseEgy,VCU_NotChrgModECCCnseEgyPerc,VCU_NotChrgModExtDchaCnseEgy,VCU_NotChrgModExtDchaCnseEgyPerc,VCU_NotChrgModVehDrvCnseEgy,VCU_NotChrgModVehDrvCnseEgyPerc,VCU_NotChrgModVehTotCnseEgy,VCU_OTAInhbRdy,VCU_OTARdy_Fb,VCU_OTAVehCdnChk_ASIL,VCU_OTAVehCdnChk_QM_sts,VCU_OTA_ECUInhb_Req,VCU_OutpACWakeupSigSts,VCU_OutpFCWakeupSigSts,VCU_PPrkgFltLvl,VCU_ParkRdy,VCU_PrefillReq,VCU_PwrBattECCEnaCmd,VCU_PwrSplyEquipSts,VCU_RMCUHVSelfChkTiOut,VCU_R_MnTqLim,VCU_R_MxTqGrad,VCU_R_MxTqLim,VCU_R_rspdEmMax,VCU_R_rspdEmMin,VCU_RdyLamp,VCU_ReCrtWhlTq,VCU_ReMotDampgFobdFlg,VCU_ReMotModCmd,VCU_ReMotTarTqCmd,VCU_RegenLvlFb,VCU_RemBattHeatgFailReason,VCU_RemChrgEndCmd,VCU_RemChrgEndReason,VCU_RemCtrlDrvgBrkReq,VCU_RemCtrlDrvgEmgyBrkReq,VCU_RemECCEndCmd,VCU_RemECCEndReason,VCU_RmnUsrECCFctLmtDispCmd,VCU_SCChrgCnt,VCU_SOCLoWakeUpEnaCmd,VCU_SOCLoWakeUpThd,VCU_SOCTooHiAlrm,VCU_SOCTooLoAlrm,VCU_SchedChrgnStsFb,VCU_Set_BMS_Mod,VCU_Set_Balc_Mod,VCU_Set_Chrg_Mod,VCU_ShiftMisoper,VCU_ShiftOperRmn,VCU_SlipRate,VCU_StgyGearSig,VCU_Sts_CC_ICC,VCU_SwVersFlash,VCU_SwVersMain,VCU_SwVersMinor,VCU_SwVersPatch,VCU_SysAllwMaxFbPwr,VCU_SysAllwMaxPwrCnse,VCU_TotOverVoltLvl2ProtFlg,VCU_TqLim,VCU_TripAvgEgyConsmd,VCU_TripMilg,VCU_VcuErrBitVect,VCU_VcuErrCtgy,VCU_VcuState,VCU_VehChrgDchgMod,VCU_VehCrtChrgEndSOC,VCU_VehCtrlrEEPROMFlt,VCU_VehCtrlrRAMFlt,VCU_VehCtrlrROMFlt,VCU_VehExtDchaSts,VCU_VehMCUFMaxElecTq,VCU_VehMCUFMaxPwrGennTq,VCU_VehMCURMaxElecTq,VCU_VehMCURMaxPwrGennTq,VCU_VehMaxWhlTq,VCU_VehMod,VCU_VehOperMod,VCU_VehRemOperModPwrOnReq,VCU_VehRemSCModPwrOnReq,VCU_VehSt,VCU_VehSts,VCU_VehTqDistbnRat,VCU_VehTqFltSts,VCU_WhlTqAct,WTC_B_ActGear,WTC_B_ComLost_ECC,WTC_B_Crt,WTC_B_CrtSts,WTC_B_CrtT,WTC_B_InlT,WTC_H_ActGear,WTC_H_Crt,WTC_H_CrtPwr,WTC_H_CrtSts,WTC_H_CrtT,WTC_H_InpT,XXX_OTAInhbRdy,XXX_OTARdy_Fb,YRS_LatAcce,YRS_LgtAcce,iBooster_BrkPedlAppldFlg,iBooster_BrkPedlAppldVld,iBooster_DrvrBrkPedlTrvl,iBooster_DrvrBrkPedlTrvlSts,iBooster_ExtBrkReqSts,iBooster_MstCylBrkPedlActTrvl,iBooster_MstCylBrkPedlActTrvlSts +`, + }, + "failed_filter": { + q: "", + expStatus: http.StatusBadRequest, + expBody: `{"message":"VIN required. TimestampStart required. TimestampEnd required","error":"Bad Request"}`, + }, + "invalid query": { + q: invalidQuery, + expStatus: http.StatusBadRequest, + expBody: `{"message":"either Select All of a list of CAN Signals required","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: "vin", Value: validVin}, + } + ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p) + r := httptest.NewRequest(http.MethodGet, "http://example.com/ota_update/can_signals/export"+tt.q, nil). + WithContext(ctx) + + handlers.HandleCanSignalVINGet(w, r) + assert.Equal(t, tt.expStatus, w.Code) + assert.Equal(t, tt.expBody, w.Body.String()) + }) + } +} + +var canSignalList = []common.CANSignalNameList{{Signal_Name: "ACU_AirBagSysAlrmLampSts"}, {Signal_Name: "ACU_Crash_cfm"}, {Signal_Name: "ACU_Drvr_Occpt_St"}, {Signal_Name: "ACU_FrntCrashOutpSts"}, {Signal_Name: "ACU_LeSideCrashOutpSts"}, {Signal_Name: "ACU_Pass_OCS_St"}, {Signal_Name: "ACU_RearCrashOutpSts"}, {Signal_Name: "ACU_RiSideCrashOutpSts"}, {Signal_Name: "ACU_RollovrCrashOutpSts"}, {Signal_Name: "ADAS_AEB_ActvTyp"}, {Signal_Name: "ADAS_AEB_DecelReq"}, {Signal_Name: "ADAS_BSDSts"}, {Signal_Name: "ADAS_BSD_CID_LeDispReq"}, {Signal_Name: "ADAS_BSD_CID_RiDispReq"}, {Signal_Name: "ADAS_ChimeReq"}, {Signal_Name: "ADAS_DCAASts"}, {Signal_Name: "ADAS_DOW_Sts"}, {Signal_Name: "ADAS_DoorLockReq"}, {Signal_Name: "ADAS_ELKASts"}, {Signal_Name: "ADAS_ELKA_TelltaleReq"}, {Signal_Name: "ADAS_ESA_TelltaleReq"}, {Signal_Name: "ADAS_FACM_Sts"}, {Signal_Name: "ADAS_FACM_TelltaleReq"}, {Signal_Name: "ADAS_IDS_WarnTxtReq"}, {Signal_Name: "ADAS_ISAOverSpeedWarning"}, {Signal_Name: "ADAS_ISASts"}, {Signal_Name: "ADAS_ISA_CutOffReq"}, {Signal_Name: "ADAS_ISA_SpdLmt"}, {Signal_Name: "ADAS_LKASts"}, {Signal_Name: "ADAS_LatCtrl_Req"}, {Signal_Name: "ADAS_LatCtrl_SteerAnReq"}, {Signal_Name: "ADAS_LatCtrl_Sts"}, {Signal_Name: "ADAS_LatCtrl_Typ"}, {Signal_Name: "ADAS_LeMirrWarnReq"}, {Signal_Name: "ADAS_LeMirrWarnSrc"}, {Signal_Name: "ADAS_LgtCtrl_ESP_Sts"}, {Signal_Name: "ADAS_ParkGearReq"}, {Signal_Name: "ADAS_RiMirrWarnReq"}, {Signal_Name: "ADAS_RiMirrWarnSrc"}, {Signal_Name: "ADAS_Sts_TLR"}, {Signal_Name: "ADAS_SysFltWarnReq"}, {Signal_Name: "ADAS_TLR_WarnReq"}, {Signal_Name: "ADAS_TSRSpeedLimit"}, {Signal_Name: "ADAS_TSRSts"}, {Signal_Name: "AEB_ReAEB_Sts"}, {Signal_Name: "BCM_BrkLampOutpCmd"}, {Signal_Name: "BCM_BrkLampSwtSts"}, {Signal_Name: "BCM_BrkSwtSts"}, {Signal_Name: "BCM_DoorUnlockSetFb"}, {Signal_Name: "BCM_DrFrntDoorSts"}, {Signal_Name: "BCM_DrvrDoorUnlckOutpCmd"}, {Signal_Name: "BCM_FrntDrDoorLockSts"}, {Signal_Name: "BCM_LeReDoorSts"}, {Signal_Name: "BCM_LeSolaValr_RLS"}, {Signal_Name: "BCM_LockAllDoorCmd"}, {Signal_Name: "BCM_PasFrntDoorSts"}, {Signal_Name: "BCM_PassDoorUnlckOutpCmd"}, {Signal_Name: "BCM_PwrMod"}, {Signal_Name: "BCM_PwrModVld"}, {Signal_Name: "BCM_RiReDoorSts"}, {Signal_Name: "BCM_RiSolaValr_RLS"}, {Signal_Name: "BCM_SBRMatSt2ndRowLeft"}, {Signal_Name: "BCM_SBRMatSt2ndRowMid"}, {Signal_Name: "BCM_SBRMatSt2ndRowRight"}, {Signal_Name: "BCM_TotMilg_ODO"}, {Signal_Name: "BMS_ACChrgSoktOverTFlt"}, {Signal_Name: "BMS_ACCrtFlt"}, {Signal_Name: "BMS_ACRmngChrgTi"}, {Signal_Name: "BMS_AccueChrgTotAh"}, {Signal_Name: "BMS_AccueDchaTotAh"}, {Signal_Name: "BMS_Bat_Actual_Pack_Capacity"}, {Signal_Name: "BMS_Bat_Bus_measure_Temperature"}, {Signal_Name: "BMS_Bat_C_MAXcurr_PLT"}, {Signal_Name: "BMS_Bat_C_MAXcurr_PST"}, {Signal_Name: "BMS_Bat_C_MAXcurr_PmLT"}, {Signal_Name: "BMS_Bat_C_MaxPack_I"}, {Signal_Name: "BMS_Bat_C_MaxPack_U"}, {Signal_Name: "BMS_Bat_Coolant_in"}, {Signal_Name: "BMS_Bat_Coolant_out"}, {Signal_Name: "BMS_Bat_D_MAXcurr_PLT"}, {Signal_Name: "BMS_Bat_D_MAXcurr_PST"}, {Signal_Name: "BMS_Bat_D_MAXcurr_PmLT"}, {Signal_Name: "BMS_Bat_D_MAXvolt_PST"}, {Signal_Name: "BMS_Bat_D_MaxPack_I"}, {Signal_Name: "BMS_Bat_D_MinCell_U"}, {Signal_Name: "BMS_Bat_D_MinPack_U"}, {Signal_Name: "BMS_Bat_HVMeasure_Current"}, {Signal_Name: "BMS_Bat_HVMeasure_V_Pack"}, {Signal_Name: "BMS_Bat_HVmeasure_Current"}, {Signal_Name: "BMS_Bat_Hvmeasure_V_Pack"}, {Signal_Name: "BMS_Bat_SOC_Real"}, {Signal_Name: "BMS_Bat_SOH"}, {Signal_Name: "BMS_Bat_SoC_usable"}, {Signal_Name: "BMS_Bat_measure_Energy"}, {Signal_Name: "BMS_BattAvrgT"}, {Signal_Name: "BMS_BattBalActv"}, {Signal_Name: "BMS_BattCellVoltUnbalAlrm"}, {Signal_Name: "BMS_BattInlTDmd"}, {Signal_Name: "BMS_BattInlT_Max"}, {Signal_Name: "BMS_BattInlT_Min"}, {Signal_Name: "BMS_BattInlT_Target"}, {Signal_Name: "BMS_BattPack1OverVoltFlt"}, {Signal_Name: "BMS_BattPack1UnderVoltFlt"}, {Signal_Name: "BMS_BatteryGeneralStatus_OK"}, {Signal_Name: "BMS_CL15_Sts"}, {Signal_Name: "BMS_CL30C_Sts"}, {Signal_Name: "BMS_CellDiffT"}, {Signal_Name: "BMS_CellMaxT"}, {Signal_Name: "BMS_CellMinT"}, {Signal_Name: "BMS_CellMinTAlrm"}, {Signal_Name: "BMS_CellVolt12"}, {Signal_Name: "BMS_CellVolt120"}, {Signal_Name: "BMS_Cell_Volt_max"}, {Signal_Name: "BMS_Cell_Volt_min"}, {Signal_Name: "BMS_Cell_Volt_nom"}, {Signal_Name: "BMS_Cont_Act_St_HV_Neg"}, {Signal_Name: "BMS_Cont_Act_St_HV_Pos"}, {Signal_Name: "BMS_Cont_Act_St_HV_Prec"}, {Signal_Name: "BMS_Cont_CurrentState_PV_Neg"}, {Signal_Name: "BMS_Cont_CurrentState_PV_Pos"}, {Signal_Name: "BMS_Cont_Error_Ch1_HV_Neg1"}, {Signal_Name: "BMS_Cont_Error_Ch1_HV_PV_Neg1"}, {Signal_Name: "BMS_Cont_Error_Ch1_HV_PV_Pos1"}, {Signal_Name: "BMS_Cont_Error_Ch1_HV_Pos1"}, {Signal_Name: "BMS_Cont_Error_Ch1_HV_Prec1"}, {Signal_Name: "BMS_Cont_Error_DisCh1_HV_PV_Neg1"}, {Signal_Name: "BMS_Cont_Error_DisCh1_HV_PV_Pos1"}, {Signal_Name: "BMS_Cont_Error_DisCh1_HV_Pos1"}, {Signal_Name: "BMS_Cont_Error_DisCh1_HV_Prec"}, {Signal_Name: "BMS_DCChrgRmngTi"}, {Signal_Name: "BMS_DCChrgSoktOverTFlt"}, {Signal_Name: "BMS_DTC_status"}, {Signal_Name: "BMS_ErrorActive_AbsChargeCurrent"}, {Signal_Name: "BMS_ErrorActive_AbsChargePowerLi"}, {Signal_Name: "BMS_ErrorActive_AbsDischargeCurr"}, {Signal_Name: "BMS_ErrorActive_AbsDischargePowe"}, {Signal_Name: "BMS_ErrorActive_CellOverVoltage"}, {Signal_Name: "BMS_ErrorActive_CellTempHigh"}, {Signal_Name: "BMS_ErrorActive_CellTempLow"}, {Signal_Name: "BMS_ErrorActive_CellUnderVoltage"}, {Signal_Name: "BMS_ErrorActive_CellVoltageDiffO"}, {Signal_Name: "BMS_ErrorActive_ChargePowerLimit"}, {Signal_Name: "BMS_ErrorActive_Diff_U1_UCellSum"}, {Signal_Name: "BMS_ErrorActive_DischargePowerLi"}, {Signal_Name: "BMS_ErrorActive_HV_OverVoltage"}, {Signal_Name: "BMS_ErrorActive_HV_UnderVoltage"}, {Signal_Name: "BMS_ErrorActive_Iso_Crit"}, {Signal_Name: "BMS_ErrorActive_OverChargeCurren"}, {Signal_Name: "BMS_ErrorActive_OverDischargeCur"}, {Signal_Name: "BMS_ErrorActive_SOC_Low"}, {Signal_Name: "BMS_ErrorActive_SOH_Low"}, {Signal_Name: "BMS_ErrorWarning_ChargePowerLimi"}, {Signal_Name: "BMS_ErrorWarning_DischargePowerL"}, {Signal_Name: "BMS_ErrorWarning_OverChargeCurre"}, {Signal_Name: "BMS_ErrorWarning_OverDischargeCu"}, {Signal_Name: "BMS_ErrorWarning_SOH_Low"}, {Signal_Name: "BMS_ErrorWarning_overcurrent_Pre"}, {Signal_Name: "BMS_HVIL_OK"}, {Signal_Name: "BMS_HV_ContactorsStatus_OK"}, {Signal_Name: "BMS_HVlinkError_ContactorAuxFail"}, {Signal_Name: "BMS_HVlinkError_ContactorGeneric"}, {Signal_Name: "BMS_HVlinkError_ContactorOpen"}, {Signal_Name: "BMS_HVlinkError_ContactorPwmEcon"}, {Signal_Name: "BMS_HWerror_Cell_Temp_Sensor_Fai"}, {Signal_Name: "BMS_HWerror_KL30_Voltage"}, {Signal_Name: "BMS_LVPwrSplyFlt"}, {Signal_Name: "BMS_OperationStates"}, {Signal_Name: "BMS_PVIU_EnergyOverall"}, {Signal_Name: "BMS_PVIU_F_12V_OVP"}, {Signal_Name: "BMS_PVIU_F_12V_UVP"}, {Signal_Name: "BMS_PVIU_F_CANRx_Timeout"}, {Signal_Name: "BMS_PVIU_F_HVIL"}, {Signal_Name: "BMS_PVIU_F_Input_OVP"}, {Signal_Name: "BMS_PVIU_F_Input_UVP"}, {Signal_Name: "BMS_PVIU_F_OTP"}, {Signal_Name: "BMS_PVIU_F_Output_OCP"}, {Signal_Name: "BMS_PVIU_F_Output_OVP"}, {Signal_Name: "BMS_PVIU_F_Output_SCP"}, {Signal_Name: "BMS_PVIU_F_Output_UVP"}, {Signal_Name: "BMS_PVIU_FailSt"}, {Signal_Name: "BMS_PVIU_ModeSt"}, {Signal_Name: "BMS_PwrBattCellOverVolt"}, {Signal_Name: "BMS_PwrBattCellOverVolt_FS"}, {Signal_Name: "BMS_PwrBattCellVoltUnbal"}, {Signal_Name: "BMS_PwrBattChrgDchaCrt1"}, {Signal_Name: "BMS_PwrBattChrgDchaCrt2"}, {Signal_Name: "BMS_PwrBattExtShoCirc"}, {Signal_Name: "BMS_PwrBattHeatgMngDmd"}, {Signal_Name: "BMS_PwrBattIntShoCirc"}, {Signal_Name: "BMS_PwrBattMSDBreak"}, {Signal_Name: "BMS_PwrBattNegGNDInsulR"}, {Signal_Name: "BMS_PwrBattNegRlyAdh"}, {Signal_Name: "BMS_PwrBattNegRlyBreak"}, {Signal_Name: "BMS_PwrBattOverChrgAlrm"}, {Signal_Name: "BMS_PwrBattOverT"}, {Signal_Name: "BMS_PwrBattOverTAlrm"}, {Signal_Name: "BMS_PwrBattPackNr"}, {Signal_Name: "BMS_PwrBattPosGNDInsulR"}, {Signal_Name: "BMS_PwrBattPosRlyAdh"}, {Signal_Name: "BMS_PwrBattPosRlyBreak"}, {Signal_Name: "BMS_PwrBattPrecRBreak"}, {Signal_Name: "BMS_PwrBattPrecRlyAdh"}, {Signal_Name: "BMS_PwrBattPrecRlyBreak"}, {Signal_Name: "BMS_PwrBattPwrStsSOH"}, {Signal_Name: "BMS_PwrBattRmngCpSOC"}, {Signal_Name: "BMS_PwrBattTRiseFast"}, {Signal_Name: "BMS_PwrBattTUnbal"}, {Signal_Name: "BMS_PwrBattTyp"}, {Signal_Name: "BMS_SDIL_Sts"}, {Signal_Name: "BMS_Splr"}, {Signal_Name: "BMS_StateMAchine_Status"}, {Signal_Name: "BMS_SwVers"}, {Signal_Name: "BMS_SwVersM"}, {Signal_Name: "BMS_SwVersS"}, {Signal_Name: "BMS_SystemError_HV_CCT_NoBatVolt"}, {Signal_Name: "BMS_SystemError_HV_CCT_WrongPola"}, {Signal_Name: "BMS_SystemError_HV_Fuse"}, {Signal_Name: "BMS_SystemError_HV_Interlock"}, {Signal_Name: "BMS_SystemError_KL30C"}, {Signal_Name: "BMS_SystemError_OmmissionOfKL15"}, {Signal_Name: "BMS_SystemError_Precharge"}, {Signal_Name: "BMS_TotVoltOver"}, {Signal_Name: "CIM_GearLvrCrtPosnInfo"}, {Signal_Name: "CMRR_FL_WhlDrvDirSigInvld"}, {Signal_Name: "CMRR_FL_WhlSpdSigInvld"}, {Signal_Name: "CMRR_FR_WhlDrvDirSigInvld"}, {Signal_Name: "CMRR_FR_WhlSpdSigInvld"}, {Signal_Name: "CMRR_RL_WhlDrvDirSigInvld"}, {Signal_Name: "CMRR_RL_WhlSpdSigInvld"}, {Signal_Name: "CMRR_RR_WhlSpdSigInvld"}, {Signal_Name: "DCDC_CrtSts"}, {Signal_Name: "DCDC_InpCrt"}, {Signal_Name: "DCDC_InpVolt"}, {Signal_Name: "DCDC_LVOutpOverVoltFlt"}, {Signal_Name: "DCDC_LVOutpUnderVoltFlt"}, {Signal_Name: "DCDC_OutpCrt"}, {Signal_Name: "DCDC_OutpVolt"}, {Signal_Name: "DCDC_OverTTurnDwnLoadFlt"}, {Signal_Name: "DCDC_T"}, {Signal_Name: "DSMC_DrvrSeatT1"}, {Signal_Name: "DSMC_DrvrSeatT2"}, {Signal_Name: "EAS_CrtPwr"}, {Signal_Name: "EAS_MotSpd"}, {Signal_Name: "ECC_ACSts"}, {Signal_Name: "ECC_AUTOSts"}, {Signal_Name: "ECC_ActvGrilleAg"}, {Signal_Name: "ECC_AirClnSts"}, {Signal_Name: "ECC_BattFldLvlSnsrLamp"}, {Signal_Name: "ECC_CircSts"}, {Signal_Name: "ECC_CoolgFanSpdRatSts"}, {Signal_Name: "ECC_CoolgFanSts"}, {Signal_Name: "ECC_DchaT"}, {Signal_Name: "ECC_DrvrAirOutlMod"}, {Signal_Name: "ECC_DrvrTSetSts"}, {Signal_Name: "ECC_EvaprT"}, {Signal_Name: "ECC_FrntMotOutlT"}, {Signal_Name: "ECC_HPTSnsrT"}, {Signal_Name: "ECC_HeatPumpHeatgEXVSts"}, {Signal_Name: "ECC_HeatSts"}, {Signal_Name: "ECC_InsdT"}, {Signal_Name: "ECC_LPTSnsrT"}, {Signal_Name: "ECC_LeBlowFaceAirOutlT"}, {Signal_Name: "ECC_LeBlowFootAirOutlT"}, {Signal_Name: "ECC_MCUFCooltFlow"}, {Signal_Name: "ECC_MCURCooltFlow"}, {Signal_Name: "ECC_MCURInlT"}, {Signal_Name: "ECC_MCURInlTSnsrFlt"}, {Signal_Name: "ECC_OutdT"}, {Signal_Name: "ECC_OutdTSnsrFlt"}, {Signal_Name: "ECC_OutdTVld"}, {Signal_Name: "ECC_PDUInlT"}, {Signal_Name: "ECC_PDUInlTSnsrFlt"}, {Signal_Name: "ECC_PassTSetSts"}, {Signal_Name: "ECC_ReMotOutlT"}, {Signal_Name: "ECC_RiBlowFaceAirOutlT"}, {Signal_Name: "ECC_RiBlowFootAirOutlT"}, {Signal_Name: "ECC_SYNCSts"}, {Signal_Name: "ECC_SwVersM"}, {Signal_Name: "ECC_SwVersS"}, {Signal_Name: "ECC_TarGear_WTC_B"}, {Signal_Name: "ECC_TarGear_WTC_H"}, {Signal_Name: "ECC_TarPwrReq_WTC_B"}, {Signal_Name: "ECC_TarPwrReq_WTC_H"}, {Signal_Name: "ECC_TarSpdCmd_EAS"}, {Signal_Name: "EPS_AbortFb"}, {Signal_Name: "EPS_AdasLatCtrlSts"}, {Signal_Name: "EPS_AdasLatCtrlStsVld"}, {Signal_Name: "EPS_AsscMotCrtPwr"}, {Signal_Name: "EPS_AsscMotCrtTq"}, {Signal_Name: "EPS_ComLostFlt_ESP"}, {Signal_Name: "EPS_DrvrIntvSteerWhlDetd"}, {Signal_Name: "EPS_DrvrIntvSteerWhlVld"}, {Signal_Name: "EPS_DrvrSteerTq"}, {Signal_Name: "EPS_FltIndcn"}, {Signal_Name: "EPS_MaxSftyTq"}, {Signal_Name: "EPS_SpdDataErr"}, {Signal_Name: "EPS_SteerAgSnsrCalSts"}, {Signal_Name: "EPS_SteerWhlAgSig"}, {Signal_Name: "EPS_TSnsrFlt"}, {Signal_Name: "ESP_BrkFStsFlg"}, {Signal_Name: "ESP_BrkLiReq"}, {Signal_Name: "ESP_BrkOverTIndcn"}, {Signal_Name: "ESP_BrkPadWearWarn"}, {Signal_Name: "ESP_BrkPedlSts"}, {Signal_Name: "ESP_BrkPedlStsVld"}, {Signal_Name: "ESP_EmgyBrkLi"}, {Signal_Name: "ESP_MstCylP"}, {Signal_Name: "ESP_MstCylPOffs"}, {Signal_Name: "ESP_NoBrkP"}, {Signal_Name: "ESP_ParkBrkSt"}, {Signal_Name: "ESP_SysSts_EPB"}, {Signal_Name: "ESP_TotBrkTqReq"}, {Signal_Name: "ESP_VLC_APCtrlSaturated"}, {Signal_Name: "ESP_VehSpd"}, {Signal_Name: "ESP_WhlMovgDir_LF"}, {Signal_Name: "ESP_WhlMovgDir_RF"}, {Signal_Name: "ESP_WhlMovgDir_RL"}, {Signal_Name: "ESP_WhlMovgDir_RR"}, {Signal_Name: "ESP_WhlOdoEdgesVld_FL"}, {Signal_Name: "ESP_WhlOdoEdgesVld_FR"}, {Signal_Name: "ESP_WhlOdoEdgesVld_RL"}, {Signal_Name: "ESP_WhlOdoEdgesVld_RR"}, {Signal_Name: "ESP_WhlOdoEdges_FL"}, {Signal_Name: "ESP_WhlOdoEdges_FR"}, {Signal_Name: "ESP_WhlOdoEdges_RL"}, {Signal_Name: "ESP_WhlOdoEdges_RR"}, {Signal_Name: "ESP_WhlSpdVld_LF"}, {Signal_Name: "ESP_WhlSpdVld_RF"}, {Signal_Name: "ESP_WhlSpdVld_RL"}, {Signal_Name: "ESP_WhlSpdVld_RR"}, {Signal_Name: "ESP_WhlSpd_LF"}, {Signal_Name: "ESP_WhlSpd_RF"}, {Signal_Name: "ESP_WhlSpd_RL"}, {Signal_Name: "ESP_WhlSpd_RR"}, {Signal_Name: "EWP_B_SpdRatSts"}, {Signal_Name: "EWP_FD_SpdRatSts"}, {Signal_Name: "EWP_H_SpdRatSts"}, {Signal_Name: "EWP_RD_SpdRatSts"}, {Signal_Name: "FCM_BattVoltBelowThd"}, {Signal_Name: "FCM_BattVoltOverThd"}, {Signal_Name: "IBS_AvailableCapacity"}, {Signal_Name: "IBS_AvgRi"}, {Signal_Name: "IBS_BatteryCurrent"}, {Signal_Name: "IBS_BatteryDefect"}, {Signal_Name: "IBS_BatteryTemperature"}, {Signal_Name: "IBS_BatteryVoltage"}, {Signal_Name: "IBS_CapacityLossBottom"}, {Signal_Name: "IBS_CapacityLossTop"}, {Signal_Name: "IBS_CurrentAutorange"}, {Signal_Name: "IBS_DischargeableAh"}, {Signal_Name: "IBS_EngineStart"}, {Signal_Name: "IBS_Error"}, {Signal_Name: "IBS_EstVoltaDrop"}, {Signal_Name: "IBS_LowVoltage"}, {Signal_Name: "IBS_NominalCapacity"}, {Signal_Name: "IBS_OptChargeVolt"}, {Signal_Name: "IBS_PowerOn"}, {Signal_Name: "IBS_Recalibrated"}, {Signal_Name: "IBS_SOCLowerTolerance"}, {Signal_Name: "IBS_SOCUpperTolerance"}, {Signal_Name: "IBS_StateOfCharge"}, {Signal_Name: "IBS_StateOfHealth"}, {Signal_Name: "IBS_Sulfation"}, {Signal_Name: "IBS_WakeupStatus"}, {Signal_Name: "ICC_AirVolSet"}, {Signal_Name: "ICC_DispVehSpd"}, {Signal_Name: "ICC_DoorCtrlSwtSts"}, {Signal_Name: "ICC_DoorUnlockSet"}, {Signal_Name: "ICC_DrvrTSet"}, {Signal_Name: "ICC_ECCAUTOReq"}, {Signal_Name: "ICC_ECCSysSwtCmd"}, {Signal_Name: "ICC_FrntWiprCtrl"}, {Signal_Name: "ICC_Ota_Inst"}, {Signal_Name: "ICC_PassTSet"}, {Signal_Name: "ICC_RiOutlDamprMotActvCmd"}, {Signal_Name: "ICC_RiOutlLeRiMotActvCmd"}, {Signal_Name: "ICC_RiOutlUpDwnMotActvCmd"}, {Signal_Name: "ICC_SchedUpdate_Selection"}, {Signal_Name: "ICC_SetChrgEndSOC"}, {Signal_Name: "ICC_StartChrgBtn"}, {Signal_Name: "ICC_StopChrgBtn"}, {Signal_Name: "ICC_TotMilg_ODO"}, {Signal_Name: "MCU_F_ActDampTq"}, {Signal_Name: "MCU_F_ActSafeSt"}, {Signal_Name: "MCU_F_AlrmLamp_FS"}, {Signal_Name: "MCU_F_BoostEnStat"}, {Signal_Name: "MCU_F_BoostModActCounter"}, {Signal_Name: "MCU_F_CooltEstimnT"}, {Signal_Name: "MCU_F_CooltOverTFlt"}, {Signal_Name: "MCU_F_CrtMod"}, {Signal_Name: "MCU_F_CrtSpd"}, {Signal_Name: "MCU_F_CrtSts"}, {Signal_Name: "MCU_F_CrtTq"}, {Signal_Name: "MCU_F_DCBusCrt"}, {Signal_Name: "MCU_F_DCBusOverVoltFlt"}, {Signal_Name: "MCU_F_DCBusUnderVoltFlt"}, {Signal_Name: "MCU_F_DCBusVolt"}, {Signal_Name: "MCU_F_EmRotorT"}, {Signal_Name: "MCU_F_EmStatorT"}, {Signal_Name: "MCU_F_HVActvDchaSts"}, {Signal_Name: "MCU_F_IGBTOverTFltU"}, {Signal_Name: "MCU_F_IGBTT"}, {Signal_Name: "MCU_F_LVPwrSplyOverVoltFlt"}, {Signal_Name: "MCU_F_LVPwrSplyUnderVoltFlt"}, {Signal_Name: "MCU_F_MOT_T"}, {Signal_Name: "MCU_F_MaxElecTq"}, {Signal_Name: "MCU_F_MaxPwrGennTq"}, {Signal_Name: "MCU_F_MaxTqLim"}, {Signal_Name: "MCU_F_MinTqLim"}, {Signal_Name: "MCU_F_OverTFlt"}, {Signal_Name: "MCU_F_ParkLockStatus_EV"}, {Signal_Name: "MCU_F_PrkLckStatNrmlOp_EV"}, {Signal_Name: "MCU_F_Sum"}, {Signal_Name: "MCU_R_ActDampTq"}, {Signal_Name: "MCU_R_ActSafeSt"}, {Signal_Name: "MCU_R_AlrmLamp_FS"}, {Signal_Name: "MCU_R_BoostEnStat"}, {Signal_Name: "MCU_R_BoostModActCounter"}, {Signal_Name: "MCU_R_CooltEstimnT"}, {Signal_Name: "MCU_R_CooltOverTFlt"}, {Signal_Name: "MCU_R_CrtMod"}, {Signal_Name: "MCU_R_CrtSpd"}, {Signal_Name: "MCU_R_CrtSts"}, {Signal_Name: "MCU_R_CrtTq"}, {Signal_Name: "MCU_R_DCBusCrt"}, {Signal_Name: "MCU_R_DCBusVolt"}, {Signal_Name: "MCU_R_Decoup_State"}, {Signal_Name: "MCU_R_EmRotorT"}, {Signal_Name: "MCU_R_EmStatorT"}, {Signal_Name: "MCU_R_HVActvDchaSts"}, {Signal_Name: "MCU_R_IGBTOverTFltU"}, {Signal_Name: "MCU_R_IGBTT"}, {Signal_Name: "MCU_R_LVPwrSplyOverVoltFlt"}, {Signal_Name: "MCU_R_LVPwrSplyUnderVoltFlt"}, {Signal_Name: "MCU_R_MOT_T"}, {Signal_Name: "MCU_R_MaxElecTq"}, {Signal_Name: "MCU_R_MaxPwrGennTq"}, {Signal_Name: "MCU_R_MaxTqLim"}, {Signal_Name: "MCU_R_MinTqLim"}, {Signal_Name: "MCU_R_OverTFlt"}, {Signal_Name: "MCU_R_Sum"}, {Signal_Name: "MRR_VCUWhlTqActVldSigInvld"}, {Signal_Name: "MRR_WhlSpdAmpInvld"}, {Signal_Name: "MRR_WhlSpdDirInvld"}, {Signal_Name: "OBC_12VPwrSplyVoltOver"}, {Signal_Name: "OBC_12VPwrSplyVoltUnder"}, {Signal_Name: "OBC_ACCrt"}, {Signal_Name: "OBC_ACCrtPhase1"}, {Signal_Name: "OBC_ACCrtPhase2"}, {Signal_Name: "OBC_ACCrtPhase3"}, {Signal_Name: "OBC_ACVolt"}, {Signal_Name: "OBC_ComLostFlt_VCU"}, {Signal_Name: "OBC_CrtSts"}, {Signal_Name: "OBC_DCChrggEvseInlVolt"}, {Signal_Name: "OBC_DCCrt"}, {Signal_Name: "OBC_DCNegRlyAdhFlt"}, {Signal_Name: "OBC_DCNegRlyCtrlSts"}, {Signal_Name: "OBC_DCPosRlyAdhFlt"}, {Signal_Name: "OBC_DCPosRlyCtrlSts"}, {Signal_Name: "OBC_DCVolt"}, {Signal_Name: "OBC_EASIntlkSigChkCircFlt"}, {Signal_Name: "OBC_EVChrgElectcLockStsFbSig"}, {Signal_Name: "OBC_InpVoltRng"}, {Signal_Name: "OBC_MaxOutpVolt"}, {Signal_Name: "OBC_MinOutpVolt"}, {Signal_Name: "OBC_OperMod"}, {Signal_Name: "OBC_OutpMaxPwr"}, {Signal_Name: "OBC_Pwr"}, {Signal_Name: "OBC_T"}, {Signal_Name: "PKC_DoorOpenRmd"}, {Signal_Name: "PSM_PassSeatT1"}, {Signal_Name: "PSM_PassSeatT2"}, {Signal_Name: "PVIU_DCDC_ActHVCurr"}, {Signal_Name: "PVIU_DCDC_ActHVVolt"}, {Signal_Name: "PVIU_DCDC_ActLVCurr"}, {Signal_Name: "PVIU_DCDC_ActLVVolt"}, {Signal_Name: "PWC_ChrgSts"}, {Signal_Name: "PWC_R_ChrgSts"}, {Signal_Name: "TBOX_California_mode"}, {Signal_Name: "TBOX_Crash_cfm"}, {Signal_Name: "TBOX_CtrlBitVector_Bit4_ActWake"}, {Signal_Name: "TBOX_FlsFlg"}, {Signal_Name: "TBOX_GPSHei"}, {Signal_Name: "TBOX_GPSLati"}, {Signal_Name: "TBOX_GPSLongi"}, {Signal_Name: "TBOX_ICC_FlashPct"}, {Signal_Name: "TBOX_ICC_UpdStrt"}, {Signal_Name: "TBOX_OTAInhbReq"}, {Signal_Name: "TBOX_OTAReq_ECUx"}, {Signal_Name: "TBOX_OTAResFb"}, {Signal_Name: "TBOX_OTA_TiRng"}, {Signal_Name: "TBOX_OTA_ownCondchk"}, {Signal_Name: "TBOX_RemClsTrCmd"}, {Signal_Name: "TBOX_RemCtrlLockCmd"}, {Signal_Name: "TBOX_RemKL15PwrOnReq"}, {Signal_Name: "TBOX_RemSeekCarCtrlCmd"}, {Signal_Name: "TBOX_Remflsh"}, {Signal_Name: "TBOX_ScheduleCharg_Status"}, {Signal_Name: "TBOX_Sw_upd"}, {Signal_Name: "TBOX_VoltHigh"}, {Signal_Name: "TBOX_VoltLow"}, {Signal_Name: "TBOX_eCallSt"}, {Signal_Name: "TRM_IndcrLeSts"}, {Signal_Name: "TRM_IndcrRiSts"}, {Signal_Name: "TRM_TrailerSts"}, {Signal_Name: "VCU_ACChrgCrtUpprLmt"}, {Signal_Name: "VCU_ACChrgDchaGunCnctnSts"}, {Signal_Name: "VCU_ACChrgDchaIndcrLampSts"}, {Signal_Name: "VCU_ACChrgDchaSoktT1"}, {Signal_Name: "VCU_ACChrgShttrSts"}, {Signal_Name: "VCU_ACChrgStrtFailReason"}, {Signal_Name: "VCU_ACDCChrgIndcrLamp12VFlt"}, {Signal_Name: "VCU_ACDchaStrtFailReason"}, {Signal_Name: "VCU_APSPerc"}, {Signal_Name: "VCU_AccelModFb"}, {Signal_Name: "VCU_ActChrgTotAh"}, {Signal_Name: "VCU_ActDchgTotAh"}, {Signal_Name: "VCU_ActRgnTq_CRB"}, {Signal_Name: "VCU_ActvDchaEnaFlg"}, {Signal_Name: "VCU_BMSLVWakeUpSts"}, {Signal_Name: "VCU_BattChrgSOC"}, {Signal_Name: "VCU_BattChrgSOCEEPROM"}, {Signal_Name: "VCU_BattFltIndcn"}, {Signal_Name: "VCU_BattVolt"}, {Signal_Name: "VCU_BoostCmd_F"}, {Signal_Name: "VCU_BoostCmd_R"}, {Signal_Name: "VCU_BoostEnStat"}, {Signal_Name: "VCU_BoostEnStatCoded"}, {Signal_Name: "VCU_BrkLampCtrlSts"}, {Signal_Name: "VCU_BrkPedlPwrSplyFlt"}, {Signal_Name: "VCU_BrkPedlSts_GB"}, {Signal_Name: "VCU_BrkSig"}, {Signal_Name: "VCU_BrkSigVld"}, {Signal_Name: "VCU_BrkStrtReq"}, {Signal_Name: "VCU_CcTrgSpdDisp"}, {Signal_Name: "VCU_CellMaxVolt"}, {Signal_Name: "VCU_CellMinVolt"}, {Signal_Name: "VCU_CellVoltLoHVPwrOffFlg"}, {Signal_Name: "VCU_ChrgDchaGunCnctnIndcrLamp"}, {Signal_Name: "VCU_ChrgEndSOCCrtCtrlFlg"}, {Signal_Name: "VCU_ChrgIndcrLamp"}, {Signal_Name: "VCU_ChrgModBHMCnseEgyPerc"}, {Signal_Name: "VCU_ChrgModBattHeatgMngCnseEgy"}, {Signal_Name: "VCU_ChrgModChrgEffCnseEgy"}, {Signal_Name: "VCU_ChrgModChrgEffCnseEgyPerc"}, {Signal_Name: "VCU_ChrgModVehChrgEgy"}, {Signal_Name: "VCU_ChrgPwrLim"}, {Signal_Name: "VCU_ChrgSts"}, {Signal_Name: "VCU_ChrgSts_GB"}, {Signal_Name: "VCU_ChrgSysOperCmd"}, {Signal_Name: "VCU_CoastTqReq"}, {Signal_Name: "VCU_ComChkFlt_BMS"}, {Signal_Name: "VCU_ComChkFlt_ESP"}, {Signal_Name: "VCU_ComChkFlt_VCU"}, {Signal_Name: "VCU_ComLostFlt_ABS"}, {Signal_Name: "VCU_ComLostFlt_BCM"}, {Signal_Name: "VCU_ComLostFlt_BMS"}, {Signal_Name: "VCU_ComLostFlt_BOBC"}, {Signal_Name: "VCU_ComLostFlt_DCDC"}, {Signal_Name: "VCU_ComLostFlt_EHU"}, {Signal_Name: "VCU_ComLostFlt_ESP"}, {Signal_Name: "VCU_ComLostFlt_ICC"}, {Signal_Name: "VCU_ComLostFlt_ICM"}, {Signal_Name: "VCU_ComLostFlt_MCU_F"}, {Signal_Name: "VCU_ComLostFlt_NBS"}, {Signal_Name: "VCU_ComLostFlt_OBC"}, {Signal_Name: "VCU_ComLostFlt_PEPS"}, {Signal_Name: "VCU_ComLostFlt_TBOX"}, {Signal_Name: "VCU_ComLostFlt_VCU"}, {Signal_Name: "VCU_CrashDIRSigPWMPct"}, {Signal_Name: "VCU_CrtSts"}, {Signal_Name: "VCU_DCChrgCrtCmd"}, {Signal_Name: "VCU_DCChrgDchaGunCnctnSts"}, {Signal_Name: "VCU_DCChrgDchaIndcrLampSts"}, {Signal_Name: "VCU_DCChrgOutpCrt"}, {Signal_Name: "VCU_DCChrgOutpVolt"}, {Signal_Name: "VCU_DCChrgRmngTi"}, {Signal_Name: "VCU_DCChrgShttrSts"}, {Signal_Name: "VCU_DCChrgSoktT1"}, {Signal_Name: "VCU_DCChrgSoktT2"}, {Signal_Name: "VCU_DCCrtCmd_OBC"}, {Signal_Name: "VCU_DCDCEnaCmd"}, {Signal_Name: "VCU_DCDCOutpVoltCmd"}, {Signal_Name: "VCU_DCPosRlyCtrlCmd"}, {Signal_Name: "VCU_DCRlyCtrlCmd"}, {Signal_Name: "VCU_DCVoltCmd_OBC"}, {Signal_Name: "VCU_DDmdTq"}, {Signal_Name: "VCU_Decel_ReqType"}, {Signal_Name: "VCU_Decoup_Rq_Mod"}, {Signal_Name: "VCU_DrvModShiftMisoper"}, {Signal_Name: "VCU_DrvModSigFb"}, {Signal_Name: "VCU_DrvMotOutpPwrPerc"}, {Signal_Name: "VCU_DrvTqLimSts"}, {Signal_Name: "VCU_DrvgMilg"}, {Signal_Name: "VCU_DrvrFrntMotTqReq"}, {Signal_Name: "VCU_DrvrMotNr_GB"}, {Signal_Name: "VCU_DrvrReMotTqReq"}, {Signal_Name: "VCU_ECCEnaCmd"}, {Signal_Name: "VCU_ECCSysAllwMaxPwrCnse"}, {Signal_Name: "VCU_ECC_PwrMax"}, {Signal_Name: "VCU_ECC_PwrMin"}, {Signal_Name: "VCU_EPBReq"}, {Signal_Name: "VCU_EPB_Req"}, {Signal_Name: "VCU_EVCANBusFlt"}, {Signal_Name: "VCU_EgyCnsHist01"}, {Signal_Name: "VCU_EmgyShutOff"}, {Signal_Name: "VCU_EvChrgElectcLockCtrlCmd"}, {Signal_Name: "VCU_FCChrgCnt"}, {Signal_Name: "VCU_FMCUHVSelfChkTiOut"}, {Signal_Name: "VCU_F_MnTqLim"}, {Signal_Name: "VCU_F_MxTqGrad"}, {Signal_Name: "VCU_F_MxTqLim"}, {Signal_Name: "VCU_F_cmdSlipCtrlComp"}, {Signal_Name: "VCU_F_rspdEmMax"}, {Signal_Name: "VCU_F_rspdEmMin"}, {Signal_Name: "VCU_FrntCrtWhlTq"}, {Signal_Name: "VCU_FrntMotModCmd"}, {Signal_Name: "VCU_FrntMotTarTqCmd"}, {Signal_Name: "VCU_FrntMotTarTqCmdVld"}, {Signal_Name: "VCU_GearSig"}, {Signal_Name: "VCU_GearSig_GB"}, {Signal_Name: "VCU_HVBActivateDeactivateRq"}, {Signal_Name: "VCU_HVBattActPwr"}, {Signal_Name: "VCU_HVCustSOC"}, {Signal_Name: "VCU_HVEMBoostModSt"}, {Signal_Name: "VCU_HVPwrOffReq"}, {Signal_Name: "VCU_IsolationMonitor_Rq"}, {Signal_Name: "VCU_KL15Req"}, {Signal_Name: "VCU_KickdwnFlg"}, {Signal_Name: "VCU_LVWakeUpSts_CCU"}, {Signal_Name: "VCU_LVWakeUpSts_MCU"}, {Signal_Name: "VCU_LVWakeUpSts_PDU"}, {Signal_Name: "VCU_LVbattCtrlSigOpenFlt"}, {Signal_Name: "VCU_LVbattCtrlSigSTBFlt"}, {Signal_Name: "VCU_LVbattCtrlSigSTGFlt"}, {Signal_Name: "VCU_Lvl2ProtStrtFlg"}, {Signal_Name: "VCU_MaxBoostCntr"}, {Signal_Name: "VCU_NetWakeUpSig"}, {Signal_Name: "VCU_NotChrgModAcsyCnseEgy"}, {Signal_Name: "VCU_NotChrgModAcsyCnseEgyPerc"}, {Signal_Name: "VCU_NotChrgModBHMCnseEgy"}, {Signal_Name: "VCU_NotChrgModBHMCnseEgyPerc"}, {Signal_Name: "VCU_NotChrgModECCCnseEgy"}, {Signal_Name: "VCU_NotChrgModECCCnseEgyPerc"}, {Signal_Name: "VCU_NotChrgModExtDchaCnseEgy"}, {Signal_Name: "VCU_NotChrgModExtDchaCnseEgyPerc"}, {Signal_Name: "VCU_NotChrgModVehDrvCnseEgy"}, {Signal_Name: "VCU_NotChrgModVehDrvCnseEgyPerc"}, {Signal_Name: "VCU_NotChrgModVehTotCnseEgy"}, {Signal_Name: "VCU_OTAInhbRdy"}, {Signal_Name: "VCU_OTARdy_Fb"}, {Signal_Name: "VCU_OTAVehCdnChk_ASIL"}, {Signal_Name: "VCU_OTAVehCdnChk_QM_sts"}, {Signal_Name: "VCU_OTA_ECUInhb_Req"}, {Signal_Name: "VCU_OutpACWakeupSigSts"}, {Signal_Name: "VCU_OutpFCWakeupSigSts"}, {Signal_Name: "VCU_PPrkgFltLvl"}, {Signal_Name: "VCU_ParkRdy"}, {Signal_Name: "VCU_PrefillReq"}, {Signal_Name: "VCU_PwrBattECCEnaCmd"}, {Signal_Name: "VCU_PwrSplyEquipSts"}, {Signal_Name: "VCU_RMCUHVSelfChkTiOut"}, {Signal_Name: "VCU_R_MnTqLim"}, {Signal_Name: "VCU_R_MxTqGrad"}, {Signal_Name: "VCU_R_MxTqLim"}, {Signal_Name: "VCU_R_rspdEmMax"}, {Signal_Name: "VCU_R_rspdEmMin"}, {Signal_Name: "VCU_RdyLamp"}, {Signal_Name: "VCU_ReCrtWhlTq"}, {Signal_Name: "VCU_ReMotDampgFobdFlg"}, {Signal_Name: "VCU_ReMotModCmd"}, {Signal_Name: "VCU_ReMotTarTqCmd"}, {Signal_Name: "VCU_RegenLvlFb"}, {Signal_Name: "VCU_RemBattHeatgFailReason"}, {Signal_Name: "VCU_RemChrgEndCmd"}, {Signal_Name: "VCU_RemChrgEndReason"}, {Signal_Name: "VCU_RemCtrlDrvgBrkReq"}, {Signal_Name: "VCU_RemCtrlDrvgEmgyBrkReq"}, {Signal_Name: "VCU_RemECCEndCmd"}, {Signal_Name: "VCU_RemECCEndReason"}, {Signal_Name: "VCU_RmnUsrECCFctLmtDispCmd"}, {Signal_Name: "VCU_SCChrgCnt"}, {Signal_Name: "VCU_SOCLoWakeUpEnaCmd"}, {Signal_Name: "VCU_SOCLoWakeUpThd"}, {Signal_Name: "VCU_SOCTooHiAlrm"}, {Signal_Name: "VCU_SOCTooLoAlrm"}, {Signal_Name: "VCU_SchedChrgnStsFb"}, {Signal_Name: "VCU_Set_BMS_Mod"}, {Signal_Name: "VCU_Set_Balc_Mod"}, {Signal_Name: "VCU_Set_Chrg_Mod"}, {Signal_Name: "VCU_ShiftMisoper"}, {Signal_Name: "VCU_ShiftOperRmn"}, {Signal_Name: "VCU_SlipRate"}, {Signal_Name: "VCU_StgyGearSig"}, {Signal_Name: "VCU_Sts_CC_ICC"}, {Signal_Name: "VCU_SwVersFlash"}, {Signal_Name: "VCU_SwVersMain"}, {Signal_Name: "VCU_SwVersMinor"}, {Signal_Name: "VCU_SwVersPatch"}, {Signal_Name: "VCU_SysAllwMaxFbPwr"}, {Signal_Name: "VCU_SysAllwMaxPwrCnse"}, {Signal_Name: "VCU_TotOverVoltLvl2ProtFlg"}, {Signal_Name: "VCU_TqLim"}, {Signal_Name: "VCU_TripAvgEgyConsmd"}, {Signal_Name: "VCU_TripMilg"}, {Signal_Name: "VCU_VcuErrBitVect"}, {Signal_Name: "VCU_VcuErrCtgy"}, {Signal_Name: "VCU_VcuState"}, {Signal_Name: "VCU_VehChrgDchgMod"}, {Signal_Name: "VCU_VehCrtChrgEndSOC"}, {Signal_Name: "VCU_VehCtrlrEEPROMFlt"}, {Signal_Name: "VCU_VehCtrlrRAMFlt"}, {Signal_Name: "VCU_VehCtrlrROMFlt"}, {Signal_Name: "VCU_VehExtDchaSts"}, {Signal_Name: "VCU_VehMCUFMaxElecTq"}, {Signal_Name: "VCU_VehMCUFMaxPwrGennTq"}, {Signal_Name: "VCU_VehMCURMaxElecTq"}, {Signal_Name: "VCU_VehMCURMaxPwrGennTq"}, {Signal_Name: "VCU_VehMaxWhlTq"}, {Signal_Name: "VCU_VehMod"}, {Signal_Name: "VCU_VehOperMod"}, {Signal_Name: "VCU_VehRemOperModPwrOnReq"}, {Signal_Name: "VCU_VehRemSCModPwrOnReq"}, {Signal_Name: "VCU_VehSt"}, {Signal_Name: "VCU_VehSts"}, {Signal_Name: "VCU_VehTqDistbnRat"}, {Signal_Name: "VCU_VehTqFltSts"}, {Signal_Name: "VCU_WhlTqAct"}, {Signal_Name: "WTC_B_ActGear"}, {Signal_Name: "WTC_B_ComLost_ECC"}, {Signal_Name: "WTC_B_Crt"}, {Signal_Name: "WTC_B_CrtSts"}, {Signal_Name: "WTC_B_CrtT"}, {Signal_Name: "WTC_B_InlT"}, {Signal_Name: "WTC_H_ActGear"}, {Signal_Name: "WTC_H_Crt"}, {Signal_Name: "WTC_H_CrtPwr"}, {Signal_Name: "WTC_H_CrtSts"}, {Signal_Name: "WTC_H_CrtT"}, {Signal_Name: "WTC_H_InpT"}, {Signal_Name: "XXX_OTAInhbRdy"}, {Signal_Name: "XXX_OTARdy_Fb"}, {Signal_Name: "YRS_LatAcce"}, {Signal_Name: "YRS_LgtAcce"}, {Signal_Name: "iBooster_BrkPedlAppldFlg"}, {Signal_Name: "iBooster_BrkPedlAppldVld"}, {Signal_Name: "iBooster_DrvrBrkPedlTrvl"}, {Signal_Name: "iBooster_DrvrBrkPedlTrvlSts"}, {Signal_Name: "iBooster_ExtBrkReqSts"}, {Signal_Name: "iBooster_MstCylBrkPedlActTrvl"}, {Signal_Name: "iBooster_MstCylBrkPedlActTrvlSts"}} diff --git a/services/ota_update_go/handlers/car_configuration_update.go b/services/ota_update_go/handlers/car_configuration_update.go new file mode 100644 index 0000000..a52a25d --- /dev/null +++ b/services/ota_update_go/handlers/car_configuration_update.go @@ -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 " +// @Param Api-Key header string false "" +// @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", + }) +} diff --git a/services/ota_update_go/handlers/car_software_information.go b/services/ota_update_go/handlers/car_software_information.go new file mode 100644 index 0000000..81ec0b1 --- /dev/null +++ b/services/ota_update_go/handlers/car_software_information.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/car_software_information_v2.go b/services/ota_update_go/handlers/car_software_information_v2.go new file mode 100644 index 0000000..a4ed5aa --- /dev/null +++ b/services/ota_update_go/handlers/car_software_information_v2.go @@ -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 " +// @Param Api-Key header string false "" +// @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"} \ No newline at end of file diff --git a/services/ota_update_go/handlers/cars_allowed_access.go b/services/ota_update_go/handlers/cars_allowed_access.go new file mode 100644 index 0000000..1e66a8c --- /dev/null +++ b/services/ota_update_go/handlers/cars_allowed_access.go @@ -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 " +// @Param Api-Key header string false "" +// @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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/cars_by_manifest.go b/services/ota_update_go/handlers/cars_by_manifest.go new file mode 100644 index 0000000..72255ce --- /dev/null +++ b/services/ota_update_go/handlers/cars_by_manifest.go @@ -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 " +// @Param Api-Key header string false "" +// @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, + }) +} diff --git a/services/ota_update_go/handlers/cars_by_manifest_get_test.go b/services/ota_update_go/handlers/cars_by_manifest_get_test.go new file mode 100644 index 0000000..33981f9 --- /dev/null +++ b/services/ota_update_go/handlers/cars_by_manifest_get_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/cars_change_access.go b/services/ota_update_go/handlers/cars_change_access.go new file mode 100644 index 0000000..a43da17 --- /dev/null +++ b/services/ota_update_go/handlers/cars_change_access.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/carupdate_add.go b/services/ota_update_go/handlers/carupdate_add.go new file mode 100644 index 0000000..01b7bd9 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_add.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/carupdate_add_test.go b/services/ota_update_go/handlers/carupdate_add_test.go new file mode 100644 index 0000000..61268a2 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_add_test.go @@ -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 +} diff --git a/services/ota_update_go/handlers/carupdate_cancel.go b/services/ota_update_go/handlers/carupdate_cancel.go new file mode 100644 index 0000000..8b8efe6 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_cancel.go @@ -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 " +// @Param Api-Key header string false "" +// @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(¤t) + 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(¤t) + 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 +} diff --git a/services/ota_update_go/handlers/carupdate_cancel_test.go b/services/ota_update_go/handlers/carupdate_cancel_test.go new file mode 100644 index 0000000..4e1f72c --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_cancel_test.go @@ -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)) + } + } + } +} diff --git a/services/ota_update_go/handlers/carupdate_delete.go b/services/ota_update_go/handlers/carupdate_delete.go new file mode 100644 index 0000000..6842721 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_delete.go @@ -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 " +// @Param Api-Key header string false "" +// @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", + }) +} diff --git a/services/ota_update_go/handlers/carupdate_delete_test.go b/services/ota_update_go/handlers/carupdate_delete_test.go new file mode 100644 index 0000000..64f83e6 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_delete_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/carupdate_deploy.go b/services/ota_update_go/handlers/carupdate_deploy.go new file mode 100644 index 0000000..aabb8b8 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_deploy.go @@ -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 " +// @Param Api-Key header string false "" +// @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(¤t) + 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 + } +} diff --git a/services/ota_update_go/handlers/carupdate_deploy_test.go b/services/ota_update_go/handlers/carupdate_deploy_test.go new file mode 100644 index 0000000..b547309 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_deploy_test.go @@ -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)) + } + } + } +} diff --git a/services/ota_update_go/handlers/carupdate_vehicle_cancel.go b/services/ota_update_go/handlers/carupdate_vehicle_cancel.go new file mode 100644 index 0000000..a2d8f44 --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_vehicle_cancel.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/carupdate_vehicle_cancel_test.go b/services/ota_update_go/handlers/carupdate_vehicle_cancel_test.go new file mode 100644 index 0000000..7b2df2a --- /dev/null +++ b/services/ota_update_go/handlers/carupdate_vehicle_cancel_test.go @@ -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)) + } + } + } +} diff --git a/services/ota_update_go/handlers/carupdates_get.go b/services/ota_update_go/handlers/carupdates_get.go new file mode 100644 index 0000000..6cd89a8 --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/carupdates_get_test.go b/services/ota_update_go/handlers/carupdates_get_test.go new file mode 100644 index 0000000..821d076 --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_get_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/carupdates_log.go b/services/ota_update_go/handlers/carupdates_log.go new file mode 100644 index 0000000..69bae34 --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_log.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/carupdates_log_test.go b/services/ota_update_go/handlers/carupdates_log_test.go new file mode 100644 index 0000000..6f4568f --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_log_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/carupdates_statuses.go b/services/ota_update_go/handlers/carupdates_statuses.go new file mode 100644 index 0000000..1507447 --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_statuses.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/carupdates_statuses_test.go b/services/ota_update_go/handlers/carupdates_statuses_test.go new file mode 100644 index 0000000..bb7978a --- /dev/null +++ b/services/ota_update_go/handlers/carupdates_statuses_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/customer_ota_emails_handler.go b/services/ota_update_go/handlers/customer_ota_emails_handler.go new file mode 100644 index 0000000..f418fa8 --- /dev/null +++ b/services/ota_update_go/handlers/customer_ota_emails_handler.go @@ -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 " +// @Param Api-Key header string false "" +// @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 + } +} diff --git a/services/ota_update_go/handlers/customer_ota_emails_handler_test.go b/services/ota_update_go/handlers/customer_ota_emails_handler_test.go new file mode 100644 index 0000000..616a626 --- /dev/null +++ b/services/ota_update_go/handlers/customer_ota_emails_handler_test.go @@ -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 +} diff --git a/services/ota_update_go/handlers/dbc_signals_get.go b/services/ota_update_go/handlers/dbc_signals_get.go new file mode 100644 index 0000000..18852e8 --- /dev/null +++ b/services/ota_update_go/handlers/dbc_signals_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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}) +} diff --git a/services/ota_update_go/handlers/dbc_signals_get_test.go b/services/ota_update_go/handlers/dbc_signals_get_test.go new file mode 100644 index 0000000..093cb40 --- /dev/null +++ b/services/ota_update_go/handlers/dbc_signals_get_test.go @@ -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()) + }) + } +} diff --git a/services/ota_update_go/handlers/digital_twin_signal_timestamp.go b/services/ota_update_go/handlers/digital_twin_signal_timestamp.go new file mode 100644 index 0000000..6d25b2d --- /dev/null +++ b/services/ota_update_go/handlers/digital_twin_signal_timestamp.go @@ -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 "" +// @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) +} diff --git a/services/ota_update_go/handlers/docs.go b/services/ota_update_go/handlers/docs.go new file mode 100644 index 0000000..ea1adea --- /dev/null +++ b/services/ota_update_go/handlers/docs.go @@ -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, ",") +} diff --git a/services/ota_update_go/handlers/docs_test.go b/services/ota_update_go/handlers/docs_test.go new file mode 100644 index 0000000..628cc09 --- /dev/null +++ b/services/ota_update_go/handlers/docs_test.go @@ -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 := "Swagger UI" + 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) + } +} diff --git a/services/ota_update_go/handlers/dtc_ecu_get.go b/services/ota_update_go/handlers/dtc_ecu_get.go new file mode 100644 index 0000000..346ee55 --- /dev/null +++ b/services/ota_update_go/handlers/dtc_ecu_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/dtc_ecu_get_test.go b/services/ota_update_go/handlers/dtc_ecu_get_test.go new file mode 100644 index 0000000..b9c992d --- /dev/null +++ b/services/ota_update_go/handlers/dtc_ecu_get_test.go @@ -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()) + }) + } + */ +} diff --git a/services/ota_update_go/handlers/ecu_stats_get.go b/services/ota_update_go/handlers/ecu_stats_get.go new file mode 100644 index 0000000..10b29bd --- /dev/null +++ b/services/ota_update_go/handlers/ecu_stats_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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.Valueb.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 +} diff --git a/services/ota_update_go/handlers/ecu_stats_get_test.go b/services/ota_update_go/handlers/ecu_stats_get_test.go new file mode 100644 index 0000000..73cd698 --- /dev/null +++ b/services/ota_update_go/handlers/ecu_stats_get_test.go @@ -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()) + }) + } +} diff --git a/services/ota_update_go/handlers/ecu_stats_vin_get.go b/services/ota_update_go/handlers/ecu_stats_vin_get.go new file mode 100644 index 0000000..d584e84 --- /dev/null +++ b/services/ota_update_go/handlers/ecu_stats_vin_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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.Valueb.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 +} diff --git a/services/ota_update_go/handlers/ecu_stats_vin_get_test.go b/services/ota_update_go/handlers/ecu_stats_vin_get_test.go new file mode 100644 index 0000000..4be93bf --- /dev/null +++ b/services/ota_update_go/handlers/ecu_stats_vin_get_test.go @@ -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()) + }) + } +} diff --git a/services/ota_update_go/handlers/errors.go b/services/ota_update_go/handlers/errors.go new file mode 100644 index 0000000..492408e --- /dev/null +++ b/services/ota_update_go/handlers/errors.go @@ -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") diff --git a/services/ota_update_go/handlers/experiment.go b/services/ota_update_go/handlers/experiment.go new file mode 100644 index 0000000..bf3f38f --- /dev/null +++ b/services/ota_update_go/handlers/experiment.go @@ -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, ` + + + + %s + + + + + + + +

Open Graph Page for %s

+

Preview metadata has been set in the HTML headers.

+ +`, title, title, url, image, id, id) +} diff --git a/services/ota_update_go/handlers/external_driver_handlers.go b/services/ota_update_go/handlers/external_driver_handlers.go new file mode 100644 index 0000000..259e104 --- /dev/null +++ b/services/ota_update_go/handlers/external_driver_handlers.go @@ -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 " +// @Param Api-Key header string false "" +// @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 " +// @Param Api-Key header string false "" +// @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 " +// // @Param Api-Key header string false "" +// // @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") diff --git a/services/ota_update_go/handlers/flashpack_version_add.go b/services/ota_update_go/handlers/flashpack_version_add.go new file mode 100644 index 0000000..f6fbeb4 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_add.go @@ -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 " +// @Param Api-Key header string false "" +// @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", + }) +} diff --git a/services/ota_update_go/handlers/flashpack_version_add_test.go b/services/ota_update_go/handlers/flashpack_version_add_test.go new file mode 100644 index 0000000..bfe167a --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_add_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/flashpack_version_delete.go b/services/ota_update_go/handlers/flashpack_version_delete.go new file mode 100644 index 0000000..2e7c6a2 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_delete.go @@ -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 " +// @Param Api-Key header string false "" +// @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", + }) +} diff --git a/services/ota_update_go/handlers/flashpack_version_delete_test.go b/services/ota_update_go/handlers/flashpack_version_delete_test.go new file mode 100644 index 0000000..97341c2 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_delete_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get.go b/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get.go new file mode 100644 index 0000000..dd3aa16 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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, + }) +} diff --git a/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get_test.go b/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get_test.go new file mode 100644 index 0000000..b62ad76 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_ecu_mappings_get_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/flashpack_version_info_get.go b/services/ota_update_go/handlers/flashpack_version_info_get.go new file mode 100644 index 0000000..2032a9e --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_info_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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, + }, + }) + } +} diff --git a/services/ota_update_go/handlers/flashpack_version_info_get_test.go b/services/ota_update_go/handlers/flashpack_version_info_get_test.go new file mode 100644 index 0000000..651b675 --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_version_info_get_test.go @@ -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", + }, + }, + } +} diff --git a/services/ota_update_go/handlers/flashpack_versions_get_all.go b/services/ota_update_go/handlers/flashpack_versions_get_all.go new file mode 100644 index 0000000..ee6396d --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_versions_get_all.go @@ -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 " +// @Param Api-Key header string false "" +// @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, + }) +} diff --git a/services/ota_update_go/handlers/flashpack_versions_get_all_test.go b/services/ota_update_go/handlers/flashpack_versions_get_all_test.go new file mode 100644 index 0000000..293b9bc --- /dev/null +++ b/services/ota_update_go/handlers/flashpack_versions_get_all_test.go @@ -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) +} diff --git a/services/ota_update_go/handlers/fleet_add.go b/services/ota_update_go/handlers/fleet_add.go new file mode 100644 index 0000000..626d379 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_add.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/fleet_add_test.go b/services/ota_update_go/handlers/fleet_add_test.go new file mode 100644 index 0000000..1a5b440 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_add_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_delete.go b/services/ota_update_go/handlers/fleet_delete.go new file mode 100644 index 0000000..a760120 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_delete.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/fleet_delete_test.go b/services/ota_update_go/handlers/fleet_delete_test.go new file mode 100644 index 0000000..31d0ca3 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_delete_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_filter_add.go b/services/ota_update_go/handlers/fleet_filter_add.go new file mode 100644 index 0000000..596917a --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_add.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/fleet_filter_add_test.go b/services/ota_update_go/handlers/fleet_filter_add_test.go new file mode 100644 index 0000000..b7d7951 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_add_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_filter_delete.go b/services/ota_update_go/handlers/fleet_filter_delete.go new file mode 100644 index 0000000..637c3f4 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_delete.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/fleet_filter_delete_test.go b/services/ota_update_go/handlers/fleet_filter_delete_test.go new file mode 100644 index 0000000..9df2afe --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_delete_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_filter_get_list.go b/services/ota_update_go/handlers/fleet_filter_get_list.go new file mode 100644 index 0000000..8152a5f --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_get_list.go @@ -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 " +// @Param Api-Key header string false "" +// @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) +} diff --git a/services/ota_update_go/handlers/fleet_filter_get_list_test.go b/services/ota_update_go/handlers/fleet_filter_get_list_test.go new file mode 100644 index 0000000..73a4645 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_get_list_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_filter_update.go b/services/ota_update_go/handlers/fleet_filter_update.go new file mode 100644 index 0000000..88389cc --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_update.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/fleet_filter_update_test.go b/services/ota_update_go/handlers/fleet_filter_update_test.go new file mode 100644 index 0000000..7169a02 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_filter_update_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_get.go b/services/ota_update_go/handlers/fleet_get.go new file mode 100644 index 0000000..e6bf6ed --- /dev/null +++ b/services/ota_update_go/handlers/fleet_get.go @@ -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 " +// @Param Api-Key header string false "" +// @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"` +} diff --git a/services/ota_update_go/handlers/fleet_get_list.go b/services/ota_update_go/handlers/fleet_get_list.go new file mode 100644 index 0000000..51bde21 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_get_list.go @@ -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 " +// @Param Api-Key header string false "" +// @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)) +} diff --git a/services/ota_update_go/handlers/fleet_get_list_test.go b/services/ota_update_go/handlers/fleet_get_list_test.go new file mode 100644 index 0000000..309695b --- /dev/null +++ b/services/ota_update_go/handlers/fleet_get_list_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_get_test.go b/services/ota_update_go/handlers/fleet_get_test.go new file mode 100644 index 0000000..14282ef --- /dev/null +++ b/services/ota_update_go/handlers/fleet_get_test.go @@ -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") +} diff --git a/services/ota_update_go/handlers/fleet_update.go b/services/ota_update_go/handlers/fleet_update.go new file mode 100644 index 0000000..d857c42 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_update.go @@ -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 " +// @Param Api-Key header string false "" +// @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 +} diff --git a/services/ota_update_go/handlers/fleet_update_test.go b/services/ota_update_go/handlers/fleet_update_test.go new file mode 100644 index 0000000..4021664 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_update_test.go @@ -0,0 +1,144 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + m "github.com/fiskerinc/cloud-services/pkg/common" + dbtc "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + rtc "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + tr "github.com/fiskerinc/cloud-services/pkg/testrunner" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +func TestFleetUpdate(t *testing.T) { + mockDB := dbtc.MockCars{} + services.GetDB().SetCars(&mockDB) + vin := "1G1FP87S3GN100062" + trexKey := m.TRex.Key(vin) + cacheKey := redis.CarConfigKey(vin) + + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mockMongoFleets := mongo.NewFleetsCollection(&mongo.MockCollection{}) + mockMongoVehicles := mongo.NewVehiclesCollection(&mongo.MockCollection{}) + client.SetFleets(mockMongoFleets) + client.SetVehicles(mockMongoVehicles) + + mockRedis := rtc.MockRedis{} + services.SetRedisClientPool(rtc.NewMockClientPool(&mockRedis)) + + tests := []tr.TestCase{ + { + Name: "No data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/TESTFLEET", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + }, + { + Name: "Invalid data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/TESTFLEET", mongo.Fleet{Name: "TEST_FLEET"}), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + }, + { + Name: "Valid data 1", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/TESTFLEET", mongo.Fleet{Name: "TESTFLEET"}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"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}`, + }, + RedisTestCase: &rtc.RedisTestCase{ + ExpectedMessages: nil, + ExpectedCaches: nil, + }, + }, + { + Name: "Valid data 2", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/TESTFLEET", + mongo.Fleet{ + VehiclesCount: 1, + Vehicles: []string{vin}, + Name: "TESTFLEET", + LogLevel: m.Info, + CANBus: m.CANBus{ + Enabled: true, + DTCEnabled: elptr.ElPtr(false), + }, + DebugMask: "12", + IDPSEnabled: true, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"name":"TESTFLEET","log_level":"info","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":true,"debug_mask":"12","tags":null,"vehicles":["1G1FP87S3GN100062"],"vehicles_count":1}`, + Setup: func() { + mockMongoFleets = mongo.NewFleetsCollection(&mongo.MockCollection{ + AggregateObject: []mongo.Fleet{ + { + Name: "US-TEST", + Vehicles: []string{ + vin, + }, + }, + }, + }) + client.SetFleets(mockMongoFleets) + }, + }, + RedisTestCase: &rtc.RedisTestCase{ + ExpectedMessages: map[string]string{ + trexKey: `{"data":{"canbus":{"data_logger_enabled":false,"dtc_enabled":false,"enabled":true},"idps_enabled":true,"log_level":"info"},"handler":"config"}`, + }, + ExpectedCaches: map[string]rtc.ExpiringCacheResult{ + cacheKey: {Value: "DELETED"}, + }, + }, + }, + } + + schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex) + for _, test := range tests { + mockRedis.Reset() + + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mockDB) + } + if test.RedisTestCase != nil { + test.RedisTestCase.SetupRedis(&mockRedis) + } + + if test.HttpTestCase != nil { + if test.HttpTestCase.Setup != nil { + test.HttpTestCase.Setup() + } + + w := test.HttpTestCase.TestWithParamPath(handlers.HandleFleetUpdate, "/fleet/:name") + 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) + + for _, mes := range test.RedisTestCase.ExpectedMessages { + schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes)) + } + } + } +} diff --git a/services/ota_update_go/handlers/fleet_vehicle_get_list.go b/services/ota_update_go/handlers/fleet_vehicle_get_list.go new file mode 100644 index 0000000..0a55870 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicle_get_list.go @@ -0,0 +1,134 @@ +package handlers + +import ( + "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/logger" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/julienschmidt/httprouter" +) + +// HandleFleetVehicleGetList godoc +// @Summary Get vehicles for fleet +// @Description Get vehicles for fleet +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @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}/vehicles [get] +func HandleFleetVehicleGetList(w http.ResponseWriter, r *http.Request) { + fleetVehiclesGetList.Handle(w, r) +} + +var fleetVehiclesGetList = controllers.NewMongoGetList(&fleetVehiclesGetListHelper{}) + +type fleetVehiclesGetListHelper struct{} + +func (h *fleetVehiclesGetListHelper) NewModel() interface{} { + return &mongo.Fleet{} +} + +func (h *fleetVehiclesGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) { + filter := model.(*mongo.Fleet) + + params := httprouter.ParamsFromContext(r.Context()) + filter.Name = params.ByName("name") + + filter.SetSearchQuery(r.URL.Query().Get("search")) +} + +func (h *fleetVehiclesGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) { + // does not utilize URL queries so leave this function empty +} + +func (h *fleetVehiclesGetListHelper) 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 *fleetVehiclesGetListHelper) QueryCount(filter interface{}) (int64, error) { + client, err := services.GetMongoClient() + if err != nil { + return 0, err + } + + f := filter.(*mongo.Fleet) + + return client.GetFleets().GetVehiclesForFleetCount(f.Name, f.SearchQuery()) +} + +func (h *fleetVehiclesGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + f := filter.(*mongo.Fleet) + + vins, err := client.GetFleets().GetVehiclesForFleet(f.Name, f.SearchQuery(), options) + if err != nil { + return nil, err + } + + var response []fleetVehicle + + // get CarUpdate + carUpdates := services.GetDB().GetCarUpdates() + ups, _ := carUpdates.SelectMostRecentByVINs(vins) + + // setup CarState + clientPool := services.RedisClientPool() + + // merge Vin, CarUpdate, CarState + parser := cache.NewVehicleState(clientPool) + for _, vin := range vins { + state, err := parser.Get(vin) + if err != nil { + state = common.CarState{} + logger.Warn().Err(err).Send() + } + + vehicle := fleetVehicle{ + VIN: vin, + CarState: &state, + CarUpdate: &common.CarUpdate{}, + } + + for _, carUpdate := range ups { + if carUpdate.VIN == vin { + vehicle.CarUpdate = &carUpdate + break + } + } + response = append(response, vehicle) + } + + return response, err +} + +type fleetVehicle struct { + VIN string `json:"vin" validate:"required,vin"` + CarState *common.CarState `json:"carstate"` + CarUpdate *common.CarUpdate `json:"carupdate,omitempty"` +} diff --git a/services/ota_update_go/handlers/fleet_vehicle_get_list_test.go b/services/ota_update_go/handlers/fleet_vehicle_get_list_test.go new file mode 100644 index 0000000..cf8b52c --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicle_get_list_test.go @@ -0,0 +1,71 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestFleetVehicleGetList(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + client.SetFleets(mongo.NewFleetsCollection( + &mongo.MockCollection{ + AggregateObject: []mongo.Fleet{ + { + Name: "US-TEST", + Vehicles: []string{ + "TESTVIN1234567890", + "TESTVIN1234567891", + }, + }, + }, + }, + )) + + redisClient := tester.NewRedisMock() + redisClient.SISMEMBEResults = map[string]map[string]interface{}{ + redis.CarSessionsKey(): { + "TESTVIN1234567890": int64(0), + "TESTVIN1234567891": int64(1), + }, + redis.HMISessionsKey(): { + "TESTVIN1234567890": int64(0), + "TESTVIN1234567891": int64(1), + }, + } + redisClient.HGETALLResults = map[string][]interface{}{ + redis.CarStateHashKey("TESTVIN1234567890"): { + []byte("trex_version"), []byte(`1.2.4`), + }, + } + + services.SetRedisClientPool(tester.NewMockClientPool(redisClient)) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid vin parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/$INVALIDTEST/vehicles", 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/vehicles", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"vin":"TESTVIN1234567890","carstate":{"online":false,"online_hmi":false,"trex_version":"1.2.4"},"carupdate":{"vin":"","UpdateSource":""}},{"vin":"TESTVIN1234567891","carstate":{"online":true,"online_hmi":true},"carupdate":{"vin":"","UpdateSource":""}}]}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleGetList, "/fleet/:name/vehicles") +} diff --git a/services/ota_update_go/handlers/fleet_vehicles_add.go b/services/ota_update_go/handlers/fleet_vehicles_add.go new file mode 100644 index 0000000..d697323 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicles_add.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "fmt" + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" + "github.com/julienschmidt/httprouter" +) + +// HandleFleetVehicleAdd godoc +// @Summary Add vehicle to fleet +// @Description Add vehicle to fleet +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param name path string true "Name" +// @Param config body FleetVehicleParams true "Vehicle 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}/vehicles/add [post] +func HandleFleetVehicleAdd(w http.ResponseWriter, r *http.Request) { + fleetVehicleAdd.Handle(w, r) +} + +var fleetVehicleAdd = controllers.NewMongoUpdate(&fleetVehicleAddHelper{}) + +type fleetVehicleAddHelper struct{} + +func (h *fleetVehicleAddHelper) ParseUpdateURLParams(r *http.Request) interface{} { + req := &mongo.Fleet{} + + params := httprouter.ParamsFromContext(r.Context()) + req.Name = params.ByName("name") + + return req +} + +func (h *fleetVehicleAddHelper) 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 *fleetVehicleAddHelper) NewModel() interface{} { + return &FleetVehicleParams{} +} + +func (h *fleetVehicleAddHelper) ParseRequestBody(r *http.Request, model interface{}) error { + return httphandlers.ParseRequest(r, model) +} + +func (h *fleetVehicleAddHelper) QueryUpdate(filter interface{}, model interface{}) error { + client, err := services.GetMongoClient() + if err != nil { + return err + } + + fvp := model.(*FleetVehicleParams) + + if fvp.CANBus == nil { + fvp.CANBus = &common.CANBus{Enabled: true} + } + + if fvp.CANBus.DTCEnabled == nil { + fvp.CANBus.DTCEnabled = elptr.ElPtr(true) + } + + if len(fvp.VIN) > 0 { + fvp.VINs = append(fvp.VINs, fvp.VIN) + } + + // clear out the VIN (single) if it was merged into the new property + // otherwise persist existing behavior + if len(fvp.VIN) > 0 && len(fvp.VINs) > 1 { + fvp.VIN = "" + } + + cars, err := utils.ParseVINs(fvp.VINs) + if err != nil { + return err + } + + for _, car := range cars { + hasVehicle := doesVehicleExist(car.VIN) + if !hasVehicle { + logger.Warn().Msgf("tried to add a non-existent car %s to fleet %s", car.VIN, filter.(*mongo.Fleet).Name) + err = fmt.Errorf("vin %s was not found in database", car.VIN) + return err + } + } + + err = client.GetFleets().AddVehiclesToFleet(filter.(*mongo.Fleet).Name, fvp.VINs) + if err != nil { + return err + } + + err = ResetVehiclesConfigCache(fvp.VINs) + if err != nil { + return err + } + + return nil +} + +func doesVehicleExist(vin string) bool { + count, err := services.GetDB().GetCars().Count(&common.Car{VIN: vin}) + if err != nil { + return false + } + + return count == 1 +} + +type FleetVehicleParams struct { + VIN string `json:"vin" validate:"required_without=VINs,omitempty,vin"` + VINs []string `json:"vins,omitempty" validate:"required_without=VIN,max=1000,dive,vin"` + LogLevel common.LogLevel `json:"log_level,omitempty" swaggertype:"string"` + CANBus *common.CANBus `json:"canbus,omitempty"` + IDPSEnabled bool `json:"idps_enabled,omitempty"` +} diff --git a/services/ota_update_go/handlers/fleet_vehicles_add_test.go b/services/ota_update_go/handlers/fleet_vehicles_add_test.go new file mode 100644 index 0000000..546b84d --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicles_add_test.go @@ -0,0 +1,153 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/mongo" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestFleetVehicleAdd(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + client.SetFleets(mongo.NewFleetsCollection(&mongo.MockCollection{})) + client.SetVehicles(mongo.NewVehiclesCollection(&mongo.MockCollection{})) + + mock := mocks.MockCars{SelectCarsResponse: []common.Car{{ + VIN: "1F15K3R45N1234567", + }}} + services.GetDB().SetCars(&mock) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid fleet", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/$TEST/vehicles/add", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/vehicles/add", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required_without VINs. VINs required_without VIN","error":"Bad Request"}`, + }, + { + Name: "No VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{}, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required_without VINs. VINs required_without VIN","error":"Bad Request"}`, + }, + { + Name: "Invalid VIN", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VIN: "TESTVIN", + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN 'TESTVIN' invalid","error":"Bad Request"}`, + }, + { + Name: "Valid VIN", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VIN: "1F15K3R45N1234567", + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1F15K3R45N1234567","vins":["1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`, + }, + { + Name: "Invalid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VINs: []string{"TESTVIN"}, + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs[0] 'TESTVIN' invalid","error":"Bad Request"}`, + }, + { + Name: "Valid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VINs: []string{"1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`, + }, + { + Name: "Multiple Invalid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VINs: []string{"TESTVIN", "TESTVIN2"}, + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs[0] 'TESTVIN' invalid. VINs[1] 'TESTVIN2' invalid","error":"Bad Request"}`, + }, + { + Name: "Multiple Mixed Valid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VINs: []string{"1F15K3R45N1234567", "TESTVIN"}, + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs[1] 'TESTVIN' invalid","error":"Bad Request"}`, + }, + { + Name: "Multiple Valid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VINs: []string{"1F15K3R45N1234567", "1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567","1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`, + }, + { + Name: "Multiple Valid VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/add", + handlers.FleetVehicleParams{ + VIN: "1F15K3R45N1234567", + VINs: []string{"1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567","1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleAdd, "/fleet/:name/vehicles/add") +} diff --git a/services/ota_update_go/handlers/fleet_vehicles_delete.go b/services/ota_update_go/handlers/fleet_vehicles_delete.go new file mode 100644 index 0000000..4937ed8 --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicles_delete.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "net/http" + + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/julienschmidt/httprouter" +) + +// HandleFleetVehicleDelete godoc +// @Summary Delete vehicle from fleet +// @Description Delete vehicle from fleet +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param name path string true "Name" +// @Param vins body common.VINs true "VINs" +// @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}/vehicles/delete [post] +func HandleFleetVehicleDelete(w http.ResponseWriter, r *http.Request) { + fleetVehicleDelete.Handle(w, r) +} + +var fleetVehicleDelete = controllers.NewMongoDelete(&fleetVehicleDeleteHelper{}) + +type fleetVehicleDeleteHelper struct{} + +func (h *fleetVehicleDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} { + req := &FleetVehicleDeleteParams{} + + params := httprouter.ParamsFromContext(r.Context()) + req.Name = params.ByName("name") + + httphandlers.ParseRequest(r, &req) // Populate VINs from body + + return req +} + +func (h *fleetVehicleDeleteHelper) ValidateFields(model interface{}) error { + result := model.(*FleetVehicleDeleteParams) + + err := validator.ValidateField(result.Name, "required,fleet") + if err != nil { + return controllers.ErrorPKRequired + } + + for _, vin := range result.VINs { + err := validator.ValidateField(vin, "required,vin") + if err != nil { + return controllers.ErrorPKRequired + } + } + + return nil +} + +func (h *fleetVehicleDeleteHelper) QueryDelete(filter interface{}) error { + client, err := services.GetMongoClient() + if err != nil { + return err + } + + fleet := filter.(*FleetVehicleDeleteParams) + + err = client.GetFleets().DeleteVehiclesFromFleet(fleet.Name, fleet.VINs) + if err != nil { + return err + } + + return ResetVehiclesConfigCache(fleet.VINs) +} + +type FleetVehicleDeleteParams struct { + Name string `validate:"required,fleet"` + VINs []string `json:"vins" validate:"required,max=1000,dive,vin"` +} diff --git a/services/ota_update_go/handlers/fleet_vehicles_delete_test.go b/services/ota_update_go/handlers/fleet_vehicles_delete_test.go new file mode 100644 index 0000000..74bf84a --- /dev/null +++ b/services/ota_update_go/handlers/fleet_vehicles_delete_test.go @@ -0,0 +1,79 @@ +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 TestFleetVehicleDelete(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/vehicles/delete", + handlers.FleetVehicleDeleteParams{ + Name: "$TEST", // only needed for test + VINs: []string{"1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Invalid vin", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/delete", + handlers.FleetVehicleDeleteParams{ + Name: "US-TEST", // only needed for test + VINs: []string{"INVALIDVIN"}, + }, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/delete", + handlers.FleetVehicleDeleteParams{ + Name: "US-TEST", // only needed for test + VINs: []string{"1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + { + Name: "Multiple VINs", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/fleet/US-TEST/vehicles/delete", + handlers.FleetVehicleDeleteParams{ + Name: "US-TEST", // only needed for test + VINs: []string{"1F15K3R45N1234567", "1F15K3R45N1234567"}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleDelete, "/fleet/:name/vehicles/delete") +} diff --git a/services/ota_update_go/handlers/fleetupdate_add.go b/services/ota_update_go/handlers/fleetupdate_add.go new file mode 100644 index 0000000..4fb8ece --- /dev/null +++ b/services/ota_update_go/handlers/fleetupdate_add.go @@ -0,0 +1,100 @@ +package handlers + +import ( + "net/http" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleFleetUpdatesAdd godoc +// @Summary Add car updates by fleet +// @Description Create car updates assigning update package to cars, and send notifications +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body usecase_helpers.JSONFleetUpdatesRequest true "Update manifest or package id and, car ids" +// @Success 200 {object} common.JSONDBQueryResult "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 /fleetupdate [post] +func HandleFleetUpdatesAdd(w http.ResponseWriter, r *http.Request) { + var req uhelpers.JSONFleetUpdatesRequest + + err := httphandlers.ParseRequest(r, &req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + manifest, err := getManifest(req.UpdateManifestID) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + username := httphandlers.GetClientID(r) + + err = sendManifestToFleets(req.FleetNames, manifest, username) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: req.FleetNames, + }) +} + +func sendManifestToFleets(fleetNames []string, manifest common.UpdateManifest, username string) error { + var resultNames []string + resC := make(chan string, len(fleetNames)) + // to avoid goroutines leak, we add buffer to the errC channel + errC := make(chan error, len(fleetNames)) + + for _, name := range fleetNames { + go updateFleet(name, manifest, username, errC, resC) + } + + for { + select { + case err := <-errC: + return err + case name := <-resC: + resultNames = append(resultNames, name) + if len(resultNames) == len(fleetNames) { + return nil + } + } + } +} + +func updateFleet(name string, manifest common.UpdateManifest, username string, errC chan<- error, resC chan<- string) { + client, err := services.GetMongoClient() + if err != nil { + errC <- err + return + } + + vins, err := client.GetFleets().GetVehiclesForFleet(name, "", &queries.PageQueryOptions{}) + if err != nil { + errC <- err + return + } + + d := services.GetDB().GetCarUpdates() + k := services.GetKafkaProducer() + notifier := uhelpers.NewUpdateNotifier(d, k) + _, err = notifier.Send(vins, manifest, username) + if err != nil { + errC <- err + return + } + + resC <- name +} diff --git a/services/ota_update_go/handlers/fleetupdate_add_test.go b/services/ota_update_go/handlers/fleetupdate_add_test.go new file mode 100644 index 0000000..53b9528 --- /dev/null +++ b/services/ota_update_go/handlers/fleetupdate_add_test.go @@ -0,0 +1,188 @@ +package handlers_test + +import ( + "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/kafka" + km "github.com/fiskerinc/cloud-services/pkg/kafka/mock" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "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" + "google.golang.org/protobuf/proto" +) + +func TestFleetUpdateAdd(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + redis.MockRedisConnection() + vin1 := "1G1FP87S3GN100062" + vin2 := "TESTVIN1234567891" + mockDB := dbm.MockCarUpdates{} + mockKafka := km.KafkaMock{} + mockRedis := rm.MockRedis{} + mockMongo := mongo.NewFleetsCollection( + &mongo.MockCollection{ + AggregateObject: []mongo.Fleet{ + { + Name: "Grande", + Vehicles: []string{ + vin1, + vin2, + }, + }, + }, + }, + ) + client.SetFleets(mockMongo) + services.GetDB().SetCarUpdates(&mockDB) + services.SetKafkaProducer(&mockKafka) + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + otaUpdateKey1 := common.Service.Key(vin1) + otaUpdateKey2 := common.Service.Key(vin2) + 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) + sEnc := 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", + } + forcedManifest := standardManifest + forcedManifest.Type = "forced" + + tests := []testrunner.TestCase{ + { + Name: "Bad car ids", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{ + UpdateManifestID: 1, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"FleetNames required","error":"Bad Request"}`, + }, + }, + { + Name: "No data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"UpdateManifestID required. FleetNames required","error":"Bad Request"}`, + }, + }, + { + Name: "Bad package ids", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{ + FleetNames: []string{"Slick Grande"}, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"UpdateManifestID required. FleetNames[0] fleet ","error":"Bad Request"}`, + }, + }, + { + Name: "Good data standard manifest id", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{ + UpdateManifestID: 1, + FleetNames: []string{"Slick"}, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":["Slick"]}`, + }, + 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: { + otaUpdateKey1: sEnc, + otaUpdateKey2: sEnc, + }, + }, + }, + }, + { + Name: "Error", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{ + UpdateManifestID: 1, + FleetNames: []string{"Slick"}, + }), + 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 { + w := test.HttpTestCase.Test(handlers.HandleFleetUpdatesAdd) + 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) + } + } +} diff --git a/services/ota_update_go/handlers/get_car_configuration.go b/services/ota_update_go/handlers/get_car_configuration.go new file mode 100644 index 0000000..1aee3cd --- /dev/null +++ b/services/ota_update_go/handlers/get_car_configuration.go @@ -0,0 +1,58 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + "strconv" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/manifestsender" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// GetCarConfiguration godoc +// @Summary Get the vod and cds for car. Does not generate a usable to send to the car +// @Description Get all sap codes for a car, transform them to VOD and CDS, then return it to the user +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN to get configuration update" +// @Param forced query bool false "Force configuration update" +// @Success 200 {object} common.UpdateConfigManifest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /car_config/{vin} [get] +func GetCarConfiguration(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() + + username := httphandlers.GetClientID(r) + + manifestSender := manifestsender.NewTBOXManifestSender(rds, cs, db, sms, nil) + input := manifestsender.ProcessConfigUpdateStruct{ + VIN: vin, + Username: username, + SendToCar: false, + DontCreateDatabaseEntry: true, + Forced: forced, + } + ucm, err := manifestSender.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData()) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + utils.RespJSON(w, http.StatusOK, ucm) +} diff --git a/services/ota_update_go/handlers/get_car_version.go b/services/ota_update_go/handlers/get_car_version.go new file mode 100644 index 0000000..62295a3 --- /dev/null +++ b/services/ota_update_go/handlers/get_car_version.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + "time" + + "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" +) + +// HandleVersionsGet godoc +// @Summary Returns versions for VIN. +// @Description Returns versions for VIN at a point in time +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param timestamp query string false "at date (2023-01-13)" +// @Success 200 {object} map[string]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 /vehicle/{vin}/version [get] +func HandleVersionsGet(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + qs := r.URL.Query() + req := getVersionsRequest{ + VIN: params.ByName("vin"), + Timestamp: qs.Get("timestamp"), + } + err := validator.ValidateStruct(req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + result, err := services.GetDB().GetCarVersionsLog().GetCarVersions(req.VIN, req.GetTimestamp()) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, result) +} + +type getVersionsRequest struct { + VIN string `validate:"vin,required"` + Timestamp string `validate:"yyyymmdddate"` +} + +func (g *getVersionsRequest) GetTimestamp() time.Time { + if len(g.Timestamp) > 0 { + date, err := time.Parse("2006-01-02", g.Timestamp) + if err == nil { + return date.Add(time.Second * 86399) + } + logger.Warn().AnErr("getVersionsRequest.GetTimestamp", err).Send() + } + + return time.Now() +} diff --git a/services/ota_update_go/handlers/get_car_version_logs.go b/services/ota_update_go/handlers/get_car_version_logs.go new file mode 100644 index 0000000..d20e2db --- /dev/null +++ b/services/ota_update_go/handlers/get_car_version_logs.go @@ -0,0 +1,56 @@ +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/validator" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVersionLogsGet godoc +// @Summary Returns version change logs by VIN. +// @Description Returns version change logs by VIN. +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Success 200 {object} common.CarVersionLogs +// @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 /vehicle/{vin}/version/logs [get] +func HandleVersionLogsGet(w http.ResponseWriter, r *http.Request) { + options, err := orm.ParsePageQuery(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + params := httprouter.ParamsFromContext(r.Context()) + vin := params.ByName("vin") + err = validator.GetValidator().Var(vin, "vin") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB().GetCars().SelectByVIN(vin) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + logs, total, err := services.GetDB().GetCarVersionsLog().SelectByVIN(vin, options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: logs, + Total: total, + }) +} diff --git a/services/ota_update_go/handlers/get_car_version_logs_test.go b/services/ota_update_go/handlers/get_car_version_logs_test.go new file mode 100644 index 0000000..e32e326 --- /dev/null +++ b/services/ota_update_go/handlers/get_car_version_logs_test.go @@ -0,0 +1,94 @@ +package handlers_test + +import ( + "context" + "net/http" + "net/http/httptest" + "otaupdate/handlers" + "otaupdate/services" + "testing" + "time" + + "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/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" +) + +func TestHandleVersionLogsGet(t *testing.T) { + createdAt := time.Date(2023, 1, 13, 11, 51, 12, 0, time.UTC) + correctVIN := "00000000000000000" + dbMock := services.GetDB() + dbMock.SetCars(&mocks.MockCars{}) + tests := map[string]struct { + vin string + query string + carVersionsDB queries.CarVersionsLogInterface + expStatus int + expBody string + }{ + "correct": { + vin: correctVIN, + carVersionsDB: mocks.MockCarVersionsLog{ + MockSelectByVIN: func(vin string, options *queries.PageQueryOptions) ([]common.CarVersionLogs, int, error) { + return []common.CarVersionLogs{ + { + ID: 1, + VIN: correctVIN, + VersionSource: common.TREXVersionSource, + Version: "2.3.2", + CreatedAt: &createdAt, + }, + { + ID: 2, + VIN: correctVIN, + VersionSource: common.DBCVersionSource, + Version: "hash", + CreatedAt: &createdAt, + }, + }, 2, nil + }, + }, + expStatus: http.StatusOK, + expBody: `{"data":[{"id":1,"vin":"00000000000000000","version_source":"TREX","version":"2.3.2","created_at":"2023-01-13T11:51:12Z"},{"id":2,"vin":"00000000000000000","version_source":"DBC","version":"hash","created_at":"2023-01-13T11:51:12Z"}],"total":2}`, + }, + "wrong_options": { + query: "limit=-1", + expStatus: http.StatusBadRequest, + expBody: `{"message":"Limit less than 0","error":"Bad Request"}`, + }, + "wrong_vin": { + expStatus: http.StatusBadRequest, + expBody: `{"message":"vin '' invalid","error":"Bad Request"}`, + }, + "wrong_db": { + vin: correctVIN, + carVersionsDB: mocks.MockCarVersionsLog{ + MockSelectByVIN: func(vin string, options *queries.PageQueryOptions) ([]common.CarVersionLogs, int, error) { + return nil, 0, someErr + }, + }, + expStatus: http.StatusServiceUnavailable, + expBody: `{"message":"some err","error":"Service Unavailable"}`, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + dbMock.SetCarVersionsLog(tt.carVersionsDB) + + p := httprouter.Params{ + {Key: "vin", Value: tt.vin}, + } + + ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p) + r := th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/vin/version/logs?"+tt.query, nil). + WithContext(ctx) + w := httptest.NewRecorder() + handlers.HandleVersionLogsGet(w, r) + assert.Equal(t, tt.expStatus, w.Code) + assert.Equal(t, tt.expBody, w.Body.String()) + }) + } +} diff --git a/services/ota_update_go/handlers/get_car_version_test.go b/services/ota_update_go/handlers/get_car_version_test.go new file mode 100644 index 0000000..12efc3f --- /dev/null +++ b/services/ota_update_go/handlers/get_car_version_test.go @@ -0,0 +1,50 @@ +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 TestHandleVersionGet(t *testing.T) { + mock := mo.MockCarVersionsLog{ + GetCarVersionsResult: map[string]string{ + "DBC": "dbc-version", + "TREX": "trex-version", + }, + } + services.GetDB().SetCarVersionsLog(&mock) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/1111/version", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN '1111' invalid","error":"Bad Request"}`, + }, + { + Name: "Invalid timestamp", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version?timestamp=99-99-99", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Timestamp yyyymmdddate ","error":"Bad Request"}`, + }, + { + Name: "Good request no timestamp", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"DBC":"dbc-version","TREX":"trex-version"}`, + }, + { + Name: "Good request with timestamp", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version?timestamp=2023-01-30", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"DBC":"dbc-version","TREX":"trex-version"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVersionsGet, "/vehicle/:vin/version") +} diff --git a/services/ota_update_go/handlers/guest_token_test.go b/services/ota_update_go/handlers/guest_token_test.go new file mode 100644 index 0000000..65ec879 --- /dev/null +++ b/services/ota_update_go/handlers/guest_token_test.go @@ -0,0 +1,72 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + "github.com/fiskerinc/cloud-services/pkg/redis" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/pkg/errors" + "otaupdate/handlers" +) + +var someErr = errors.New("some err") + +func TestHandleDashboardToken(t *testing.T) { + tests := map[string]struct { + mockGetAccessToken func(r redis.Client) (string, error) + mockGetGuestToken func(r redis.Client, accToken string) (string, error) + http tester.HttpTestCase + }{ + "success": { + mockGetAccessToken: successGetAccessToken, + mockGetGuestToken: successGetGuestToken, + http: tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"token":"valid_token"}`, + }, + }, + "fail_get_access": { + mockGetAccessToken: failGetAccessToken, + http: tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`, + }, + }, + "fail_get_guest": { + mockGetAccessToken: successGetAccessToken, + mockGetGuestToken: failGetGuestToken, + http: tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`, + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + handlers.SetGetAccessTokenFunc(tt.mockGetAccessToken) + handlers.SetGetGuestTokenFunc(tt.mockGetGuestToken) + w := tt.http.Test(handlers.HandleDashboardToken) + + tt.http.ValidateHttp(t, name, w) + }) + } +} + +func successGetAccessToken(r redis.Client) (string, error) { + return "", nil +} +func failGetAccessToken(r redis.Client) (string, error) { + return "", someErr +} + +func successGetGuestToken(r redis.Client, accToken string) (string, error) { + return "valid_token", nil +} +func failGetGuestToken(r redis.Client, accToken string) (string, error) { + return "", someErr +} diff --git a/services/ota_update_go/handlers/issue_get.go b/services/ota_update_go/handlers/issue_get.go new file mode 100644 index 0000000..9628112 --- /dev/null +++ b/services/ota_update_go/handlers/issue_get.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "fmt" + "net/http" + "strconv" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleIssueGet godoc +// @Summary Search issue by ID +// @Description Returns all Issue related to the issue id +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id path int true "Issue ID" +// @Success 200 {object} common.JSONDBQueryResult{data=common.Issue} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /issue/{id} [get] +func HandleIssueGet(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + id, err := strconv.Atoi(params.ByName("id")) + if err != nil { + err = fmt.Errorf("invalid id") + } + if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound) { + return + } + + issue, err := services.GetDB().GetIssues().SelectByID(id) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: issue, + }) +} diff --git a/services/ota_update_go/handlers/issue_get_test.go b/services/ota_update_go/handlers/issue_get_test.go new file mode 100644 index 0000000..f981951 --- /dev/null +++ b/services/ota_update_go/handlers/issue_get_test.go @@ -0,0 +1,41 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestIssueGet(t *testing.T) { + + mock := mo.MockIssue{} + services.GetDB().SetIssues(&mock) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid Issue ID", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/x", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: `{"message":"invalid id","error":"Not Found"}`, + }, + { + Name: "Zero Issue ID", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id cannot be less than 0","error":"Bad Request"}`, + }, + { + Name: "Good Issue Id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/1", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":{"id":1,"vin":"","title":"","description":"","timestamp":"0001-01-01T00:00:00Z","images":[{"id":1,"image":"","issue_id":1}]}}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleIssueGet, "/issues/:id") +} diff --git a/services/ota_update_go/handlers/issues_delete.go b/services/ota_update_go/handlers/issues_delete.go new file mode 100644 index 0000000..75255c1 --- /dev/null +++ b/services/ota_update_go/handlers/issues_delete.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "net/http" + "strconv" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleIssuesDelete godoc +// @Summary Delete an Issue by ID +// @Description Deletes an Issue by its ID +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id path int true "Issue 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 /issues/{id} [delete] +func HandleIssuesDelete(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + id, err := strconv.Atoi(params.ByName("id")) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB().GetIssues().Delete(id) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "Deleted", + }) +} diff --git a/services/ota_update_go/handlers/issues_delete_test.go b/services/ota_update_go/handlers/issues_delete_test.go new file mode 100644 index 0000000..e86c0e3 --- /dev/null +++ b/services/ota_update_go/handlers/issues_delete_test.go @@ -0,0 +1,40 @@ +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 TestIssuesDelete(t *testing.T) { + db := services.GetDB() + + db.SetIssues(&mocks.MockIssue{}) + tests := []th.BasicHttpTest{ + { + Name: "Invalid ID", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/x", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"strconv.Atoi: parsing \"x\": invalid syntax","error":"Bad Request"}`, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id cannot be less than 0","error":"Bad Request"}`, + }, + { + Name: "Good Request", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/1", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleIssuesDelete, "/issues/:id") +} diff --git a/services/ota_update_go/handlers/issues_get.go b/services/ota_update_go/handlers/issues_get.go new file mode 100644 index 0000000..d6a92a8 --- /dev/null +++ b/services/ota_update_go/handlers/issues_get.go @@ -0,0 +1,70 @@ +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/validator" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleIssuesGet godoc +// @Summary Search Issues +// @Description Returns all Issues +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} common.JSONDBQueryResult{data=[]common.Issue} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /issues [get] +func HandleIssuesGet(w http.ResponseWriter, r *http.Request) { + var total int + c := services.GetDB().GetIssues() + + filter, err := parseIssuesFilter(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 = "vin" + } + + issues, err := c.Search(filter, options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + total, err = c.Count() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: issues, + Total: total, + }) +} + +func parseIssuesFilter(r *http.Request) (*common.IssueSearch, error) { + qs := r.URL.Query() + + filter := common.IssueSearch{ + Search: qs.Get("search"), + } + + err := validator.ValidateNonRequired(filter) + + return &filter, err +} diff --git a/services/ota_update_go/handlers/issues_get_test.go b/services/ota_update_go/handlers/issues_get_test.go new file mode 100644 index 0000000..6e2aec0 --- /dev/null +++ b/services/ota_update_go/handlers/issues_get_test.go @@ -0,0 +1,59 @@ +package handlers_test + +import ( + "net/http" + "testing" + "time" + + "otaupdate/handlers" + "otaupdate/services" + + m "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 TestIssuesGet(t *testing.T) { + db := services.GetDB() + + mock := mocks.MockIssue{} + mock.SelectIssuesResponse = []m.Issue{ + { + ID: 1, + VIN: "19XFB2F95DE056700", + Title: "test1", + Description: "", + DriverID: "d2ef41d6-d703-4c7a-9a11-ecb9622f9684 ", + Timestamp: time.Time{}, + IssueImages: []m.IssueImage{}, + }, + { + ID: 2, + VIN: "1XP5D68X4TN494870", + Title: "test2", + Description: "", + DriverID: "ada10165-484d-4141-8ad4-794f49537365 ", + Timestamp: time.Time{}, + IssueImages: []m.IssueImage{}, + }, + } + db.SetIssues(&mock) + + tests := []th.BasicHttpTest{ + { + Name: "Get all", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"id":1,"vin":"19XFB2F95DE056700","title":"test1","description":"","driver_id":"d2ef41d6-d703-4c7a-9a11-ecb9622f9684\t","timestamp":"0001-01-01T00:00:00Z"},{"id":2,"vin":"1XP5D68X4TN494870","title":"test2","description":"","driver_id":"ada10165-484d-4141-8ad4-794f49537365\t","timestamp":"0001-01-01T00:00:00Z"}],"total":1}`, + }, + { + Name: "Get by search", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues?order=created_at+asc&search=test1", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"id":1,"vin":"19XFB2F95DE056700","title":"test1","description":"","driver_id":"d2ef41d6-d703-4c7a-9a11-ecb9622f9684\t","timestamp":"0001-01-01T00:00:00Z"}],"total":1}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleIssuesGet, "/issues") +} diff --git a/services/ota_update_go/handlers/manifest_migrate_version.go b/services/ota_update_go/handlers/manifest_migrate_version.go new file mode 100644 index 0000000..42fecdc --- /dev/null +++ b/services/ota_update_go/handlers/manifest_migrate_version.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/pkg/utils" +) + +// HandleManifestMigrateVersion godoc +// @Summary Return the version of manifest migrate that this service is running +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body ManifestMigrateBody true "The manifest to migrate" +// @Success 200 {object} ManifestMigrateVersion +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifestmigrate-version [get] +func HandleUpdateManifestMigrateVersion(w http.ResponseWriter, r *http.Request) { + utils.RespJSON(w, http.StatusOK, ManifestMigrateVersion{Version: MIGRATION_VERSION}) +} diff --git a/services/ota_update_go/handlers/manufacture_dll_certs.go b/services/ota_update_go/handlers/manufacture_dll_certs.go new file mode 100644 index 0000000..dd256a2 --- /dev/null +++ b/services/ota_update_go/handlers/manufacture_dll_certs.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "net/http" + + v "github.com/fiskerinc/cloud-services/pkg/hashvault" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleGetDLLManufactureCerts godoc +// @Summary Generates public and private certificates for security dll. +// @Description Generates public and private certificates for security dll to access manufacture/secaccess API +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} common.Certificate "Created public and private pems" +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manufacture-certs [post] +func HandleGetDLLManufactureCerts(w http.ResponseWriter, r *http.Request) { + cert, err := v.GetVaultClient().CreatePKICertificate("ALL") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, cert) +} diff --git a/services/ota_update_go/handlers/sms_send.go b/services/ota_update_go/handlers/sms_send.go new file mode 100644 index 0000000..740818d --- /dev/null +++ b/services/ota_update_go/handlers/sms_send.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "otaupdate/services" + "strings" + + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleSMSSend godoc +// @Summary Send SMS +// @Description Send SMS using SMS service +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param config body sms.SendSMSRequest true "SMS data" +// @Success 200 {object} sms.SMSDetailsResponse +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 408 {object} common.JSONError "Request timeout" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /sms [post] +func HandleSMSSend(w http.ResponseWriter, r *http.Request) { + var smsReq sms.SendSMSRequest + if err := json.NewDecoder(r.Body).Decode(&smsReq); err != nil { + loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) + return + } + + res, err := services.GetSMSClient().HandleSMSSend(r.Context(), &smsReq) + if err != nil { + s, ok := status.FromError(err) + if !ok { + loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) + return + } + + errMsg, httpCode := grpcErrToHttpErr(s) + loggerdataresp.BadDataErrorResp(w, errors.New(errMsg), httpCode) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + tow, err := protojson.Marshal(res) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + w.Write(tow) + + return +} + +func grpcErrToHttpErr(s *status.Status) (string, int) { + switch s.Code() { + case codes.Canceled: + return s.Message(), 499 + case codes.Unknown: + return s.Message(), http.StatusInternalServerError + case codes.InvalidArgument: + return s.Message(), http.StatusBadRequest + case codes.DeadlineExceeded: + return s.Message(), http.StatusRequestTimeout + case codes.NotFound: + return s.Message(), http.StatusNotFound + case codes.AlreadyExists: + return s.Message(), http.StatusConflict + case codes.PermissionDenied: + return s.Message(), http.StatusForbidden + case codes.ResourceExhausted: + return s.Message(), http.StatusTooManyRequests + case codes.FailedPrecondition: + return s.Message(), http.StatusPreconditionFailed + case codes.Aborted: + return s.Message(), http.StatusConflict + case codes.OutOfRange: + return s.Message(), http.StatusBadRequest + case codes.Unimplemented: + return s.Message(), http.StatusNotImplemented + case codes.Unavailable: + return s.Message(), http.StatusServiceUnavailable + case codes.DataLoss: + return s.Message(), http.StatusInternalServerError + case codes.Unauthenticated: + return s.Message(), http.StatusUnauthorized + case codes.Internal: + if strings.Contains(s.Message(), "bad message status") { + return s.Message(), http.StatusExpectationFailed + } + + return s.Message(), http.StatusInternalServerError + default: + return s.Message(), http.StatusInternalServerError + } +} diff --git a/services/ota_update_go/handlers/sms_send_test.go b/services/ota_update_go/handlers/sms_send_test.go new file mode 100644 index 0000000..f6ec30c --- /dev/null +++ b/services/ota_update_go/handlers/sms_send_test.go @@ -0,0 +1,340 @@ +package handlers + +import ( + "context" + "net/http" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type MockSmsClientSucc struct{} + +// HandleSMSQueue implements sms.SMSServiceClient. +func (*MockSmsClientSucc) HandleSMSQueue(ctx context.Context, in *sms.SendSMSRequest, opts ...grpc.CallOption) (*sms.SMSQueueResponse, error) { + return &sms.SMSQueueResponse{ + SmsMsgID: "1023", + SentSuccessful: false, + }, nil +} + +func (m *MockSmsClientSucc) HandleSMSSend(ctx context.Context,in *sms.SendSMSRequest,opts ...grpc.CallOption,) (*sms.SMSDetailsResponse, error) { + return &sms.SMSDetailsResponse{ + SmsMsgID: "123", + Status: 5, + MessageText: "messageText", + SenderLogin: "sender", + SentFrom: "from", + SentTo: "to", + DateSent: "dateSent", + MsgType: "type", + DateModified: "dateModified", + ICCID: "iccid", + }, nil +} + +func (m *MockSmsClientSucc) HandleGetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest, opts ...grpc.CallOption) (*sms.GetAvailableProductsResponse, error) { + return nil, nil +} +func (m *MockSmsClientSucc) HandleChangeRatePlan(ctx context.Context, in *sms.ChangeRatePlanRequest, opts ...grpc.CallOption) (*sms.ChangeRatePlanResponse, error) { + return nil, nil +} +func (m *MockSmsClientSucc) HandleCustomAttributes(ctx context.Context, in *sms.CustomAtributesRequest, opts ...grpc.CallOption) (*sms.CustomAtributeResponse, error) { + return nil, nil +} +func (m *MockSmsClientSucc) HandleDeviceDetails(ctx context.Context, in *sms.DeviceDetailsRequest, opts ...grpc.CallOption) (*sms.DeviceDetailsResponse, error) { + return nil, nil +} + +type MockSmsClientFail struct{} + +// HandleSMSQueue implements sms.SMSServiceClient. +func (*MockSmsClientFail) HandleSMSQueue(ctx context.Context, in *sms.SendSMSRequest, opts ...grpc.CallOption) (*sms.SMSQueueResponse, error) { + return &sms.SMSQueueResponse{ + SmsMsgID: "1023", + SentSuccessful: false, + }, nil +} + +func (m *MockSmsClientFail) HandleSMSSend( + ctx context.Context, + in *sms.SendSMSRequest, + opts ...grpc.CallOption, +) (*sms.SMSDetailsResponse, error) { + return nil, errors.New("unintelligable error") +} + +func (m *MockSmsClientFail) HandleGetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest, opts ...grpc.CallOption) (*sms.GetAvailableProductsResponse, error) { + return nil, nil +} +func (m *MockSmsClientFail) HandleChangeRatePlan(ctx context.Context, in *sms.ChangeRatePlanRequest, opts ...grpc.CallOption) (*sms.ChangeRatePlanResponse, error) { + return nil, nil +} +func (m *MockSmsClientFail) HandleCustomAttributes(ctx context.Context, in *sms.CustomAtributesRequest, opts ...grpc.CallOption) (*sms.CustomAtributeResponse, error) { + return nil, nil +} +func (m *MockSmsClientFail) HandleDeviceDetails(ctx context.Context, in *sms.DeviceDetailsRequest, opts ...grpc.CallOption) (*sms.DeviceDetailsResponse, error) { + return nil, nil +} + +type MockSmsClientFailWithGoodErr struct{} + +// HandleSMSQueue implements sms.SMSServiceClient. +func (*MockSmsClientFailWithGoodErr) HandleSMSQueue(ctx context.Context, in *sms.SendSMSRequest, opts ...grpc.CallOption) (*sms.SMSQueueResponse, error) { + return &sms.SMSQueueResponse{ + SmsMsgID: "1023", + SentSuccessful: false, + }, nil +} + +func (m *MockSmsClientFailWithGoodErr) HandleSMSSend( + ctx context.Context, + in *sms.SendSMSRequest, + opts ...grpc.CallOption, +) (*sms.SMSDetailsResponse, error) { + return nil, status.Error(codes.Internal, "bad message status") +} + +func (m *MockSmsClientFailWithGoodErr) HandleGetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest, opts ...grpc.CallOption) (*sms.GetAvailableProductsResponse, error) { + return nil, nil +} +func (m *MockSmsClientFailWithGoodErr) HandleChangeRatePlan(ctx context.Context, in *sms.ChangeRatePlanRequest, opts ...grpc.CallOption) (*sms.ChangeRatePlanResponse, error) { + return nil, nil +} +func (m *MockSmsClientFailWithGoodErr) HandleCustomAttributes(ctx context.Context, in *sms.CustomAtributesRequest, opts ...grpc.CallOption) (*sms.CustomAtributeResponse, error) { + return nil, nil +} +func (m *MockSmsClientFailWithGoodErr) HandleDeviceDetails(ctx context.Context, in *sms.DeviceDetailsRequest, opts ...grpc.CallOption) (*sms.DeviceDetailsResponse, error) { + return nil, nil +} + +func Test_HandleSMSSend(t *testing.T) { + type testCase struct { + name string + doBefore func() + httpTC tester.HttpTestCase + } + + testCases := []testCase{ + { + name: "Valid", + doBefore: func() { services.SetSmsClient(&MockSmsClientSucc{}) }, + httpTC: tester.HttpTestCase{ + Request: th.MakeTestRequest( + "POST", + "http://example.com/sms", + sms.SendSMSRequest{ICCID: "iccid", MessageText: "messageText"}), + ExpectedStatus: http.StatusOK, + ExpectedRegexMap: map[string]string{"smsMsgID": "123", "status": "Delivered", "messageText": "messageText", "senderLogin": "sender", "sentTo": "to", "sentFrom": "from", "msgType": "type", "dateSent": "dateSent", "dateModified": "dateModified", "ICCID": "iccid"}, + }, + }, + { + name: "InvalidJson", + doBefore: func() { services.SetSmsClient(&MockSmsClientSucc{}) }, + httpTC: tester.HttpTestCase{ + Request: th.MakeTestRequest( + "POST", + "http://example.com/sms", + "invalid"), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"json: cannot unmarshal string into Go value of type sms.SendSMSRequest","error":"Bad Request"}`, + }, + }, + { + name: "Failing with bad error", + doBefore: func() { services.SetSmsClient(&MockSmsClientFail{}) }, + httpTC: tester.HttpTestCase{ + Request: th.MakeTestRequest( + "POST", + "http://example.com/sms", + sms.SendSMSRequest{ICCID: "iccid", MessageText: "messageText"}), + ExpectedStatus: http.StatusInternalServerError, + ExpectedResponse: `{"message":"unintelligable error","error":"Internal Server Error"}`, + }, + }, + { + name: "Failing with good error", + doBefore: func() { services.SetSmsClient(&MockSmsClientFailWithGoodErr{}) }, + httpTC: tester.HttpTestCase{ + Request: th.MakeTestRequest( + "POST", + "http://example.com/sms", + sms.SendSMSRequest{ICCID: "iccid", MessageText: "messageText"}), + ExpectedStatus: http.StatusExpectationFailed, + ExpectedResponse: `{"message":"bad message status","error":"Expectation Failed"}`, + }, + }, + } + + for _, tc := range testCases { + tc.doBefore() + w := tc.httpTC.Test(HandleSMSSend) + tc.httpTC.ValidateHttp(t, tc.name, w) + } +} + +func Test_grpcErrToHttpErr(t *testing.T) { + type args struct { + status *status.Status + } + + tests := []struct { + name string + args args + wantCode int + wantMsg string + }{ + { + name: "Internal", + args: args{ + status: status.New(codes.Internal, "just"), + }, + wantCode: 500, + wantMsg: "just", + }, + { + name: "Internal with bad status", + args: args{ + status: status.New(codes.Internal, "bad message status"), + }, + wantCode: http.StatusExpectationFailed, + wantMsg: "bad message status", + }, + + { + name: "InvalidArgument", + args: args{ + status: status.New(codes.InvalidArgument, "just"), + }, + wantCode: 400, + wantMsg: "just", + }, + { + name: "NotFound", + args: args{ + status: status.New(codes.NotFound, "just"), + }, + wantCode: 404, + wantMsg: "just", + }, + { + name: "AlreadyExists", + args: args{ + status: status.New(codes.AlreadyExists, "just"), + }, + wantCode: 409, + wantMsg: "just", + }, + { + name: "PermissionDenied", + args: args{ + status: status.New(codes.PermissionDenied, "just"), + }, + wantCode: 403, + wantMsg: "just", + }, + { + name: "Unauthenticated", + args: args{ + status: status.New(codes.Unauthenticated, "just"), + }, + wantCode: 401, + wantMsg: "just", + }, + { + name: "Unimplemented", + args: args{ + status: status.New(codes.Unimplemented, "just"), + }, + wantCode: 501, + wantMsg: "just", + }, + { + name: "Unknown", + args: args{ + status: status.New(codes.Unknown, "just"), + }, + wantCode: 500, + wantMsg: "just", + }, + { + name: "Unavailable", + args: args{ + status: status.New(codes.Unavailable, "just"), + }, + wantCode: 503, + wantMsg: "just", + }, + { + name: "DeadlineExceeded", + args: args{ + status: status.New(codes.DeadlineExceeded, "just"), + }, + wantCode: http.StatusRequestTimeout, + wantMsg: "just", + }, + { + name: "Canceled", + args: args{ + status: status.New(codes.Canceled, "just"), + }, + wantCode: 499, + wantMsg: "just", + }, + { + name: "FailedPrecondition", + args: args{ + status: status.New(codes.FailedPrecondition, "just"), + }, + wantCode: 412, + wantMsg: "just", + }, + { + name: "OutOfRange", + args: args{ + status: status.New(codes.OutOfRange, "just"), + }, + wantCode: 400, + wantMsg: "just", + }, + { + name: "Aborted", + args: args{ + status: status.New(codes.Aborted, "just"), + }, + wantCode: 409, + wantMsg: "just", + }, + { + name: "ResourceExhausted", + args: args{ + status: status.New(codes.ResourceExhausted, "just"), + }, + wantCode: 429, + wantMsg: "just", + }, + { + name: "DataLoss", + args: args{ + status: status.New(codes.DataLoss, "just"), + }, + wantCode: http.StatusInternalServerError, + wantMsg: "just", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMsg, gotCode := grpcErrToHttpErr(tt.args.status); gotCode != tt.wantCode || gotMsg != tt.wantMsg { + t.Errorf("grpcErrToHttpErr() = (%v, %v), want (%v, %v)", gotCode, gotMsg, tt.wantCode, tt.wantMsg) + } + }) + } +} diff --git a/services/ota_update_go/handlers/sms_send_v2.go b/services/ota_update_go/handlers/sms_send_v2.go new file mode 100644 index 0000000..ada7c8e --- /dev/null +++ b/services/ota_update_go/handlers/sms_send_v2.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "context" + "errors" + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleSendWakeSMSToVIN 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 " +// @Param Api-Key header string false "" +// @Param vin query string true "VIN" +// @Success 200 {object} string "Message Status String" +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /car/wake [post] +func HandleSendWakeSMSToVIN(w http.ResponseWriter, r *http.Request) { + qs := r.URL.Query() + vin := qs.Get("vin") + if vin == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("missing vin")) + return + } + + status, err := sendWakeSMSToVIN(vin) + if loggerdataresp.BadDataError(err) { + return + } + w.Write([]byte(status)) +} + +// Sends wak up SMS along with CAN Status wak +func sendWakeSMSToVIN(vin string) (status string, err error) { + car, err := services.GetDB().GetCars().SelectByVIN(vin) + if err != nil { + return "failed", err + } + if car == nil { + return "failed", errors.New("vehicle not found") + } + + iccid := car.ICCID + + k := sms.SendSMSRequest{ + ICCID: iccid, + MessageText: "wake", + Await: true, + } + res, err := services.GetSMSClient().HandleSMSSend(context.Background(), &k) + if err != nil { + return "failed", err + } + // Send the can bus wake up command + err = sendCANAwakeToVIN(vin) + if err != nil { + return "failed can awake", err + } + return res.Status.String(), nil +} + +func sendCANAwakeToVIN(vin string) (err error) { + err = services.GetRedisV2Client().SafeQueueMessage(common.TRex.Key(vin), common.Message{ + Handler: "can_network", + Data: common.RemoteCANNetworkCommandArgs{Action: "on", Timeout: 180}, + }) + return err +} diff --git a/services/ota_update_go/handlers/sub_config_add.go b/services/ota_update_go/handlers/sub_config_add.go new file mode 100644 index 0000000..84a6f85 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_add.go @@ -0,0 +1,52 @@ +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/google/uuid" +) + +// @deprecated +// HandleSubscriptionConfigAdd godoc +// @Summary Add subscription config +// @Description Create subscription config +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param config body SubConfigRequest true "Subscription config 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 /subscriptionconfig [post] +func HandleSubscriptionConfigAdd(w http.ResponseWriter, r *http.Request) { + subscriptionConfigAdd.Handle(w, r) +} + +var subscriptionConfigAdd = controllers.NewCreate(&subscriptionConfigsCreateHelper{}) + +type subscriptionConfigsCreateHelper struct { + SubscriptionConfigsHelper +} + +func (h *subscriptionConfigsCreateHelper) QueryInsert(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubConfigurations().Insert(model.(*common.SubscriptionConfiguration)) +} + +type SubConfigRequest struct { + SubscriptionFeatureID uuid.UUID `json:"feature_id"` + ECU string `json:"name"` + SoftwareVersion string `json:"sw_version"` + HardwareVersion string `json:"hw_version"` + Configuration []byte `json:"configuration"` + DID []byte `json:"did"` + PID []byte `json:"pid"` + Mask []byte `json:"mask"` +} diff --git a/services/ota_update_go/handlers/sub_config_add_test.go b/services/ota_update_go/handlers/sub_config_add_test.go new file mode 100644 index 0000000..45db8a4 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_add_test.go @@ -0,0 +1,67 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionConfigAdd(t *testing.T) { + mock := mo.MockSubscriptionConfigurations{} + services.GetDB().SetSubConfigurations(&mock) + testFeatureID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + testConfiguration := common.BinaryHex{0x46, 0x44, 0x73, 0x81} + testDID := common.BinaryHex{0x32, 0x45} + testPID := common.BinaryHex{0xAA, 0xAA} + testMask := common.BinaryHex{0x00, 0x00, 0x00, 0x10} + validData := common.SubscriptionConfiguration{ + SubscriptionFeatureID: testFeatureID, + ECU: "TBOX", + SoftwareVersion: "SoftwareVersion", + HardwareVersion: "HardwareVersion", + Configuration: &testConfiguration, + DID: &testDID, + PID: &testPID, + Mask: &testMask, + } + tests := []mo.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionconfig", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"SubscriptionFeatureID required. ECU required. SoftwareVersion required. HardwareVersion required. Configuration required. DID required. PID required. Mask required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionconfig", common.SubscriptionFeature{ + Name: "feature_name", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"SubscriptionFeatureID required. ECU required. SoftwareVersion required. HardwareVersion required. Configuration required. DID required. PID required. Mask required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionconfig", validData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"feature_id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","ecu":"TBOX","sw_version":"SoftwareVersion","hw_version":"HardwareVersion","configuration":"46447381","did":"3245","pid":"aaaa","mask":"00000010"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionconfig", 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.HandleSubscriptionConfigAdd, &mock) +} diff --git a/services/ota_update_go/handlers/sub_config_delete.go b/services/ota_update_go/handlers/sub_config_delete.go new file mode 100644 index 0000000..c807f78 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_delete.go @@ -0,0 +1,56 @@ +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/google/uuid" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionConfigDelete godoc +// @Summary Delete subscription configuration +// @Description Delete subscription configuration data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param feature_id query string true "Subscription feature id" +// @Param ecu query string true "ECU 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 /subscriptionconfig [delete] +func HandleSubscriptionConfigDelete(w http.ResponseWriter, r *http.Request) { + subscriptionConfigDelete.Handle(w, r) +} + +var subscriptionConfigDelete = controllers.NewDelete(&subscriptionConfigDeleteHelper{}) + +type subscriptionConfigDeleteHelper struct { + SubscriptionConfigsHelper +} + +func (h *subscriptionConfigDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} { + req := common.SubscriptionConfiguration{} + decoder := schema.NewDecoder() + + decoder.SetAliasTag("json") + decoder.Decode(&req, r.URL.Query()) + + return &req +} + +func (h *subscriptionConfigDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubConfigurations().Delete(model.(*common.SubscriptionConfiguration)) +} + +type SubscriptionConfigurationDeleteRequest struct { + SubscriptionFeatureID uuid.UUID `validate:"required"` + ECU string `validate:"required,max=100"` +} diff --git a/services/ota_update_go/handlers/sub_config_delete_test.go b/services/ota_update_go/handlers/sub_config_delete_test.go new file mode 100644 index 0000000..3aaefd2 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_delete_test.go @@ -0,0 +1,67 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionConfigDelete(t *testing.T) { + results := mocks.MockORMResults{ + ReturnedRows: 1, + AffectedRows: 1, + } + mock := mocks.MockSubscriptionConfigurations{ + DBMockHelper: mocks.DBMockHelper{ + ORMResponse: &results, + }, + } + services.GetDB().SetSubConfigurations(&mock) + testFeatureID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + + tests := []mocks.DBHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionconfig", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionconfig?feature_id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionconfig?feature_id=0557bd1d-76d3-41e5-a44e-13c479e55ab0&ecu=ADAS", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + DBTestCase: mocks.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{ + SubscriptionFeatureID: testFeatureID, + ECU: "ADAS", + }, + }, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionconfig?feature_id=0557bd1d-76d3-41e5-a44e-13c479e55ab0&ecu=ADAS", 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.HandleSubscriptionConfigDelete, &mock) +} diff --git a/services/ota_update_go/handlers/sub_config_update.go b/services/ota_update_go/handlers/sub_config_update.go new file mode 100644 index 0000000..bfae241 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_update.go @@ -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" +) + +// @deprecated +// HandleSubscriptionConfigUpdate godoc +// @Summary Update subscription package +// @Description Update subscription package +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param config body common.SubscriptionConfiguration true "Subscription configuration 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 /subscriptionconfig [put] +func HandleSubscriptionConfigUpdate(w http.ResponseWriter, r *http.Request) { + subscriptionConfigUpdate.Handle(w, r) +} + +var subscriptionConfigUpdate = controllers.NewUpdate(&subscriptionConfigUpdateHelper{}) + +type subscriptionConfigUpdateHelper struct { + SubscriptionConfigsHelper +} + +func (h *subscriptionConfigUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubConfigurations().Update(model.(*common.SubscriptionConfiguration)) +} diff --git a/services/ota_update_go/handlers/sub_config_update_test.go b/services/ota_update_go/handlers/sub_config_update_test.go new file mode 100644 index 0000000..a556c11 --- /dev/null +++ b/services/ota_update_go/handlers/sub_config_update_test.go @@ -0,0 +1,75 @@ +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" + + "github.com/google/uuid" +) + +func TestSubscriptionConfigUpdate(t *testing.T) { + mock := mocks.MockSubscriptionConfigurations{} + services.GetDB().SetSubConfigurations(&mock) + testUUID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + testConfiguration := common.BinaryHex{0x46, 0x44, 0x73, 0x81} + testDID := common.BinaryHex{0x32, 0x45} + testPID := common.BinaryHex{0xAA, 0xAA} + testMask := common.BinaryHex{0x00, 0x00, 0x00, 0x10} + validData := common.SubscriptionConfiguration{ + SubscriptionFeatureID: testUUID, + ECU: "TBOX", + SoftwareVersion: "SoftwareVersion", + HardwareVersion: "HardwareVersion", + Configuration: &testConfiguration, + DID: &testDID, + PID: &testPID, + Mask: &testMask, + } + + tests := []mocks.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionconfig", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"SubscriptionFeatureID required. ECU required. SoftwareVersion required. HardwareVersion required. Configuration required. DID required. PID required. Mask required","error":"Bad Request"}`, + }, + { + Name: "Missing PK", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionconfig", common.SubscriptionConfiguration{ + SoftwareVersion: "SoftwareVersion", + HardwareVersion: "HardwareVersion", + Configuration: &testConfiguration, + DID: &testDID, + PID: &testPID, + Mask: &testMask, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"SubscriptionFeatureID required. ECU required","error":"Bad Request"}`, + }, + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionconfig", validData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"feature_id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","ecu":"TBOX","sw_version":"SoftwareVersion","hw_version":"HardwareVersion","configuration":"46447381","did":"3245","pid":"aaaa","mask":"00000010"}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", 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.HandleSubscriptionConfigUpdate, &mock) +} diff --git a/services/ota_update_go/handlers/sub_configs_get.go b/services/ota_update_go/handlers/sub_configs_get.go new file mode 100644 index 0000000..e4848da --- /dev/null +++ b/services/ota_update_go/handlers/sub_configs_get.go @@ -0,0 +1,87 @@ +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/google/uuid" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionConfigsGetList godoc +// @Summary Search subscription features +// @Description Get subscription features filtered by id or name +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription feature id" +// @Param name query string false "Subscription feature 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 /subscriptionconfigs [get] +func HandleSubscriptionConfigsGetList(w http.ResponseWriter, r *http.Request) { + subscriptionConfigsGetList.Handle(w, r) +} + +var subscriptionConfigsGetList = controllers.NewGetList(&subscriptionConfigsGetListHelper{}) + +type subscriptionConfigsGetListHelper struct { + SubscriptionConfigsHelper +} + +func (h *subscriptionConfigsGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} { + schema := schema.NewDecoder() + filter := common.SubscriptionConfiguration{} + + schema.SetAliasTag("json") + schema.Decode(&filter, r.URL.Query()) + + return &filter +} + +func (h *subscriptionConfigsGetListHelper) QueryCount(filter interface{}) (int, error) { + return services.GetDB().GetSubConfigurations().Count(filter.(*common.SubscriptionConfiguration)) +} + +func (h *subscriptionConfigsGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + return services.GetDB().GetSubConfigurations().Select(filter.(*common.SubscriptionConfiguration), options) +} + +type SubscriptionConfigsHelper struct { + controllers.HelperBase +} + +func (h *SubscriptionConfigsHelper) NewModel() interface{} { + return &common.SubscriptionConfiguration{} +} + +func (h *SubscriptionConfigsHelper) HasPK(filter interface{}) bool { + result := filter.(*common.SubscriptionConfiguration) + return result.SubscriptionFeatureID != uuid.Nil && result.ECU != "" +} + +func (h *SubscriptionConfigsHelper) ValidatePK(model interface{}) error { + result := model.(*common.SubscriptionConfiguration) + + err := validator.ValidateField(result.SubscriptionFeatureID, "required") + if err != nil { + return controllers.ErrorPKRequired + } + + err = validator.ValidateField(result.ECU, "required") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} diff --git a/services/ota_update_go/handlers/sub_configs_get_test.go b/services/ota_update_go/handlers/sub_configs_get_test.go new file mode 100644 index 0000000..6bd6387 --- /dev/null +++ b/services/ota_update_go/handlers/sub_configs_get_test.go @@ -0,0 +1,136 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionConfigsGetList(t *testing.T) { + mock := mo.MockSubscriptionConfigurations{} + services.GetDB().SetSubConfigurations(&mock) + testFeatureID := uuid.MustParse("ecfb89e0-ca03-4aa9-a43a-a9d703256edb") + testConfiguration := common.BinaryHex{0x46, 0x44, 0x73, 0x81} + testDID := common.BinaryHex{0x32, 0x45} + testPID := common.BinaryHex{0xAA, 0xAA} + testMask := common.BinaryHex{0x00, 0x00, 0x00, 0x10} + listData := []common.SubscriptionConfiguration{ + { + SubscriptionFeatureID: testFeatureID, + ECU: "TEST", + SoftwareVersion: "SoftwareVersion", + HardwareVersion: "HardwareVersion", + Configuration: &testConfiguration, + DID: &testDID, + PID: &testPID, + Mask: &testMask, + }, + } + expectedResp := `{"data":[{"feature_id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","ecu":"TEST","sw_version":"SoftwareVersion","hw_version":"HardwareVersion","configuration":"46447381","did":"3245","pid":"aaaa","mask":"00000010"}],"total":1}` + expectedRespNoTotal := `{"data":[{"feature_id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","ecu":"TEST","sw_version":"SoftwareVersion","hw_version":"HardwareVersion","configuration":"46447381","did":"3245","pid":"aaaa","mask":"00000010"}]}` + defaultOrder := "created_at DESC" + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionconfigs", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Feature Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionconfigs?feature_id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{ + SubscriptionFeatureID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "ECU parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionconfigs?ecu=TEST", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{ + ECU: "TEST", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionconfigs?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionconfigs", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionConfiguration{}, + 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/subscriptionconfigs?ecu=TEST&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/subscriptionconfigs?ecu=TEST&limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleSubscriptionConfigsGetList, &mock) +} diff --git a/services/ota_update_go/handlers/sub_feature_add.go b/services/ota_update_go/handlers/sub_feature_add.go new file mode 100644 index 0000000..e228702 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_add.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/go-pg/pg/v10/orm" +) + +// @deprecated +// HandleSubscriptionFeatureAdd godoc +// @Summary Add subscription feature +// @Description Create subscription feature +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param feature body SubFeatureRequest true "Subscription feature data" +// @Success 200 {object} common.SubscriptionFeature +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionfeature [post] +func HandleSubscriptionFeatureAdd(w http.ResponseWriter, r *http.Request) { + subscriptionFeatureAdd.Handle(w, r) +} + +var subscriptionFeatureAdd = controllers.NewCreate(&subscriptionFeatureCreateHelper{}) + +type subscriptionFeatureCreateHelper struct { + SubscriptionFeatureHelper +} + +func (h *subscriptionFeatureCreateHelper) QueryInsert(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubFeatures().Insert(model.(*common.SubscriptionFeature)) +} + +type SubFeatureRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/services/ota_update_go/handlers/sub_feature_add_test.go b/services/ota_update_go/handlers/sub_feature_add_test.go new file mode 100644 index 0000000..d2dae59 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_add_test.go @@ -0,0 +1,55 @@ +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 TestSubscriptionFeatureAdd(t *testing.T) { + mock := mo.MockSubscriptionFeatures{} + services.GetDB().SetSubFeatures(&mock) + validData := common.SubscriptionFeature{ + Name: "feature_name", + Description: "description", + } + tests := []mo.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionfeature", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required. Description required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionfeature", common.SubscriptionFeature{ + Name: "feature_name", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Description required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionfeature", validData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","name":"feature_name","description":"description"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionfeature", 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.HandleSubscriptionFeatureAdd, &mock) +} diff --git a/services/ota_update_go/handlers/sub_feature_assign.go b/services/ota_update_go/handlers/sub_feature_assign.go new file mode 100644 index 0000000..2b2463e --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_assign.go @@ -0,0 +1,49 @@ +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/utils" + "github.com/google/uuid" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// @deprecated +// HandleSubscriptionFeatureAssign godoc +// @Summary Assign feature to package +// @Description Assign feature to package +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param feature body SubFeatureAssignRequest true "Assignment package data" +// @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 /subscriptionpackagefeature [post] +func HandleSubscriptionFeatureAssign(w http.ResponseWriter, r *http.Request) { + item := SubFeatureAssignRequest{} + err := httphandlers.ParseRequest(r, &item) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + db := services.GetDB().GetSubPackages() + _, err = db.AssociateFeature(item.PackageID, item.FeatureID) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "OK", + }) +} + +type SubFeatureAssignRequest struct { + PackageID uuid.UUID `json:"package_id" validate:"required"` + FeatureID uuid.UUID `json:"feature_id" validate:"required"` +} diff --git a/services/ota_update_go/handlers/sub_feature_assign_test.go b/services/ota_update_go/handlers/sub_feature_assign_test.go new file mode 100644 index 0000000..f933999 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_assign_test.go @@ -0,0 +1,57 @@ +package handlers_test + +import ( + "fmt" + "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" + "github.com/google/uuid" +) + +func TestSubscriptionFeatureAssign(t *testing.T) { + mock := mo.MockSubscriptionPackages{} + services.GetDB().SetSubPackages(&mock) + testPackageID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + testFeatureID := uuid.MustParse("ecfb89e0-ca03-4aa9-a43a-a9d703256edb") + validData := handlers.SubFeatureAssignRequest{ + PackageID: testPackageID, + FeatureID: testFeatureID, + } + tests := []mo.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackagefeature", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"PackageID required. FeatureID required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackagefeature", handlers.SubFeatureAssignRequest{ + PackageID: testPackageID, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"FeatureID required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackagefeature", validData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"OK"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackagefeature", 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.HandleSubscriptionFeatureAssign, &mock) +} diff --git a/services/ota_update_go/handlers/sub_feature_delete.go b/services/ota_update_go/handlers/sub_feature_delete.go new file mode 100644 index 0000000..6ead303 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_delete.go @@ -0,0 +1,55 @@ +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/google/uuid" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionFeatureDelete godoc +// @Summary Delete subscription feature +// @Description Delete subscription feature data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string true "Subscription feature 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 /subscriptionfeature [delete] +func HandleSubscriptionFeatureDelete(w http.ResponseWriter, r *http.Request) { + subscriptionFeatureDelete.Handle(w, r) +} + +var subscriptionFeatureDelete = controllers.NewDelete(&subscriptionFeatureDeleteHelper{}) + +type subscriptionFeatureDeleteHelper struct { + SubscriptionFeatureHelper +} + +func (h *subscriptionFeatureDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} { + req := common.SubscriptionFeature{} + decoder := schema.NewDecoder() + + decoder.SetAliasTag("json") + decoder.Decode(&req, r.URL.Query()) + + return &req +} + +func (h *subscriptionFeatureDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubFeatures().Delete(model.(*common.SubscriptionFeature)) +} + +type SubscriptionFeatureDeleteRequest struct { + ID uuid.UUID `validate:"required"` +} diff --git a/services/ota_update_go/handlers/sub_feature_delete_test.go b/services/ota_update_go/handlers/sub_feature_delete_test.go new file mode 100644 index 0000000..a0c2797 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_delete_test.go @@ -0,0 +1,58 @@ +package handlers_test + +import ( + "fmt" + "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 TestSubscriptionFeatureDelete(t *testing.T) { + results := mocks.MockORMResults{ + ReturnedRows: 1, + AffectedRows: 1, + } + mock := mocks.MockSubscriptionFeatures{ + DBMockHelper: mocks.DBMockHelper{ + ORMResponse: &results, + }, + } + services.GetDB().SetSubFeatures(&mock) + + tests := []mocks.DBHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionfeature", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionfeature?id=0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionfeature?id=ecfb89e0-ca03-4aa9-a43a-a9d703256edb", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionfeature?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", 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.HandleSubscriptionFeatureDelete, &mock) +} diff --git a/services/ota_update_go/handlers/sub_feature_get.go b/services/ota_update_go/handlers/sub_feature_get.go new file mode 100644 index 0000000..c907155 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_get.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionFeatureGet godoc +// @Summary Get update manifest +// @Description Get update manifest by id +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription feature id" +// @Success 200 {object} common.SubscriptionFeature +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionfeature [get] +func HandleSubscriptionFeatureGet(w http.ResponseWriter, r *http.Request) { + subscriptionFeatureGet.Handle(w, r) +} + +var subscriptionFeatureGet = controllers.NewGetModel(&subscriptionFeatureGetModelHelper{}) + +type subscriptionFeatureGetModelHelper struct { + SubscriptionFeatureHelper +} + +func (h *subscriptionFeatureGetModelHelper) ParseGetModelParams(r *http.Request) interface{} { + decoder := schema.NewDecoder() + model := common.SubscriptionFeature{} + + decoder.SetAliasTag("json") + decoder.Decode(&model, r.URL.Query()) + + return &model +} + +func (h *subscriptionFeatureGetModelHelper) QueryLoad(model interface{}) error { + return services.GetDB().GetSubFeatures().Load(model.(*common.SubscriptionFeature)) +} diff --git a/services/ota_update_go/handlers/sub_feature_get_test.go b/services/ota_update_go/handlers/sub_feature_get_test.go new file mode 100644 index 0000000..c9d4b49 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_get_test.go @@ -0,0 +1,88 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionFeatureGetObject(t *testing.T) { + mock := mo.MockSubscriptionFeatures{} + services.GetDB().SetSubFeatures(&mock) + testFeatureID := uuid.MustParse("ecfb89e0-ca03-4aa9-a43a-a9d703256edb") + testConfiguration := common.BinaryHex{0x46, 0x44, 0x73, 0x81} + testDID := common.BinaryHex{0x32, 0x45} + testPID := common.BinaryHex{0xAA, 0xAA} + testMask := common.BinaryHex{0x00, 0x00, 0x00, 0x10} + expectedFilter := &common.SubscriptionFeature{ + ID: testFeatureID, + } + expectedResp := `{"id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","name":"Test Feature","description":"Test Description","configurations":[{"feature_id":"00000000-0000-0000-0000-000000000000","ecu":"TBOX","sw_version":"SoftwareVersion","hw_version":"HardwareVersion","configuration":"46447381","did":"3245","pid":"aaaa","mask":"00000010"}]}` + data := common.SubscriptionFeature{ + ID: testFeatureID, + Name: "Test Feature", + Description: "Test Description", + Configurations: []common.SubscriptionConfiguration{ + { + ECU: "TBOX", + SoftwareVersion: "SoftwareVersion", + HardwareVersion: "HardwareVersion", + Configuration: &testConfiguration, + DID: &testDID, + PID: &testPID, + Mask: &testMask, + }, + }, + } + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeature", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeature?id=ecfb89e0-ca03-4aa9-a43a-a9d703256edb", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: expectedFilter, + MockLoadResponse: &data, + }, + }, + { + Name: "Name parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeature?name=Test%20Feature", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{ + Name: "Test Feature", + }, + MockLoadResponse: &data, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeature?id=ecfb89e0-ca03-4aa9-a43a-a9d703256edb", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: expectedFilter, + MockError: fmt.Errorf("something went wrong"), + }, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleSubscriptionFeatureGet, &mock) +} diff --git a/services/ota_update_go/handlers/sub_feature_update.go b/services/ota_update_go/handlers/sub_feature_update.go new file mode 100644 index 0000000..bc493ee --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_update.go @@ -0,0 +1,44 @@ +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/google/uuid" +) + +// @deprecated +// HandleSubscriptionFeatureUpdate godoc +// @Summary Add subscription package +// @Description Create subscription package +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param feature body SubFeatureUpdateRequest true "Subscription feature data" +// @Success 200 {object} common.SubscriptionFeature +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionfeature [put] +func HandleSubscriptionFeatureUpdate(w http.ResponseWriter, r *http.Request) { + subscriptionFeatureUpdate.Handle(w, r) +} + +var subscriptionFeatureUpdate = controllers.NewUpdate(&subscriptionFeatureUpdateHelper{}) + +type subscriptionFeatureUpdateHelper struct { + SubscriptionFeatureHelper +} + +func (h *subscriptionFeatureUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubFeatures().Update(model.(*common.SubscriptionFeature)) +} + +type SubFeatureUpdateRequest struct { + ID uuid.UUID `json:"uuid"` + SubFeatureRequest +} diff --git a/services/ota_update_go/handlers/sub_feature_update_test.go b/services/ota_update_go/handlers/sub_feature_update_test.go new file mode 100644 index 0000000..4b4deb4 --- /dev/null +++ b/services/ota_update_go/handlers/sub_feature_update_test.go @@ -0,0 +1,70 @@ +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" + + "github.com/google/uuid" +) + +func TestSubscriptionFeatureUpdate(t *testing.T) { + mock := mocks.MockSubscriptionFeatures{} + services.GetDB().SetSubFeatures(&mock) + testUUID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + reqGoodData := common.SubscriptionFeature{ + ID: testUUID, + Name: "Test", + Description: "Description", + } + + tests := []mocks.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required. Description required","error":"Bad Request"}`, + }, + { + Name: "Missing PK", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", common.SubscriptionFeature{ + Name: "Test", + Description: "Description", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Bad Data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", common.SubscriptionFeature{ + ID: testUUID, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required. Description required","error":"Bad Request"}`, + }, + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", reqGoodData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"Test","description":"Description"}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionfeature", reqGoodData), + 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.HandleSubscriptionFeatureUpdate, &mock) +} diff --git a/services/ota_update_go/handlers/sub_features_get.go b/services/ota_update_go/handlers/sub_features_get.go new file mode 100644 index 0000000..6c40fb1 --- /dev/null +++ b/services/ota_update_go/handlers/sub_features_get.go @@ -0,0 +1,84 @@ +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/google/uuid" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionFeaturesGet godoc +// @Summary Search subscription features +// @Description Get subscription features filtered by id or name +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription feature id" +// @Param name query string false "Subscription feature name" +// @Param description query string false "Subscription feature description" +// @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 /subscriptionfeatures [get] +func HandleSubscriptionFeaturesGet(w http.ResponseWriter, r *http.Request) { + subscriptionFeaturesGet.Handle(w, r) +} + +var subscriptionFeaturesGet = controllers.NewGetList(&subscriptionFeaturesGetListHelper{}) + +type subscriptionFeaturesGetListHelper struct { + SubscriptionFeatureHelper +} + +func (h *subscriptionFeaturesGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} { + schema := schema.NewDecoder() + filter := common.SubscriptionFeature{} + + schema.SetAliasTag("json") + schema.Decode(&filter, r.URL.Query()) + + return &filter +} + +func (h *subscriptionFeaturesGetListHelper) QueryCount(filter interface{}) (int, error) { + return services.GetDB().GetSubFeatures().Count(filter.(*common.SubscriptionFeature)) +} + +func (h *subscriptionFeaturesGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + return services.GetDB().GetSubFeatures().Select(filter.(*common.SubscriptionFeature), options) +} + +type SubscriptionFeatureHelper struct { + controllers.HelperBase +} + +func (h *SubscriptionFeatureHelper) NewModel() interface{} { + return &common.SubscriptionFeature{} +} + +func (h *SubscriptionFeatureHelper) HasPK(filter interface{}) bool { + result := filter.(*common.SubscriptionFeature) + return result.ID != uuid.Nil || result.Name != "" +} + +func (h *SubscriptionFeatureHelper) ValidatePK(model interface{}) error { + result := model.(*common.SubscriptionFeature) + + err := validator.ValidateField(result.ID, "required") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} diff --git a/services/ota_update_go/handlers/sub_features_get_test.go b/services/ota_update_go/handlers/sub_features_get_test.go new file mode 100644 index 0000000..2c67c54 --- /dev/null +++ b/services/ota_update_go/handlers/sub_features_get_test.go @@ -0,0 +1,127 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionFeaturesGetList(t *testing.T) { + mock := mo.MockSubscriptionFeatures{} + services.GetDB().SetSubFeatures(&mock) + testFeatureID := uuid.MustParse("ecfb89e0-ca03-4aa9-a43a-a9d703256edb") + expectedResp := `{"data":[{"id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","name":"Test Feature","description":"Test Description"}],"total":1}` + expectedRespNoTotal := `{"data":[{"id":"ecfb89e0-ca03-4aa9-a43a-a9d703256edb","name":"Test Feature","description":"Test Description"}]}` + defaultOrder := "created_at DESC" + listData := []common.SubscriptionFeature{ + { + ID: testFeatureID, + Name: "Test Feature", + Description: "Test Description", + }, + } + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{ + ID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Name parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeatures?name=Test%20Feature", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{ + Name: "Test Feature", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeatures?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionfeatures", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionFeature{}, + 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/subscriptionpackages?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/subscriptionpackages?limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleSubscriptionFeaturesGet, &mock) +} diff --git a/services/ota_update_go/handlers/sub_package_add.go b/services/ota_update_go/handlers/sub_package_add.go new file mode 100644 index 0000000..2569ec3 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_add.go @@ -0,0 +1,42 @@ +package handlers + +import ( + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/go-pg/pg/v10/orm" +) + +// @deprecated +// HandleSubscriptionPackageAdd godoc +// @Summary Add subscription package +// @Description Create subscription package +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param package body SubPackagesAddRequest true "Subscription package data" +// @Success 200 {object} common.SubscriptionPackage +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionpackage [post] +func HandleSubscriptionPackageAdd(w http.ResponseWriter, r *http.Request) { + subscriptionPackageAdd.Handle(w, r) +} + +var subscriptionPackageAdd = controllers.NewCreate(&subscriptionPackageCreateHelper{}) + +type subscriptionPackageCreateHelper struct { + SubscriptionPackagesHelper +} + +func (h *subscriptionPackageCreateHelper) QueryInsert(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubPackages().Insert(model.(*common.SubscriptionPackage)) +} + +type SubPackagesAddRequest struct { + Name string `json:"name"` +} diff --git a/services/ota_update_go/handlers/sub_package_add_test.go b/services/ota_update_go/handlers/sub_package_add_test.go new file mode 100644 index 0000000..a2b8ec0 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_add_test.go @@ -0,0 +1,48 @@ +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 TestSubscriptionPackageAdd(t *testing.T) { + mock := mo.MockSubscriptionPackages{} + services.GetDB().SetSubPackages(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackage", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackage", common.SubscriptionPackage{ + Name: "package_name", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"package_name"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/subscriptionpackage", common.SubscriptionPackage{ + Name: "package_name", + }), + 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.HandleSubscriptionPackageAdd, &mock) +} diff --git a/services/ota_update_go/handlers/sub_package_delete.go b/services/ota_update_go/handlers/sub_package_delete.go new file mode 100644 index 0000000..ba7b592 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_delete.go @@ -0,0 +1,51 @@ +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" +) + +// @deprecated +// SubscriptionPackageDelete godoc +// @Summary Delete subscription package +// @Description Delete subscription package data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string true "Subscription package 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 /subscriptionpackage [delete] +func HandleSubscriptionPackageDelete(w http.ResponseWriter, r *http.Request) { + subscriptionPackageDelete.Handle(w, r) +} + +var subscriptionPackageDelete = controllers.NewDelete(&subscriptionPackageDeleteHelper{}) + +type subscriptionPackageDeleteHelper struct { + SubscriptionPackagesHelper +} + +func (h *subscriptionPackageDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} { + req := common.SubscriptionPackage{} + decoder := schema.NewDecoder() + + decoder.SetAliasTag("json") + decoder.Decode(&req, r.URL.Query()) + + return &req +} + +func (h *subscriptionPackageDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubPackages().Delete(model.(*common.SubscriptionPackage)) +} diff --git a/services/ota_update_go/handlers/sub_package_delete_test.go b/services/ota_update_go/handlers/sub_package_delete_test.go new file mode 100644 index 0000000..2de0263 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_delete_test.go @@ -0,0 +1,58 @@ +package handlers_test + +import ( + "fmt" + "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 TestSubscriptionPackageDelete(t *testing.T) { + results := mocks.MockORMResults{ + ReturnedRows: 1, + AffectedRows: 1, + } + mock := mocks.MockSubscriptionPackages{ + DBMockHelper: mocks.DBMockHelper{ + ORMResponse: &results, + }, + } + services.GetDB().SetSubPackages(&mock) + + tests := []mocks.DBHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionpackage", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionpackage?id=0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionpackage?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionpackage?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", 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.HandleSubscriptionPackageDelete, &mock) +} diff --git a/services/ota_update_go/handlers/sub_package_get.go b/services/ota_update_go/handlers/sub_package_get.go new file mode 100644 index 0000000..cd8d715 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_get.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionPackageGet godoc +// @Summary Get update manifest +// @Description Get update manifest by id +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription package id" +// @Success 200 {object} common.SubscriptionPackage +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionpackage [get] +func HandleSubscriptionPackageGet(w http.ResponseWriter, r *http.Request) { + subscriptionPackageGet.Handle(w, r) +} + +var subscriptionPackageGet = controllers.NewGetModel(&subscriptionPackageGetModelHelper{}) + +type subscriptionPackageGetModelHelper struct { + SubscriptionPackagesHelper +} + +func (h *subscriptionPackageGetModelHelper) ParseGetModelParams(r *http.Request) interface{} { + decoder := schema.NewDecoder() + model := common.SubscriptionPackage{} + + decoder.SetAliasTag("json") + decoder.Decode(&model, r.URL.Query()) + + return &model +} + +func (h *subscriptionPackageGetModelHelper) QueryLoad(model interface{}) error { + return services.GetDB().GetSubPackages().Load(model.(*common.SubscriptionPackage)) +} diff --git a/services/ota_update_go/handlers/sub_package_get_test.go b/services/ota_update_go/handlers/sub_package_get_test.go new file mode 100644 index 0000000..145fec8 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_get_test.go @@ -0,0 +1,78 @@ +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" + "github.com/google/uuid" +) + +func TestSubscriptionPackageGetObject(t *testing.T) { + mock := mo.MockSubscriptionPackages{} + services.GetDB().SetSubPackages(&mock) + expectedFilter := &common.SubscriptionPackage{ + ID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + } + expectedResp := `{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"Test Package","features":[{"id":"6666bd1d-76d3-41e5-a44e-13c479e55ab6","name":"Test Feature","description":"Feature Description"}]}` + data := common.SubscriptionPackage{ + ID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + Name: "Test Package", + Features: []common.SubscriptionFeature{ + { + ID: uuid.MustParse("6666bd1d-76d3-41e5-a44e-13c479e55ab6"), + Name: "Test Feature", + Description: "Feature Description", + }, + }, + } + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackage", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackage?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: expectedFilter, + MockLoadResponse: data, + }, + }, + { + Name: "Name parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackage?name=Test%20Package", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{ + Name: "Test Package", + }, + MockLoadResponse: data, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackage?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: expectedFilter, + MockError: fmt.Errorf("something went wrong"), + }, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleSubscriptionPackageGet, &mock) +} diff --git a/services/ota_update_go/handlers/sub_package_update.go b/services/ota_update_go/handlers/sub_package_update.go new file mode 100644 index 0000000..2a55de7 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_update.go @@ -0,0 +1,44 @@ +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/google/uuid" +) + +// @deprecated +// HandleSubscriptionPackageUpdate godoc +// @Summary Update subscription package +// @Description Update subscription package +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param package body SubPackagesUpdateRequest true "Subscription package data" +// @Success 200 {object} common.SubscriptionPackage +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /subscriptionpackage [put] +func HandleSubscriptionPackageUpdate(w http.ResponseWriter, r *http.Request) { + subscriptionPackageUpdate.Handle(w, r) +} + +var subscriptionPackageUpdate = controllers.NewUpdate(&subscriptionPackageUpdateHelper{}) + +type subscriptionPackageUpdateHelper struct { + SubscriptionPackagesHelper +} + +func (h *subscriptionPackageUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubPackages().Update(model.(*common.SubscriptionPackage)) +} + +type SubPackagesUpdateRequest struct { + ID uuid.UUID `json:"uuid"` + SubPackagesAddRequest +} diff --git a/services/ota_update_go/handlers/sub_package_update_test.go b/services/ota_update_go/handlers/sub_package_update_test.go new file mode 100644 index 0000000..cb21d32 --- /dev/null +++ b/services/ota_update_go/handlers/sub_package_update_test.go @@ -0,0 +1,69 @@ +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" + + "github.com/google/uuid" +) + +func TestSubscriptionPackageUpdate(t *testing.T) { + mock := mocks.MockSubscriptionPackages{} + services.GetDB().SetSubPackages(&mock) + testUUID := uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0") + reqGoodData := common.SubscriptionPackage{ + ID: testUUID, + Name: "Test", + } + + tests := []mocks.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionpackage", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required","error":"Bad Request"}`, + }, + { + Name: "Missing PK", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionpackage", common.SubscriptionPackage{ + Name: "Test", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Bad Data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionpackage", common.SubscriptionPackage{ + ID: testUUID, + Name: "", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required","error":"Bad Request"}`, + }, + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionpackage", reqGoodData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"Test"}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/subscriptionpackage", reqGoodData), + 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.HandleSubscriptionPackageUpdate, &mock) +} diff --git a/services/ota_update_go/handlers/sub_packages_get.go b/services/ota_update_go/handlers/sub_packages_get.go new file mode 100644 index 0000000..95d90b6 --- /dev/null +++ b/services/ota_update_go/handlers/sub_packages_get.go @@ -0,0 +1,82 @@ +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/google/uuid" + "github.com/gorilla/schema" +) + +// @deprecated +// HandleSubscriptionPackagesGetList godoc +// @Summary Search subscription packages +// @Description Get subscription packages filtered by id, name +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription package id" +// @Param name query string false "Subscription package 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 /subscriptionpackages [get] +func HandleSubscriptionPackagesGetList(w http.ResponseWriter, r *http.Request) { + subscriptionPackagesGetList.Handle(w, r) +} + +var subscriptionPackagesGetList = controllers.NewGetList(&subscriptionPackagesGetListHelper{}) + +type subscriptionPackagesGetListHelper struct { + SubscriptionPackagesHelper +} + +func (h *subscriptionPackagesGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} { + schema := schema.NewDecoder() + filter := common.SubscriptionPackage{} + + schema.SetAliasTag("json") + schema.Decode(&filter, r.URL.Query()) + + return &filter +} + +func (h *subscriptionPackagesGetListHelper) QueryCount(filter interface{}) (int, error) { + return services.GetDB().GetSubPackages().Count(filter.(*common.SubscriptionPackage)) +} + +func (h *subscriptionPackagesGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + return services.GetDB().GetSubPackages().Select(filter.(*common.SubscriptionPackage), options) +} + +type SubscriptionPackagesHelper struct { + controllers.HelperBase +} + +func (h *SubscriptionPackagesHelper) NewModel() interface{} { + return &common.SubscriptionPackage{} +} + +func (h *SubscriptionPackagesHelper) HasPK(filter interface{}) bool { + result := filter.(*common.SubscriptionPackage) + return result.ID != uuid.Nil || result.Name != "" +} + +func (h *SubscriptionPackagesHelper) ValidatePK(model interface{}) error { + result := model.(*common.SubscriptionPackage) + + err := validator.ValidateField(result.ID, "required") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} diff --git a/services/ota_update_go/handlers/sub_packages_get_test.go b/services/ota_update_go/handlers/sub_packages_get_test.go new file mode 100644 index 0000000..578cd09 --- /dev/null +++ b/services/ota_update_go/handlers/sub_packages_get_test.go @@ -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" + "github.com/google/uuid" +) + +func TestSubscriptionPackagesGetList(t *testing.T) { + mock := mo.MockSubscriptionPackages{} + services.GetDB().SetSubPackages(&mock) + expectedResp := `{"data":[{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"Test Package"}],"total":1}` + expectedRespNoTotal := `{"data":[{"id":"0557bd1d-76d3-41e5-a44e-13c479e55ab0","name":"Test Package"}]}` + defaultOrder := "created_at DESC" + listData := []common.SubscriptionPackage{ + { + ID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + Name: "Test Package", + }, + } + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages?id=0557bd1d-76d3-41e5-a44e-13c479e55ab0", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{ + ID: uuid.MustParse("0557bd1d-76d3-41e5-a44e-13c479e55ab0"), + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Name parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages?name=Test", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{ + Name: "Test", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/subscriptionpackages", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.SubscriptionPackage{}, + 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/subscriptionpackages?name=Test&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/subscriptionpackages?name=Test&limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleSubscriptionPackagesGetList, &mock) +} diff --git a/services/ota_update_go/handlers/subscriptions_delete.go b/services/ota_update_go/handlers/subscriptions_delete.go new file mode 100644 index 0000000..b073d2d --- /dev/null +++ b/services/ota_update_go/handlers/subscriptions_delete.go @@ -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/validator" + + "github.com/go-pg/pg/v10/orm" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +// @deprecated +// HandleSubscriptionDelete godoc +// @Summary Delete subscription +// @Description Delete subscription data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Subscription id" +// @Param name query string false "Subscription type name" +// @Param vin query string false "Subscription vin" +// @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 /subscription [delete] +func HandleSubscriptionDelete(w http.ResponseWriter, r *http.Request) { + subscriptionDelete.Handle(w, r) +} + +var subscriptionDelete = controllers.NewDelete(&subscriptionDeleteHelper{}) + +type subscriptionDeleteHelper struct { + controllers.HandleDelete +} + +func (h *subscriptionDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} { + req := queries.SubscriptionDeleteRequest{} + decoder := schema.NewDecoder() + + decoder.SetAliasTag("json") + decoder.Decode(&req, r.URL.Query()) + + return &req +} + +func (h *subscriptionDeleteHelper) ValidatePK(model interface{}) error { + req, ok := model.(*queries.SubscriptionDeleteRequest) + if !ok { + return errors.New("invalid request") + } + + if req.ID > 0 { + return nil + } + + err := validator.ValidateStruct(req) + if err != nil { + return errors.New("primary key required") + } + + return nil +} + +func (h *subscriptionDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) { + return services.GetDB().GetSubscriptions().Delete(model.(*queries.SubscriptionDeleteRequest)) +} diff --git a/services/ota_update_go/handlers/subscriptions_delete_test.go b/services/ota_update_go/handlers/subscriptions_delete_test.go new file mode 100644 index 0000000..3919354 --- /dev/null +++ b/services/ota_update_go/handlers/subscriptions_delete_test.go @@ -0,0 +1,109 @@ +package handlers_test + +import ( + "fmt" + "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 TestSubscriptionDelete(t *testing.T) { + results := mocks.MockORMResults{ + ReturnedRows: 0, + AffectedRows: 1, + } + mock := mocks.MockSubscriptions{ + DBMockHelper: mocks.DBMockHelper{ + ORMResponse: &results, + }, + } + services.GetDB().SetSubscriptions(&mock) + + tests := []mocks.DBHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription?id=0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription?id=100", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "Good vin and sub name", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription?vin=1G1FP87S3GN100062&name=TEST", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "Bad vin", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription?vin=1G1FP87S3GN100&name=TEST", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "Record not found", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscription?vin=1G1FP87S3GN100062&name=TEST", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: `{"message":"Nothing deleted","error":"Not Found"}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 0 + }, + }, + }, + { + Name: "DB error", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/subscriptionpackage?id=100", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mocks.DBTestCase{ + MockError: fmt.Errorf("something went wrong"), + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + } + + mocks.RunDBTests(t, tests, handlers.HandleSubscriptionDelete, &mock) +} diff --git a/services/ota_update_go/handlers/superset_embedded_dashboards.go b/services/ota_update_go/handlers/superset_embedded_dashboards.go new file mode 100644 index 0000000..033671e --- /dev/null +++ b/services/ota_update_go/handlers/superset_embedded_dashboards.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/superset" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleSupersetEmbeddedDashboardsGet godoc +// @Summary Get the list of embeddable superset dashboards +// @Description Returns list of dashboard embedding id and their title +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} []superset.EmbeddableDashboard +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /dashboard/embedded-dashboards [get] +func HandleSupersetEmbeddedDashboardsGet(w http.ResponseWriter, r *http.Request) { + redisClient := services.RedisClientPool().GetFromPool() + defer redisClient.Close() + + accToken, err := getAccessTokenFunc(redisClient) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + dashboards, err := superset.GetEmbeddableDashboards(accToken) + + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, &dashboards) +} diff --git a/services/ota_update_go/handlers/superset_guest_token.go b/services/ota_update_go/handlers/superset_guest_token.go new file mode 100644 index 0000000..8978ca5 --- /dev/null +++ b/services/ota_update_go/handlers/superset_guest_token.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "net/http" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/superset" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +type GuestTokenResp struct { + Token string `json:"token"` +} + +var ( + getAccessTokenFunc = superset.GetAccessToken + getGuestTokenFunc = superset.GetGuestToken +) + +// HandleDashboardToken godoc +// @Summary Get token for accessing dashboard +// @Description Returns token +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} GuestTokenResp +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /dashboard/guest-token [get] +func HandleDashboardToken(w http.ResponseWriter, r *http.Request) { + redisClient := services.RedisClientPool().GetFromPool() + defer redisClient.Close() + + accToken, err := getAccessTokenFunc(redisClient) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + guestToken, err := getGuestTokenFunc(redisClient, accToken) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, GuestTokenResp{Token: guestToken}) +} + +func SetGetAccessTokenFunc(f func(r redis.Client) (string, error)) { + getAccessTokenFunc = f +} + +func SetGetGuestTokenFunc(f func(r redis.Client, accToken string) (string, error)) { + getGuestTokenFunc = f +} diff --git a/services/ota_update_go/handlers/supplier_activate.go b/services/ota_update_go/handlers/supplier_activate.go new file mode 100644 index 0000000..a0712bb --- /dev/null +++ b/services/ota_update_go/handlers/supplier_activate.go @@ -0,0 +1,49 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + "time" + + "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" +) + +// HandleSupplierActivate godoc +// @Summary Activate supplier account +// @Description Updates the activated date for the supplier +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param email path string true "Supplier email address" +// @Success 200 {object} ApproveResponse +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /supplier/activate/{email} [post] +func HandleSupplierActivate(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + email := params.ByName("email") + + err := validator.ValidateField(email, "required,email") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB().GetSupplierAccounts().Approve(email) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, ApproveResponse{ + Activate: time.Now(), + }) +} + +type ApproveResponse struct { + Activate time.Time `json:"activate"` +} diff --git a/services/ota_update_go/handlers/supplier_activate_test.go b/services/ota_update_go/handlers/supplier_activate_test.go new file mode 100644 index 0000000..196a40e --- /dev/null +++ b/services/ota_update_go/handlers/supplier_activate_test.go @@ -0,0 +1,51 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "regexp" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleSupplierActivate(t *testing.T) { + mock := mocks.MockSupplierAccounts{} + services.GetDB().SetSupplierAccount(&mock) + + tests := []mocks.DBHttpTest{ + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/supplier/activate/test@supplier.com", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponseRegex: regexp.MustCompile(`{"activate":"[^"]+"}`), + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/supplier/activate/test@supplier.com", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mocks.DBTestCase{ + MockError: fmt.Errorf("something went wrong"), + }, + }, + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/supplier/activate/", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: err404NotFound, + }, + { + Name: "Bad Data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/supplier/activate/test", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"email email ","error":"Bad Request"}`, + }, + } + + mocks.RunParamHttpTests(t, tests, handlers.HandleSupplierActivate, "/supplier/activate/:email", &mock) +} diff --git a/services/ota_update_go/handlers/supplier_delete.go b/services/ota_update_go/handlers/supplier_delete.go new file mode 100644 index 0000000..98f7e75 --- /dev/null +++ b/services/ota_update_go/handlers/supplier_delete.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "net/http" + + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/go-pg/pg/v10/orm" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" +) + +// HandleSupplierDelete godoc +// @Summary Delete filter from vehicle +// @Description Delete filter from vehicle +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param email path string true "Email" +// @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 /supplier/{email} [delete] +func HandleSupplierDelete(w http.ResponseWriter, r *http.Request) { + supplierDelete.Handle(w, r) +} + +var supplierDelete = controllers.NewDelete(&supplierDeleteHelper{}) + +type supplierDeleteHelper struct { + controllers.HandleDelete +} + +func (h *supplierDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} { + req := common.SupplierAccount{} + params := httprouter.ParamsFromContext(r.Context()) + + req.Email = params.ByName("email") + + return &req +} + +func (h *supplierDeleteHelper) ValidatePK(model interface{}) error { + req, ok := model.(*common.SupplierAccount) + if !ok { + return errors.New("invalid request") + } + + if req.Email != "" { + return nil + } + + err := validator.ValidateStruct(req) + if err != nil { + return errors.New("email required") + } + + return nil +} + +func (h *supplierDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) { + return services.GetDB().GetSupplierAccounts().Delete(model.(*common.SupplierAccount)) +} diff --git a/services/ota_update_go/handlers/supplier_delete_test.go b/services/ota_update_go/handlers/supplier_delete_test.go new file mode 100644 index 0000000..83a9601 --- /dev/null +++ b/services/ota_update_go/handlers/supplier_delete_test.go @@ -0,0 +1,90 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/testrunner" +) + +func TestSupplierDelete(t *testing.T) { + route := "/supplier/:email" + results := mocks.MockORMResults{ + ReturnedRows: 0, + AffectedRows: 1, + } + mock := mocks.MockSupplierAccounts{ + DBMockHelper: mocks.DBMockHelper{ + ORMResponse: &results, + }, + } + services.GetDB().SetSupplierAccount(&mock) + + tests := []testrunner.TestCase{ + { + Name: "Record not found", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/supplier/no@supplier.com", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: `{"message":"Nothing deleted","error":"Not Found"}`, + }, + DBTestCase: &mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 0 + }, + }, + }, + { + Name: "No id", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/supplier", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: err404NotFound, + }, + }, + { + Name: "Good id", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/supplier/test@supplier.com", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + DBTestCase: &mocks.DBTestCase{ + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + { + Name: "DB error", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/supplier/test@supplier.com", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + }, + DBTestCase: &mocks.DBTestCase{ + MockError: fmt.Errorf("something went wrong"), + SetupMockResponse: func() { + results.AffectedRows = 1 + }, + }, + }, + } + + for _, test := range tests { + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mock) + } + + w := test.HttpTestCase.TestWithParamPath(handlers.HandleSupplierDelete, route) + + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } +} diff --git a/services/ota_update_go/handlers/supplier_update.go b/services/ota_update_go/handlers/supplier_update.go new file mode 100644 index 0000000..0d3ef9e --- /dev/null +++ b/services/ota_update_go/handlers/supplier_update.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/go-pg/pg/v10/orm" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" +) + +// HandleSupplierUpdate godoc +// @Summary Approve supplier account +// @Description Approve supplier account with Azure AD oid +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param email path string true "Supplier email address" +// @Param account body common.SupplierAccount true "Supplier account data" +// @Success 200 {object} common.SupplierAccount +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /supplier/{email} [put] +func HandleSupplierUpdate(w http.ResponseWriter, r *http.Request) { + supplierUpdate.Handle(w, r) +} + +var supplierUpdate = controllers.NewUpdate(&supplierUpdateHelper{}) + +type supplierUpdateHelper struct { + SuppliersHelper +} + +func (h *supplierUpdateHelper) ParseRequest(r *http.Request, data interface{}) error { + model, ok := data.(*common.SupplierAccount) + if !ok { + return errors.New("model is not SupplierAccount") + } + + err := json.NewDecoder(r.Body).Decode(data) + if err != nil { + return err + } + + params := httprouter.ParamsFromContext(r.Context()) + model.Email = params.ByName("email") + + return nil +} + +func (h *supplierUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) { + err := validator.ValidateStruct(model) + if err != nil { + return nil, err + } + + result, err := services.GetDB().GetSupplierAccounts().Update(model.(*common.SupplierAccount)) + if err == nil && result.RowsAffected() == 0 { + return result, errors.New("account does not exist") + } + + return result, err +} diff --git a/services/ota_update_go/handlers/supplier_update_test.go b/services/ota_update_go/handlers/supplier_update_test.go new file mode 100644 index 0000000..e5a0483 --- /dev/null +++ b/services/ota_update_go/handlers/supplier_update_test.go @@ -0,0 +1,105 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/testrunner" +) + +const err404NotFound = "404 page not found\n" + +func TestHandleSupplierUpdate(t *testing.T) { + route := "/supplier/:email" + mock := mocks.MockSupplierAccounts{} + reqGoodData := common.SupplierAccount{ + SupplierOrganizationID: 14, + Company: "TEST", + Address: "100 Main", + Contact: "John Doe", + Telephone: "+1-555-555-5555", + Program: "Ocean", + ECUs: []string{"ADAS"}, + } + + tests := []testrunner.TestCase{ + { + Name: "No data", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/supplier", nil), + ExpectedStatus: http.StatusNotFound, + ExpectedResponse: err404NotFound, + }, + }, + { + Name: "Bad Data", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/supplier/test", common.SupplierAccount{ + Company: "TEST", + Address: "100 Main", + Contact: "John Doe", + Telephone: "+1-555-555-5555", + Program: "Ocean", + ECUs: []string{}, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + }, + { + Name: "Good data", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/supplier/test@supplier.com", reqGoodData), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"supplier_organization_id":"14","email":"test@supplier.com","company":"TEST","address":"100 Main","contact":"John Doe","telephone":"+1-555-555-5555","program":"Ocean","ecus":["ADAS"]}`, + }, + }, + { + Name: "Error unregistered email", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/supplier/test@supplier.com", reqGoodData), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"account does not exist","error":"Service Unavailable"}`, + Setup: func() { + mock.ORMResponse = &mocks.MockORMResults{AffectedRows: 0} + services.GetDB().SetSupplierAccount(&mock) + }, + }, + }, + { + Name: "Error", + HttpTestCase: &tester.HttpTestCase{Request: th.MakeTestRequest(http.MethodPut, "http://example.com/supplier/test@supplier.com", reqGoodData), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + }, + DBTestCase: &mocks.DBTestCase{ + MockError: fmt.Errorf("something went wrong"), + }, + }, + } + + for _, test := range tests { + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mock) + } + + mock.ORMResponse = &mocks.MockORMResults{AffectedRows: 1} + services.GetDB().SetSupplierAccount(&mock) + + if test.HttpTestCase != nil && test.HttpTestCase.Setup != nil { + test.HttpTestCase.Setup() + } + + w := test.HttpTestCase.TestWithParamPath(handlers.HandleSupplierUpdate, route) + + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } +} diff --git a/services/ota_update_go/handlers/suppliers_get.go b/services/ota_update_go/handlers/suppliers_get.go new file mode 100644 index 0000000..2396e2b --- /dev/null +++ b/services/ota_update_go/handlers/suppliers_get.go @@ -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/db/queries" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/gorilla/schema" +) + +// HandleSuppliersGetList godoc +// @Summary Search supplier accounts +// @Description Get supplier accounts filtered by id or email +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Supplier id" +// @Param email query string false "Supplier email" +// @Param limit query int false "Max number of records" +// @Param offset query int false "Records offset" +// @Success 200 {object} common.JSONDBQueryResult{data=[]common.SupplierAccount} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /suppliers [get] +func HandleSuppliersGetList(w http.ResponseWriter, r *http.Request) { + suppliersGetList.Handle(w, r) +} + +var suppliersGetList = controllers.NewGetList(&suppliersGetListHelper{}) + +type suppliersGetListHelper struct { + SuppliersHelper +} + +func (h *suppliersGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} { + schema := schema.NewDecoder() + filter := common.SupplierAccount{} + + schema.SetAliasTag("json") + schema.Decode(&filter, r.URL.Query()) + + return &filter +} + +func (h *suppliersGetListHelper) QueryCount(filter interface{}) (int, error) { + return services.GetDB().GetSupplierAccounts().Count(filter.(*common.SupplierAccount)) +} + +func (h *suppliersGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + return services.GetDB().GetSupplierAccounts().Select(filter.(*common.SupplierAccount), options) +} + +type SuppliersHelper struct { + controllers.HelperBase +} + +func (h *SuppliersHelper) NewModel() interface{} { + return &common.SupplierAccount{} +} + +func (h *SuppliersHelper) HasPK(filter interface{}) bool { + result := filter.(*common.SupplierAccount) + return result.Email != "" +} + +func (h *SuppliersHelper) ValidatePK(model interface{}) error { + result := model.(*common.SupplierAccount) + + err := validator.ValidateField(result.Email, "required,email") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} diff --git a/services/ota_update_go/handlers/suppliers_get_test.go b/services/ota_update_go/handlers/suppliers_get_test.go new file mode 100644 index 0000000..0a4e2d4 --- /dev/null +++ b/services/ota_update_go/handlers/suppliers_get_test.go @@ -0,0 +1,135 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/testrunner" +) + +func TestHandleSuppliersGetList(t *testing.T) { + supplier := common.SupplierAccount{ + SupplierOrganizationID: 14, + Email: "test@supplier.com", + Company: "TEST", + Address: "100 Main", + Contact: "John Doe", + Telephone: "+1-555-555-5555", + Program: "Ocean", + ECUs: []string{"ADAS"}, + } + mock := mo.MockSupplierAccounts{} + services.GetDB().SetSupplierAccount(&mock) + expectedSupplier := `{"supplier_organization_id":"14","email":"test@supplier.com","company":"TEST","address":"100 Main","contact":"John Doe","telephone":"+1-555-555-5555","program":"Ocean","ecus":["ADAS"]}` + expectedResp := fmt.Sprintf(`{"data":[%s],"total":1}`, expectedSupplier) + expectedRespNoTotal := fmt.Sprintf(`{"data":[%s]}`, expectedSupplier) + defaultOrder := "created_at DESC" + listData := []common.SupplierAccount{supplier} + + tests := []testrunner.TestCase{ + { + Name: "Error", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + }, + DBTestCase: &mo.DBTestCase{ + ExpectedFilter: &common.SupplierAccount{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockError: fmt.Errorf("something went wrong"), + }, + }, + { + Name: "No parameters", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + }, + DBTestCase: &mo.DBTestCase{ + ExpectedFilter: &common.SupplierAccount{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Email parameter", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers?email=test@supplier.com", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + }, + DBTestCase: &mo.DBTestCase{ + ExpectedFilter: &common.SupplierAccount{ + Email: "test@supplier.com", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + }, + DBTestCase: &mo.DBTestCase{ + ExpectedFilter: &common.SupplierAccount{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + Name: "Wrong limit, -100", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers?email=test@supplier.com&limit=-100", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`, + }, + }, + { + Name: "Wrong limit, 1000", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/suppliers?email=test@supplier.com&limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + }, + } + + for _, test := range tests { + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mock) + } + + w := test.HttpTestCase.Test(handlers.HandleSuppliersGetList) + + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } +} diff --git a/services/ota_update_go/handlers/tags_append.go b/services/ota_update_go/handlers/tags_append.go new file mode 100644 index 0000000..bae5627 --- /dev/null +++ b/services/ota_update_go/handlers/tags_append.go @@ -0,0 +1,49 @@ +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/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleTagsAppend godoc +// @Summary Append tags for cars. +// @Description Append tags for the provided set of car VINs. +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body TagsUpdateRequest true "Update tags" +// @Success 200 {object} common.JSONMessage "Updated tags" +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /tags [post] +func HandleTagsAppend(w http.ResponseWriter, r *http.Request) { + + var req TagsUpdateRequest + + err := httphandlers.ParseRequest(r, &req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + for _, vin := range req.VINs { + c := &common.Car{ + VIN: vin, + Tags: req.Tags, + } + + _, err = services.GetDB().GetTags().UpdateDistinctTags(c) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, &req) + +} diff --git a/services/ota_update_go/handlers/tags_append_test.go b/services/ota_update_go/handlers/tags_append_test.go new file mode 100644 index 0000000..8b40a7f --- /dev/null +++ b/services/ota_update_go/handlers/tags_append_test.go @@ -0,0 +1,66 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + m "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + tr "github.com/fiskerinc/cloud-services/pkg/testrunner" +) + +func TestHandleTagsAppend(t *testing.T) { + vin := "1G1FP87S3GN100062" + + services.GetDB().SetTags(&mocks.MockTags{}) + + tests := []tr.TestCase{ + { + Name: "Basic good data test", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", + handlers.TagsUpdateRequest{ + VINs: []string{vin}, + Tags: []string{"tag1"}, + }), + ExpectedStatus: http.StatusOK, + }, + }, + { + Name: "Error", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", m.Car{ + VIN: "1G1FP87S3GN100062", + Model: "Ocean", + Year: 2021, + Trim: "Basic", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Tags required. VINs required","error":"Bad Request"}`, + }, + }, + { + Name: "Error - blank tag", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", + handlers.TagsUpdateRequest{ + VINs: []string{vin}, + Tags: []string{"tag1", ""}, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Tags[1] required","error":"Bad Request"}`, + }, + }, + } + + for _, test := range tests { + if test.HttpTestCase != nil { + w := test.HttpTestCase.Test(handlers.HandleTagsAppend) + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } + } +} diff --git a/services/ota_update_go/handlers/tags_update.go b/services/ota_update_go/handlers/tags_update.go new file mode 100644 index 0000000..bce302e --- /dev/null +++ b/services/ota_update_go/handlers/tags_update.go @@ -0,0 +1,69 @@ +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/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleTagsUpdate godoc +// @Summary Update tags for cars. +// @Description Replace the tags for the provided set of car VINs. +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body TagsUpdateRequest true "Update tags" +// @Success 200 {object} common.JSONMessage "Updated tags" +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /tags [put] +func HandleTagsUpdate(w http.ResponseWriter, r *http.Request) { + var req TagsUpdateRequest + + err := httphandlers.ParseRequest(r, &req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + for _, vin := range req.VINs { + c := &common.Car{ + VIN: vin, + Tags: RemoveDuplicateTags(req.Tags), + } + + _, err = services.GetDB().GetTags().Update(c) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, &req) +} + +type TagsUpdateRequest struct { + Tags []string `json:"tags" validate:"required,dive,required"` + VINs []string `json:"vins" validate:"required,gte=0,lte=100,dive,vin"` +} + +func RemoveDuplicateTags(tags []string) []string { + uniqueMap := make(map[string]struct{}) + + // Iterate over the original slice + for _, value := range tags { + uniqueMap[value] = struct{}{} + } + + // Create a new slice with unique elements + uniqueTags := make([]string, 0, len(uniqueMap)) + for key := range uniqueMap { + uniqueTags = append(uniqueTags, key) + } + + return uniqueTags +} diff --git a/services/ota_update_go/handlers/tags_update_test.go b/services/ota_update_go/handlers/tags_update_test.go new file mode 100644 index 0000000..f4a4c39 --- /dev/null +++ b/services/ota_update_go/handlers/tags_update_test.go @@ -0,0 +1,94 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "sort" + "testing" + + m "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + tr "github.com/fiskerinc/cloud-services/pkg/testrunner" + "github.com/stretchr/testify/assert" +) + +func TestHandleTagsUpdate(t *testing.T) { + vin := "1G1FP87S3GN100062" + + services.GetDB().SetTags(&mocks.MockTags{}) + + tests := []tr.TestCase{ + { + Name: "Basic good data test", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", + handlers.TagsUpdateRequest{ + VINs: []string{vin}, + Tags: []string{"tag1"}, + }), + ExpectedStatus: http.StatusOK, + }, + }, + { + Name: "Error", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", m.Car{ + VIN: "1G1FP87S3GN100062", + Model: "Ocean", + Year: 2021, + Trim: "Basic", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Tags required. VINs required","error":"Bad Request"}`, + }, + }, + { + Name: "Error - blank tag", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", + handlers.TagsUpdateRequest{ + VINs: []string{vin}, + Tags: []string{"tag1", ""}, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Tags[1] required","error":"Bad Request"}`, + }, + }, + } + + for _, test := range tests { + if test.HttpTestCase != nil { + w := test.HttpTestCase.Test(handlers.HandleTagsUpdate) + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } + } +} + +func TestHandleTagsUpdateSkipDuplicates(t *testing.T) { + vin := "1G1FP87S3GN100062" + + uniqueTagSet := []string{"tag1", "tag2", "tag3"} + mockTags := mocks.MockTags{} + services.GetDB().SetTags(&mockTags) + + test := tr.TestCase{ + Name: "Duplicate tags test", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/tags", + handlers.TagsUpdateRequest{ + VINs: []string{vin}, + Tags: append(uniqueTagSet, "tag1"), + }), + ExpectedStatus: http.StatusOK, + }, + } + + w := test.HttpTestCase.Test(handlers.HandleTagsUpdate) + test.HttpTestCase.ValidateHttp(t, test.Name, w) + sort.Strings(uniqueTagSet) + sort.Strings(mockTags.ReceivedTags) + assert.Equal(t, uniqueTagSet, mockTags.ReceivedTags) +} diff --git a/services/ota_update_go/handlers/trex_logs_get.go b/services/ota_update_go/handlers/trex_logs_get.go new file mode 100644 index 0000000..6901171 --- /dev/null +++ b/services/ota_update_go/handlers/trex_logs_get.go @@ -0,0 +1,115 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "otaupdate/utils" + "strconv" + "time" + + "github.com/fiskerinc/cloud-services/pkg/common" + u "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +var ReadAzureBlobFun = utils.ReadAzureBlob + +// HandleTrexLogsGet godoc +// @Summary Retrieve T.Rex logs +// @Description Get T.Rex logs for specific day +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "Car vin" +// @Param date query string true "Logs date" +// @Param offset query int false "Offset in bytes" +// @Param count query int false "Count in bytes" +// @Param direction query string false "Cursor direction 'up' or 'down', default is 'down'" +// @Success 200 {object} common.JSONBlobReadResult{data=[]common.LogTrexLog} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 404 {object} common.JSONError "Status not found" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle/{vin}/trex-logs [get] +func HandleTrexLogsGet(w http.ResponseWriter, r *http.Request) { + vin, date, direction, offset, count, ok := parse(w, r) + if !ok { + return + } + + buf, blobSize, err := ReadAzureBlobFun(utils.AzureTRexLogsContainerName, vin, date.Year(), int(date.Month()), date.Day(), offset, count, direction) + if err != nil { + u.RespError(w, http.StatusNotFound, err.Error()) + return + } + res, bytesRead, resOffset := processBlob(buf, direction, offset) + if len(res) == 0 { + resOffset = offset + bytesRead = 0 + } + u.RespJSON(w, http.StatusOK, common.JSONBlobReadResult{ + Data: res, + RealOffset: resOffset, + BytesRead: int64(bytesRead), + BlobSize: blobSize, + }) +} + +func processBlob(buf []byte, direction utils.Direction, offset int64) (res []json.RawMessage, bytesRead int64, resOffset int64) { + bytesRead = int64(len(buf)) + splitted := bytes.Split(buf, []byte("\n")) + res = make([]json.RawMessage, 0, len(splitted)) + var parsedLog common.LogTrexLog + resOffset = offset + for i, row := range splitted { + if len(row) == 0 { + continue + } + //do not add first/last row, if it contains incomplete JSON + if i == 0 && json.Unmarshal(row, &parsedLog) != nil { + if direction == utils.Down { + resOffset += int64(len(row)) + } + bytesRead -= int64(len(row)) + continue + } + if i == len(splitted)-1 && json.Unmarshal(row, &parsedLog) != nil { + if direction == utils.Up { + resOffset += int64(len(row)) + } + bytesRead -= int64(len(row)) + continue + } + res = append(res, json.RawMessage(row)) + } + return res, bytesRead, resOffset +} + +func parse(w http.ResponseWriter, r *http.Request) (vin string, date time.Time, direction utils.Direction, offset, count int64, ok bool) { + values := r.URL.Query() + params := httprouter.ParamsFromContext(r.Context()) + vin = params.ByName("vin") + dateStr := values.Get("date") + offset, err := strconv.ParseInt(values.Get("offset"), 10, 64) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + count, err = strconv.ParseInt(values.Get("count"), 10, 64) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + date, err = time.Parse("2006-01-02", dateStr) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + direction, ok = utils.ParseCursorDirection(values.Get("direction")) + if !ok { + direction = utils.Down + ok = true + } + return vin, date, direction, offset, count, ok +} diff --git a/services/ota_update_go/handlers/trex_logs_get_test.go b/services/ota_update_go/handlers/trex_logs_get_test.go new file mode 100644 index 0000000..ec1d1a9 --- /dev/null +++ b/services/ota_update_go/handlers/trex_logs_get_test.go @@ -0,0 +1,137 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/utils" + + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +var testInput = firstRecord + "\n" + secondRecord + "\n" + thirdRecord + "\n" + fourthRecord + "\n" + fifthRecord + "\n" + sixthRecord + +var firstRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:49.323538","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122482177"}` +var secondRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:49.225069","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122502977"}` +var thirdRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:51.932326","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122710681"}` +var fourthRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:51.842027","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122736981"}` +var fifthRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:51.729643","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122761582"}` +var sixthRecord = `{"level":"debug","timestamp":"2023-Jan-25 22:50:52.343492","line_number":0,"filename":"dummy","msg":"vom::on_frame_() frame 0x52","received_timestamp":"2023-Jan-25 22:51:10.122779382"}` + +var ( + testVin = "1G1FP87S1GN000414" + testYear = 2023 + testMonth = 1 + testDay = 17 + testOffset = 0 + testCount = 0 + testReadDirection = utils.Up +) + +func formatRequest(request string) string { + return fmt.Sprintf(request, testVin, testYear, testMonth, testDay, testOffset, testCount, + func() string { + if testReadDirection == utils.Up { + return "UP" + } else { + return "DOWN" + } + }()) +} + +func addTest(t *testing.T, name, expectectedResponse string) { + handlers.ReadAzureBlobFun = createTestReader(t, testVin, testYear, testMonth, testDay, testOffset, testCount, testReadDirection) + test := th.BasicHttpTest{ + Name: name, + Request: th.MakeTestRequest(http.MethodGet, + formatRequest("http://example.com/vehicle/%s/trex-logs?&date=%d-%02d-%d&offset=%d&count=%d&direction=%s"), nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectectedResponse, + } + th.RunParamHttpTest(t, test, handlers.HandleTrexLogsGet, "/vehicle/:vin/trex-logs") +} + +func createTestReader(t *testing.T, testVin string, testYear, testMonth, testDay, testOffset, testCount int, testReadDirection utils.Direction) func(string, string, int, int, int, int64, int64, utils.Direction) ([]byte, int64, error) { + return func(path, vin string, year, month, day int, offset, count int64, readDirection utils.Direction) ([]byte, int64, error) { + var check = func(format string, actual, expected interface{}) { + if actual != expected { + t.Fatalf(format, expected, actual) + } + } + check("expected vin: %s, got: %s", vin, testVin) + check("expected year: %d, got: %d", year, testYear) + check("expected month: %d, got: %d", month, testMonth) + check("expected day: %d, got: %d", day, testDay) + check("expected offset: %d, got: %d", offset, int64(testOffset)) + check("expected count: %d, got: %d", count, int64(testCount)) + check("expected direction: %d, got: %d", readDirection, testReadDirection) + //count from the end + + offset = int64(len(testInput)) - offset + + begin := offset + if readDirection == utils.Up { + begin -= count + } + if begin <= 0 { + begin = 0 + count = offset + } + min := func(a, b int64) int64 { + if a < b { + return a + } + return b + } + max := func(a, b int64) int64 { + if a > b { + return a + } + return b + } + + begin = max(0, begin) + + return []byte(testInput)[begin : begin+min(count, int64(len(testInput))-begin)], int64(len(testInput)), nil + } +} + +func TestTrexLogsGet(t *testing.T) { + testCount = 300 + testLen := int64(len(testInput)) + //expect that we read exactly 1 record + addTest(t, "Read 1 record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, sixthRecord, testOffset, len(sixthRecord)+1, testLen)) + testCount = 100 + //read nothing (100 < len(sixthRecord)) + addTest(t, "Read 0 record", + fmt.Sprintf(`{"data":[],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, testOffset, 0, testLen)) + //read all + testCount = len(testInput) + addTest(t, "Read all records", + fmt.Sprintf(`{"data":[%s,%s,%s,%s,%s,%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, + firstRecord, secondRecord, thirdRecord, fourthRecord, fifthRecord, sixthRecord, testOffset, testCount, testLen)) + //something a little big bigger than 1 record + testCount = len(sixthRecord) + 25 + testOffset = 0 + //read 1 by 1 from the bottom + addTest(t, "Read sixth record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, sixthRecord, testOffset, len(sixthRecord)+1, testLen)) + testOffset += len(sixthRecord) + 1 + addTest(t, "Read fifth record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, fifthRecord, testOffset, len(fifthRecord)+1, testLen)) + testOffset += len(fifthRecord) + 1 + addTest(t, "Read fourth record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, fourthRecord, testOffset, len(fourthRecord)+1, testLen)) + testOffset += len(fourthRecord) + 1 + addTest(t, "Read third record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, thirdRecord, testOffset, len(thirdRecord)+1, testLen)) + testOffset += len(thirdRecord) + 1 + addTest(t, "Read second record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, secondRecord, testOffset, len(secondRecord)+1, testLen)) + testOffset += len(secondRecord) + 1 + addTest(t, "Read first record", //+1 is for \n + fmt.Sprintf(`{"data":[%s],"RealOffset":%d,"bytesRead":%d,"blobSize":%d}`, firstRecord, testOffset, len(firstRecord), testLen)) +} diff --git a/services/ota_update_go/handlers/trex_logs_link_get.go b/services/ota_update_go/handlers/trex_logs_link_get.go new file mode 100644 index 0000000..f156546 --- /dev/null +++ b/services/ota_update_go/handlers/trex_logs_link_get.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "otaupdate/utils" + "time" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/remotefileupload" + "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// Need this so the swagger can use m +var _ = common.APIToken{} + +// HandleTrexLogsLinkGet godoc +// @Summary Retrieve T.Rex logs +// @Description Get T.Rex logs for specific day +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "Car vin" +// @Param year query string true "Year of file" +// @Param month query string true "Month of file" +// @Param day query string true "Day of file" +// @Success 200 {object} LinkResponse +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 404 {object} common.JSONError "Status not found" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle/{vin}/trex-logs-link [get] +func HandleTrexLogsLinkGet(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + vin := params.ByName("vin") + query := r.URL.Query() + year := query.Get("year") + month := query.Get("month") + day := query.Get("day") + if vin == "" || year == "" || month == "" || day == "" { + http.Error(w, "incomplete parameters", http.StatusBadRequest) + return + } + // We are trusting the date is good, otherwise the user will get a download to nothing + str, err := getAzureTRexBlobLink(vin, year, month, day) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + encoder := json.NewEncoder(w) + encoder.SetEscapeHTML(false) // Otherwise & becomes /00 encoded + err = encoder.Encode(LinkResponse{Link: str}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } +} + +type LinkResponse struct { + Link string +} + +func getAzureTRexBlobLink(vin, year, month, day string) (link string, err error) { + link, err = remotefileupload.AzureFilePathLink(utils.AzureAccount, utils.AzureTRexLogsContainerName, vin, year, month, day, "raw.log") + if err != nil { + logger.Err(err).Msg("") + } + sasToken, err := getSASAccessTokenOnceADay() + if err != nil { + logger.Err(err).Msg("") + } + return link + "?" + sasToken, err +} + +// We will fetch the access token once a day to prevent access to the files if someone saves a link somewhere accidentally +var cachedTokenTime time.Time // The Time we last got the token +var cachedToken string // Once a day get a new access token + +func getSASAccessTokenOnceADay() (token string, err error) { + if time.Since(cachedTokenTime) > time.Hour*24 { + cachedTokenTime = time.Now() + cachedToken, err = generateSASToken() + } + return cachedToken, err +} + +func generateSASToken() (token string, err error) { + cred, err := azblob.NewSharedKeyCredential(utils.AzureAccount, utils.AzureAccountKey) + if err != nil { + return "", errors.WithStack(err) + } + + sasQueryParams, err := azblob.BlobSASSignatureValues{ + Protocol: azblob.SASProtocolHTTPS, + StartTime: time.Now().UTC(), + ExpiryTime: time.Now().UTC().Add(48 * time.Hour), + Permissions: azblob.BlobSASPermissions{Read: true, List: true}.String(), + IPRange: azblob.IPRange{}, + ContainerName: utils.AzureTRexLogsContainerName, + }.NewSASQueryParameters(cred) + + if err != nil { + logger.Error().Err(err).Msg("Failed to sas.BlobSignatureValues") + return + } + + token = sasQueryParams.Encode() + return +} diff --git a/services/ota_update_go/handlers/update_manifest_migrate.go b/services/ota_update_go/handlers/update_manifest_migrate.go new file mode 100644 index 0000000..9a7d46c --- /dev/null +++ b/services/ota_update_go/handlers/update_manifest_migrate.go @@ -0,0 +1,264 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "otaupdate/services" + "sort" + "strconv" + "strings" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "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/utils/envtool" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +type ManifestMigrateVersion struct { + Version int +} + +var ERR_MANIFEST_MIGRATE_BAD_VERSION = errors.New("received version not recognized") + +// TargetURLS is multiple as stage pushes to Prod and Euro Prod +var apiToken string = envtool.GetEnv("MANIFEST_MIGRATE_TOKEN", "MISSING API TOKEN") +var targetURLS []string = strings.Split(envtool.GetEnv("MANIFEST_MIGRATE_URLS", "MISSING_TARGET_URL"), ",") + +// So we are going to take the information from our lower env and push it into a higher one. +// Need to decide how we want to get the information from the lower level to this higher level +// HandleUpdateManifestMigrate godoc +// @Summary Push update manifest from this environment to a higher one +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param manifest_id path string true "Manifest 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 /manifestmigrate/{manifest_id} [post] +func HandleUpdateManifestMigrate(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + manifestIdString := params.ByName("manifest_id") + + // Gather the right information + manifestId, err := strconv.Atoi(manifestIdString) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + var manifest common.UpdateManifest + manifest.ID = int64(manifestId) + + err = validator.ValidateIDField(manifest.ID) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + um := services.GetDB().GetUpdateManifests() + err = um.Load(&manifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + manifest.SUMS = "" + manifest.Env = "" + + manifest.MigratePrepareHardwareVersion() + + fileKeys, err := getFileKeys(manifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + sendStruct := ManifestMigrateBody{} + sendStruct.MigratedManifest = manifest + sendStruct.FileKeys = fileKeys + + // Send message to the other API's + sendBody, err := PrepareMigrateBodyForSending(&sendStruct) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + for index, targetURL := range targetURLS { + turl, err := url.JoinPath(targetURL, "manifestmigrate") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + req, err := http.NewRequest("POST", turl, bytes.NewBuffer(sendBody)) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Api-Key", apiToken) + resp, err := http.DefaultClient.Do(req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + deleteBecauseFailedMigrate(targetURLS[:index], int(manifest.ID)) + return + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + // Forgot to check the status code + if resp.StatusCode >= 300 { + logger.Error().Msgf("Failed to send manifest migrate: %s \n Status: %s\n Resp Body: %s\n", string(sendBody), resp.Status, string(b)) + var response common.JSONError + err := json.Unmarshal(b, &response) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + utils.RespJSON(w, http.StatusBadRequest, response) + return + } + } + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "Migration Sent", + }) +} + +// If we fail to upload a manifest to one of the urls, delete from the other environments we are uploading to +func deleteBecauseFailedMigrate(urls []string, manifestID int) { + for _, targetURL := range urls { + turl, err := url.JoinPath(targetURL, "manifestraw") + if err != nil { + logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL) + continue + } + turl += fmt.Sprintf("?id=%d", manifestID) + req, err := http.NewRequest("DELETE", turl, nil) + if err != nil { + logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL) + continue + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Api-Key", apiToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status) + continue + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + if err != nil { + logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status) + continue + } + } + } +} + +func getFileKeys(manifest common.UpdateManifest) ([]common.FileKeyResponse, error) { + conn := services.RedisClientPool().GetFromPool() + dbFK := services.GetDB().GetFileKeys() + defer conn.Close() + fileIDs := make([]string, 0) + for _, ecu := range manifest.ECUs { + for _, file := range ecu.Files { + fileIDs = append(fileIDs, file.FileID) + } + } + return cache.RetrieveFileEncryptionParams(conn, dbFK, fileIDs) +} + +// In case we need a completely different struct, we will just return the bytes +func PrepareMigrateBodyForSending(manifestMigrate *ManifestMigrateBody) (jsonBody []byte, err error) { + v, err := getVersionToSend() + if err != nil { + return + } + err = transformMigrateBody(manifestMigrate, v) + if err != nil { + return + } + return json.Marshal(manifestMigrate) +} + +// Get the version that the env we are pushing to is going to run, then we can transform is needed or not send at all +// If we do not get a version, it defaults to 0 +func getVersionToSend() (version int, err error) { + version = -1 + if len(targetURLS) == 0 { + return version, errors.New("missing target url") + } + targetURL := targetURLS[0] + // The eu prod and normal prod should always be the same version, so just going to act like they are + turl, err := url.JoinPath(targetURL, "manifestmigrate-version") + if err != nil { + return + } + req, err := http.NewRequest("GET", turl, nil) + if err != nil { + return + } + req.Header.Add("Api-Key", apiToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + // If they don't have this route, we can assume they are on version 0 + if resp.StatusCode >= 300 { + logger.Warn().Msgf("manifest migrate version returned status code %s", resp.Status) + version = 0 + return + } + var mmv ManifestMigrateVersion + err = json.NewDecoder(resp.Body).Decode(&mmv) + if err != nil { + return + } + version = mmv.Version + + return +} + +const MIGRATION_VERSION = 1 + +// If we do not get a version, it defaults to 0 +// If we cannot send the manifest due to some major change, then return an error +func transformMigrateBody(manifestMigrate *ManifestMigrateBody, version int) (err error) { + // We have to go from our version down to their version + // EX we are on version 5, they are on version 3 + // So do the conversion of 5 -> 4, then 4 -> 3 + + // For each version that is added on, add to this switch statement + // This switch statement is more of a way to organize the flow, can probably be improved + switch MIGRATION_VERSION { + case 1: // The conversion needed to go from VERSION 1 to VERSION 0 + if version == 1 { // If we are at the desired version, then we can break + break + } + swapECUInstallPriority(manifestMigrate) + fallthrough + case 0: + // THis is just an example row to show the fallthrough from case of 1 -> 0 + if version == 0 { + break + } + default: + err = ERR_MANIFEST_MIGRATE_BAD_VERSION + } + + return +} + +func swapECUInstallPriority(manifestMigrate *ManifestMigrateBody) { + ecuList := manifestMigrate.MigratedManifest.ECUs + // Order the array so we can begin to swap easily + sort.Slice(ecuList, func(i, j int) bool { return ecuList[i].InstallPriority < ecuList[j].InstallPriority }) + rightIndex := len(ecuList) - 1 + for leftIndex := 0; leftIndex < rightIndex; leftIndex++ { + ecuList[leftIndex].InstallPriority, ecuList[rightIndex].InstallPriority = ecuList[rightIndex].InstallPriority, ecuList[leftIndex].InstallPriority + rightIndex-- + } +} diff --git a/services/ota_update_go/handlers/update_manifest_migrate_test.go b/services/ota_update_go/handlers/update_manifest_migrate_test.go new file mode 100644 index 0000000..bfee8d4 --- /dev/null +++ b/services/ota_update_go/handlers/update_manifest_migrate_test.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "testing" + + "github.com/fiskerinc/cloud-services/pkg/common" +) + +func TestECUPrioritySwap(t *testing.T){ + b := ManifestMigrateBody{ + MigratedManifest: common.UpdateManifest{ + ECUs: []*common.UpdateManifestECU{{ + ID: 3, + InstallPriority: 1, + }, + { + ID: 2, + InstallPriority: 2, + }, + { + ID: 1, + InstallPriority: 3, + }}, + }, + } + swapECUInstallPriority(&b) + + for _, ecu := range b.MigratedManifest.ECUs{ + if ecu.ID != int64(ecu.InstallPriority){ + t.Fail() + } + } + + b = ManifestMigrateBody{ + MigratedManifest: common.UpdateManifest{ + ECUs: []*common.UpdateManifestECU{{ + ID: 4, + InstallPriority: 1, + }, + { + ID: 3, + InstallPriority: 2, + }, + { + ID: 2, + InstallPriority: 3, + }, + { + ID: 1, + InstallPriority: 4, + }, + }, + }, + } + swapECUInstallPriority(&b) + for _, ecu := range b.MigratedManifest.ECUs{ + if ecu.ID != int64(ecu.InstallPriority){ + t.Fail() + } + } +} \ No newline at end of file diff --git a/services/ota_update_go/handlers/updatemanifest_add.go b/services/ota_update_go/handlers/updatemanifest_add.go new file mode 100644 index 0000000..fd8a3ed --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_add.go @@ -0,0 +1,59 @@ +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/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestAdd godoc +// @Summary Add update manifest +// @Description Upload update manifest +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param manifest body common.CreateUpdateManifest true "Manifest data" +// @Success 200 {object} common.CreateUpdateManifest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest [post] +func HandleUpdateManifestAdd(w http.ResponseWriter, r *http.Request) { + m := common.CreateUpdateManifest{} + err := httphandlers.ParseRequest(r, &m) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + um := services.GetDB().GetUpdateManifests() + var manifest = common.UpdateManifest{ + Name: m.Name, + Version: m.Version, + Description: m.Description, + ReleaseNotes: m.ReleaseNotes, + RollbackEnabled: m.RollbackEnabled, + Type: m.Type, + ManifestType: m.ManifestType, + Active: m.Active, + Country: m.Country, + PowerTrain: m.PowerTrain, + Restraint: m.Restraint, + Model: m.Model, + Trim: m.Trim, + Year: m.Year, + BodyType: m.BodyType, + UpdateDuration: m.UpdateDuration, + MaxAttempts: m.MaxAttempts, + } + _, err = um.Insert(&manifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + utils.RespJSON(w, http.StatusOK, manifest) +} diff --git a/services/ota_update_go/handlers/updatemanifest_add_test.go b/services/ota_update_go/handlers/updatemanifest_add_test.go new file mode 100644 index 0000000..f893efe --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_add_test.go @@ -0,0 +1,50 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +func TestUpdateManifestAdd(t *testing.T) { + mock := mo.MockUpdateManifests{} + services.GetDB().SetUpdateManifests(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/manifest", common.CreateUpdateManifest{ + Name: "package_name", + Version: "100", + Description: "description", + ReleaseNotes: "http://releasenotes.com", + Type: "standard", + RollbackEnabled: true, + Active: elptr.ElPtr(true), + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":1,"name":"package_name","version":"100","description":"description","release_notes":"http://releasenotes.com","rollback":true,"type":"standard","active":true,"country":"US","powertrain":"MD23","restraint":"None","model":"Ocean","trim":"Sport","year":2022,"body_type":"truck"}`, + }, + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/manifest", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleUpdateManifestAdd, &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_archive_update.go b/services/ota_update_go/handlers/updatemanifest_archive_update.go new file mode 100644 index 0000000..329ed4b --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_archive_update.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "fmt" + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/utils" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestsArchive godoc +// @Summary Archive update manifests +// @Description Archive update manifests +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param manifest body common.UpdateManifestArchiveRequest true "Manifest data" +// @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 /manifests/archive [put] +func HandleUpdateManifestsArchive(w http.ResponseWriter, r *http.Request) { + var payload common.UpdateManifestArchiveRequest + err := httphandlers.ParseRequest(r, &payload) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB(). + GetUpdateManifests(). + Archive(payload.IDs, payload.Active) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + messageState := "Archived" + if payload.Active { + messageState = "Activated" + } + message := fmt.Sprintf("%s %d update manifests", messageState, len(payload.IDs)) + + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: message, + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_archive_update_test.go b/services/ota_update_go/handlers/updatemanifest_archive_update_test.go new file mode 100644 index 0000000..6aa08fb --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_archive_update_test.go @@ -0,0 +1,57 @@ +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 TestUpdateManifestArchive(t *testing.T) { + mock := mo.MockUpdateManifests{} + services.GetDB().SetUpdateManifests(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Valid data - remove active", + Request: th.MakeTestRequest( + http.MethodPut, + "http://example.com/manifests/archive", + common.UpdateManifestArchiveRequest{ + IDs: []int64{1, 2, 3}, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Archived 3 update manifests"}`, + }, + { + Name: "Valid data - make active", + Request: th.MakeTestRequest( + http.MethodPut, + "http://example.com/manifests/archive", + common.UpdateManifestArchiveRequest{ + IDs: []int64{1, 2, 3}, + Active: true, + }, + ), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Activated 3 update manifests"}`, + }, + { + Name: "No data", + Request: th.MakeTestRequest( + http.MethodPost, + "http://example.com/manifests/archive", + nil, + ), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"IDs required","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleUpdateManifestsArchive, &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_delete.go b/services/ota_update_go/handlers/updatemanifest_delete.go new file mode 100644 index 0000000..55d0281 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_delete.go @@ -0,0 +1,45 @@ +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/validator" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestDelete godoc +// @Summary Delete update manifest +// @Description Delete update manifest data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query int false "Update manifest 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 /manifest [delete] +func HandleUpdateManifestDelete(w http.ResponseWriter, r *http.Request) { + qs := r.URL.Query() + um := common.UpdateManifest{ + ID: urlhelper.GetQueryInt64(qs, "id"), + } + err := validator.ValidateNonRequired(um) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB().GetUpdateManifests().Delete(&um) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "Deleted", + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_delete_test.go b/services/ota_update_go/handlers/updatemanifest_delete_test.go new file mode 100644 index 0000000..bc775b3 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_delete_test.go @@ -0,0 +1,39 @@ +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 TestUpdateManifestDelete(t *testing.T) { + services.GetDB().SetUpdateManifests(&mocks.MockUpdateManifests{}) + + tests := []th.BasicHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifest", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id required","error":"Bad Request"}`, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifest?id=0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id required","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifest?id=1", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleUpdateManifestDelete) +} diff --git a/services/ota_update_go/handlers/updatemanifest_get.go b/services/ota_update_go/handlers/updatemanifest_get.go new file mode 100644 index 0000000..97754e4 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_get.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/authproviders" + "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" +) + +// HandleUpdateManifestGet godoc +// @Summary Get update manifest +// @Description Get update manifest by id +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query string false "Update manifest id" +// @Success 200 {object} common.UpdateManifest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest [get] +func HandleUpdateManifestGet(w http.ResponseWriter, r *http.Request) { + manifest := common.UpdateManifest{ + ID: urlhelper.GetQueryInt64(r.URL.Query(), "id"), + } + + if utils.AUTHGetProviderFromRequest(r) == authproviders.Magna { + manifest.ManifestType = common.MagnaManifestUpdateType + } + + err := validator.ValidateIDField(manifest.ID) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + um := services.GetDB().GetUpdateManifests() + err = um.Load(&manifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + manifest.SortECUs() + + for i := range manifest.ECUs { + manifest.ECUs[i].Scrub(0, false) + } + + utils.RespJSON(w, http.StatusOK, &manifest) +} diff --git a/services/ota_update_go/handlers/updatemanifest_migrate_receive.go b/services/ota_update_go/handlers/updatemanifest_migrate_receive.go new file mode 100644 index 0000000..e7a344a --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_migrate_receive.go @@ -0,0 +1,110 @@ +package handlers + +import ( + "encoding/json" + "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" + "github.com/pkg/errors" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestRawPost godoc +// @Summary Receive the raw information and load it into the database +// @Description Get update manifest by id, without modifying anything from the database +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body ManifestMigrateBody true "The manifest to migrate" +// @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 /manifestmigrate [post] +func HandleUpdateManifestMigrateReceive(w http.ResponseWriter, r *http.Request) { + var err error + var migratedManifest common.UpdateManifest + var migrateBody ManifestMigrateBody + + if r.Body == nil { + err = errors.New("HandleUpdateManifestMigrateReceive received a null body") + loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) + return + } + + err = json.NewDecoder(r.Body).Decode(&migrateBody) + err = errors.WithStack(err) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + cleansIDS(&migrateBody) + migratedManifest = migrateBody.MigratedManifest + um := services.GetDB().GetUpdateManifests() + _, err = um.Insert(&migratedManifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + logger.Err(err).Interface("migrated_manifest", migrateBody).Msg("Failed to insert migrated manifest") + return + } + //vscode://file/Users/alexanderandrews/Documents/project-ai2/cloud/modules_go/db/queries/filekeys.go:52 + // Insert the unencrypted file keys, as the insert command encrypts them for us + dbFK := services.GetDB().GetFileKeys() + for _, fkr := range migrateBody.FileKeys { + fileKey := common.FileKey{} + err = fileKey.Apply(&fkr) + if err != nil { + newErr := rollbackManifestMigrate(&migratedManifest) + if newErr != nil { + err = errors.Wrap(err, newErr.Error()) + } + loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) + return + } + _, err = dbFK.Insert(fileKey) + + if err != nil { + newErr := rollbackManifestMigrate(&migratedManifest) + if newErr != nil { + err = errors.Wrap(err, newErr.Error()) + } + loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) + return + } + } + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "Migration Received", + }) +} + +// THIS wil need changing +// If the manifest keys fail to upload, we should delete the manifest +func rollbackManifestMigrate(manifest *common.UpdateManifest) (err error) { + um := services.GetDB().GetUpdateManifests() + _, err = um.Delete(manifest) + return +} + +type ManifestMigrateBody struct { + MigratedManifest common.UpdateManifest + FileKeys []common.FileKeyResponse +} + +func cleansIDS(migrateBody *ManifestMigrateBody) { + hold := migrateBody.MigratedManifest + hold.ID = 0 + hold.CarUpdateID = 0 + for x, ecu := range hold.ECUs { + ecu.ID = 0 + ecu.UpdateManifestID = 0 + for y, file := range ecu.Files { + file.UpdateManifestECUID = 0 + ecu.Files[y] = file + } + hold.ECUs[x] = ecu + } + migrateBody.MigratedManifest = hold +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_add.go b/services/ota_update_go/handlers/updatemanifest_sums_add.go new file mode 100644 index 0000000..33a9854 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_add.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "errors" + "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/validator" +) + +// HandleUpdateManifestSUMSAdd godoc +// @Summary Create new update manifest versions +// @Description Create and save new update manifest versions, and return them +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param SUMSVersion body common.SUMSVersionCreate true "SwVersionRxSwin data" +// @Success 200 {object} []common.SUMSVersion +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest/sums [post] +func HandleUpdateManifestSUMSAdd(w http.ResponseWriter, r *http.Request) { + svc := common.SUMSVersionCreate{} + err := httphandlers.ParseRequest(r, &svc) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + if len(svc.SUMSVersions) < 1 { + loggerdataresp.BadDataErrorResp(w, errors.New("called /manifest/sums/ POST with no sums versions to add"), http.StatusBadRequest) + } + + for _, sumsVersion := range svc.SUMSVersions { + err = validator.ValidateStruct(sumsVersion) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + } + + db := services.GetDB().GetUpdateManifestVersions() + for _, sumsVersion := range svc.SUMSVersions { + _, err = db.Insert(&sumsVersion) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + 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.UpdateManifestSUMSAdd(svc) + 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, svc.SUMSVersions) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_add_test.go b/services/ota_update_go/handlers/updatemanifest_sums_add_test.go new file mode 100644 index 0000000..b4bb93f --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_add_test.go @@ -0,0 +1,36 @@ +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 TestHandleUpdateManifestSUMSAdd(t *testing.T) { + mock := mo.MockUpdateManifestVersions{} + services.GetDB().SetUpdateManifestVersions(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPost, "/manifests/sums", common.SUMSVersionCreate{ + SUMSVersions: []common.SUMSVersion{ + { + Version: "5236.12.50.67", + }, + { + Version: "5236.12.50.68", + }, + }}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `[{"version":"5236.12.50.67","os_version":""},{"version":"5236.12.50.68","os_version":""}]`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleUpdateManifestSUMSAdd, &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_delete.go b/services/ota_update_go/handlers/updatemanifest_sums_delete.go new file mode 100644 index 0000000..43dadf8 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_delete.go @@ -0,0 +1,70 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" +) + +// HandleUpdateManifestSUMSDelete godoc +// @Summary Delete a SUMS version number +// @Description Delete a SUMS version number +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param version path string true "Update manifest version 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 /manifest/sums/{version} [delete] +func HandleUpdateManifestSUMSDelete(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + version := params.ByName("version") + + err := validator.ValidateStruct(common.SUMSVersion{Version: version}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + result, err := services.GetDB().GetUpdateManifestVersions().Delete(&common.SUMSVersion{Version: version}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + if result != nil && result.RowsAffected() == 0 && loggerdataresp.BadDataErrorResp(w, errors.New("cannot delete. SUMS version does not exist"), http.StatusBadRequest) { + 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.UpdateManifestSUMSDelete(version) + 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", + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_delete_test.go b/services/ota_update_go/handlers/updatemanifest_sums_delete_test.go new file mode 100644 index 0000000..cf716ce --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_delete_test.go @@ -0,0 +1,26 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleUpdateManifestSUMSDelete(t *testing.T) { + services.GetDB().SetUpdateManifestVersions(&mocks.MockUpdateManifestVersions{}) + + tests := []th.BasicHttpTest{ + { + Name: "Good params", + Request: th.MakeTestRequest(http.MethodDelete, "/manifests/sums/5236.12.5.67", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleUpdateManifestSUMSDelete, "/manifests/sums/:version") +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_get.go b/services/ota_update_go/handlers/updatemanifest_sums_get.go new file mode 100644 index 0000000..705d6f2 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_get.go @@ -0,0 +1,52 @@ +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/loggerdataresp" +) + +// HandleUpdateManifestSUMSGet godoc +// @Summary Get all update manifest versions +// @Description Get all update manifest versions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param limit query int false "Max number of records" +// @Param offset query int false "Records offset" +// @Success 200 {object} []common.SUMSVersion +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest/sums [get] +func HandleUpdateManifestSUMSGet(w http.ResponseWriter, r *http.Request) { + options, err := orm.ParsePageQuery(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + if options.Order == "" { + options.Order = "version DESC" + } + + sums := services.GetDB().GetUpdateManifestVersions() + + allUpdateManifestVersions, err := sums.SelectAll(options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + total, err := sums.SelectAllCount() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: allUpdateManifestVersions, + Total: total, + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_get_test.go b/services/ota_update_go/handlers/updatemanifest_sums_get_test.go new file mode 100644 index 0000000..b8eae0a --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_get_test.go @@ -0,0 +1,35 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + m "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 TestHandleUpdateManifestSUMSGet(t *testing.T) { + mock := mocks.MockUpdateManifestVersions{} + services.GetDB().SetUpdateManifestVersions(&mock) + umvs := []m.SUMSVersion{{Version: "testv1", OSVersion: "1.2.3"}, {Version: "testv2", OSVersion: "1.2.3"}, {Version: "testv3", OSVersion: "1.2.3"}} + + tests := []mocks.DBHttpTest{ + { + Name: "Fetch all update manifest versions rows", + Request: th.MakeTestRequest(http.MethodGet, "/manifests/versions", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"version":"testv1","os_version":"1.2.3"},{"version":"testv2","os_version":"1.2.3"},{"version":"testv3","os_version":"1.2.3"}],"total":3}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + mock.SelectResponse = umvs + }, + }, + }, + } + + mocks.RunDBTests(t, tests, handlers.HandleUpdateManifestSUMSGet, &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add.go new file mode 100644 index 0000000..9a4e6ae --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add.go @@ -0,0 +1,73 @@ +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/validator" + "github.com/julienschmidt/httprouter" +) + +// HandleUpdateManifestSUMSRxSwinsAdd godoc +// @Summary Add one or more RX Software ID Numbers (RxSWINs) for an update manifest version +// @Description Add one or more RX Software ID Numbers (RxSWINs) for an update manifest version and return them +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param version path string true "Update manifest version name" +// @Param swVersionRxSwins body common.SwVersionRxSwinCreate true "SwVersionRxSwin data" +// @Success 200 {object} []common.SwVersionRxSwin +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest/sums/{version}/rxswins [post] +func HandleUpdateManifestSUMSRxSwinsAdd(w http.ResponseWriter, r *http.Request) { + version := httprouter.ParamsFromContext(r.Context()).ByName("version") + + err := validator.ValidateStruct(common.SUMSVersion{Version: version}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + svrsc := common.SwVersionRxSwinCreate{} + err = httphandlers.ParseRequest(r, &svrsc) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + db := services.GetDB().GetSwVerRxSwin() + for _, swVersionRxSwin := range svrsc.SwVersionRxSwins { + swVersionRxSwin.Version = version + _, err = db.Insert(&swVersionRxSwin) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + 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.UpdateManifestSUMSRxSwinsAdd(version, svrsc) + 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, svrsc.SwVersionRxSwins) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add_test.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add_test.go new file mode 100644 index 0000000..cd789f3 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_add_test.go @@ -0,0 +1,38 @@ +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 TestHandleUpdateManifestSUMVersionRxSwinsAdd(t *testing.T) { + mock := mo.MockSwVersionRxSwin{} + services.GetDB().SetSwVersionRxSwin(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPost, "/manifests/sum/5236%2E12%2E50%2E67/rxswins", common.SwVersionRxSwinCreate{ + SwVersionRxSwins: []common.SwVersionRxSwin{ + { + Version: "5236.12.50.67", + RxSwin: "testrxswin", + }, + { + Version: "5236.12.50.67", + RxSwin: "testrxswin2", + }, + }}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `[{"version":"5236.12.50.67","rxswin":"testrxswin"},{"version":"5236.12.50.67","rxswin":"testrxswin2"}]`, + }, + } + + mo.RunParamHttpTests(t, tests, handlers.HandleUpdateManifestSUMSRxSwinsAdd, "/manifests/sum/:version/rxswins", &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete.go new file mode 100644 index 0000000..3f60c6e --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete.go @@ -0,0 +1,72 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" +) + +// HandleUpdateManifestSUMSRxSwinsDelete godoc +// @Summary Delete a RX Software ID Numbers (RxSWINs) for given sum version and rxswin +// @Description Delete a RX Software ID Number (RxSWIN) for a given manifest update manifest sum version and rxswin number +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param version path string true "Update manifest version name" +// @Param rxswin path string true "Update manifest rxswin 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 /manifest/sums/{version}/rxswins/{rxswin} [delete] +func HandleUpdateManifestSUMSRxSwinsDelete(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + + version := params.ByName("version") + rxswin := params.ByName("rxswin") + + err := validator.ValidateStruct(common.SwVersionRxSwin{Version: version, RxSwin: rxswin}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + result, err := services.GetDB().GetSwVerRxSwin().Delete(&common.SwVersionRxSwin{Version: version, RxSwin: rxswin}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + if result != nil && result.RowsAffected() == 0 && loggerdataresp.BadDataErrorResp(w, errors.New("cannot delete. SUMS version does not exist"), http.StatusBadRequest) { + 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.UpdateManifestSUMSRxSwinsDelete(version, rxswin) + 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", + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete_test.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete_test.go new file mode 100644 index 0000000..373beb7 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_delete_test.go @@ -0,0 +1,26 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleUpdateManifestSUMSVersionRxSwinsDelete(t *testing.T) { + services.GetDB().SetSwVersionRxSwin(&mocks.MockSwVersionRxSwin{}) + + tests := []th.BasicHttpTest{ + { + Name: "Good params", + Request: th.MakeTestRequest(http.MethodDelete, "/manifests/sums/5236.12.5.67/rxswins/testrxswin", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleUpdateManifestSUMSRxSwinsDelete, "/manifests/sums/:version/rxswins/:rxswin") +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get.go new file mode 100644 index 0000000..224a16e --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get.go @@ -0,0 +1,60 @@ +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/validator" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestSUMSRxSwinsGet godoc +// @Summary Get all RX Software ID Numbers (RxSWINs) for a given manifest update manifest version +// @Description Get and return all RX Software ID Numbers (RxSWINs) for a given manifest update manifest version +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param version path string true "Update manifest version name" +// @Success 200 {object} []common.SwVersionRxSwin +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest/sums/{version}/rxswins [get] +func HandleUpdateManifestSUMSRxSwinsGet(w http.ResponseWriter, r *http.Request) { + version := httprouter.ParamsFromContext(r.Context()).ByName("version") + + options, err := orm.ParsePageQuery(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + if options.Order == "" { + options.Order = "created_at DESC" + } + + err = validator.ValidateStruct(common.SUMSVersion{Version: version}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + s := services.GetDB().GetSwVerRxSwin() + + allUpdateManifestVersionRxSwins, err := s.SelectByVersion(version, options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + total, err := s.SelectCountByVersion(version) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: allUpdateManifestVersionRxSwins, + Total: total, + }) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get_test.go b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get_test.go new file mode 100644 index 0000000..726209d --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_rxswins_get_test.go @@ -0,0 +1,34 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + m "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 TestHandleUpdateManifestSUMVersionRxSwinsGet(t *testing.T) { + mock := mocks.MockSwVersionRxSwin{} + services.GetDB().SetSwVersionRxSwin(&mock) + svrs := []m.SwVersionRxSwin{{Version: "testv1", RxSwin: "trx1"}, {Version: "testv1", RxSwin: "trx2"}} + + tests := []mocks.DBHttpTest{ + { + Name: "Fetch all update manifest versions rows", + Request: th.MakeTestRequest(http.MethodGet, "/manifests/sum/5236%2E12%2E50%2E67/rxswins", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"version":"testv1","rxswin":"trx1"},{"version":"testv1","rxswin":"trx2"}],"total":2}`, + DBTestCase: mocks.DBTestCase{ + SetupMockResponse: func() { + mock.SelectResponse = svrs + }, + }, + }, + } + + mocks.RunParamHttpTests(t, tests, handlers.HandleUpdateManifestSUMSRxSwinsGet, "/manifests/sum/:version/rxswins", &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_update.go b/services/ota_update_go/handlers/updatemanifest_sums_update.go new file mode 100644 index 0000000..924bb29 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_update.go @@ -0,0 +1,64 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + "strconv" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestSUMSUpdate godoc +// @Summary Update update manifest sums version +// @Description Update update manifest data +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id path int true "Update manifest id" +// @Param manifest body UpdateManifestVersion true "Manifest version" +// @Success 200 {object} common.UpdateManifest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifests/{id}/sums [put] +func HandleUpdateManifestSUMSUpdate(w http.ResponseWriter, r *http.Request) { + var err error + params := httprouter.ParamsFromContext(r.Context()) + req := UpdateManifestVersion{} + req.ID, err = strconv.ParseInt(params.ByName("id"), 10, 64) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + err = httphandlers.ParseRequest(r, &req) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + manifest := common.UpdateManifest{ + ID: req.ID, + SUMS: req.Version, + } + um := services.GetDB().GetUpdateManifests() + result, err := um.AddSUMSVersion(&manifest) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + if result != nil && result.RowsAffected() == 0 && loggerdataresp.BadDataErrorResp(w, errors.New("cannot update. SUMS version already exists"), http.StatusBadRequest) { + return + } + + utils.RespJSON(w, http.StatusOK, req) +} + +type UpdateManifestVersion struct { + ID int64 `json:"-" validate:"required"` + Version string `json:"version" validate:"required,sums_version"` +} diff --git a/services/ota_update_go/handlers/updatemanifest_sums_update_test.go b/services/ota_update_go/handlers/updatemanifest_sums_update_test.go new file mode 100644 index 0000000..4b8326d --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_sums_update_test.go @@ -0,0 +1,39 @@ +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 TestHandleUpdateManifestSUMSUpdate(t *testing.T) { + mock := mo.MockUpdateManifests{} + services.GetDB().SetUpdateManifests(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Bad data", + Request: th.MakeTestRequest(http.MethodDelete, "/manifests/123/sums", handlers.UpdateManifestVersion{ + ID: 123, + Version: "x", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Version sums_version ","error":"Bad Request"}`, + }, + { + Name: "Good params", + Request: th.MakeTestRequest(http.MethodDelete, "/manifests/123/sums", handlers.UpdateManifestVersion{ + ID: 123, + Version: "5236.12.50.67", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"version":"5236.12.50.67"}`, + }, + } + + mo.RunParamHttpTests(t, tests, handlers.HandleUpdateManifestSUMSUpdate, "/manifests/:id/sums", &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifest_update.go b/services/ota_update_go/handlers/updatemanifest_update.go new file mode 100644 index 0000000..556ec67 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_update.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/utils" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils/urlhelper" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestUpdate godoc +// @Summary Update update manifest +// @Description Update update manifest data +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param id query int false "Update manifest id" +// @Param manifest body common.UpdateManifestUpdateRequest true "Manifest data" +// @Success 200 {object} common.UpdateManifest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifest [put] +func HandleUpdateManifestUpdate(w http.ResponseWriter, r *http.Request) { + id := urlhelper.GetQueryInt64(r.URL.Query(), "id") + err := validator.ValidateIDField(id) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + var mu common.UpdateManifestUpdateRequest + err = httphandlers.ParseRequest(r, &mu) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + um := common.UpdateManifest{ + ID: id, + Name: mu.Name, + Type: mu.Type, + Active: mu.Active, + Country: mu.Country, + PowerTrain: mu.PowerTrain, + Restraint: mu.Restraint, + Model: mu.Model, + Trim: mu.Trim, + Year: mu.Year, + BodyType: mu.BodyType, + Env: mu.Env, + RollbackEnabled: mu.Rollback, + SUMS: mu.SUMS, + UpdateDuration: mu.UpdateDuration, + MaxAttempts: mu.MaxAttempts, + ReleaseNotes: mu.ReleaseNotes, + } + _, err = services.GetDB().GetUpdateManifests().Update(&um) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, um) +} diff --git a/services/ota_update_go/handlers/updatemanifest_update_test.go b/services/ota_update_go/handlers/updatemanifest_update_test.go new file mode 100644 index 0000000..1013241 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifest_update_test.go @@ -0,0 +1,74 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +func TestUpdateManifestUpdate(t *testing.T) { + services.GetDB().SetUpdateManifests(&mocks.MockUpdateManifests{}) + + tests := []th.BasicHttpTest{ + { + Name: "No id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifest", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id required","error":"Bad Request"}`, + }, + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifest?id=0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"id required","error":"Bad Request"}`, + }, + { + Name: "Validation error, empty manifest", + Request: th.MakeTestRequest( + http.MethodGet, + "http://example.com/manifest?id=1", + common.UpdateManifestUpdateRequest{}), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Name required. Type required","error":"Bad Request"}`, + }, + { + Name: "Success", + Request: th.MakeTestRequest( + http.MethodGet, + "http://example.com/manifest?id=1", + common.UpdateManifestUpdateRequest{ + Name: "new_name", + Type: "standard", + Country: "US", + PowerTrain: "None", + Restraint: "MD12", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "trailer", + UpdateDuration: 30, + MaxAttempts: 5, + ReleaseNotes: "https://fiskerinc.com", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":1,"name":"new_name","release_notes":"https://fiskerinc.com","rollback":false,"type":"standard","country":"US","powertrain":"None","restraint":"MD12","model":"Ocean","trim":"Sport","year":2022,"body_type":"trailer","update_duration":30,"max_attempts":5}`, + }, + { + Name: "Active", + Request: th.MakeTestRequest( + http.MethodGet, + "http://example.com/manifest?id=1", + common.UpdateManifestUpdateRequest{Name: "new_name", Type: "standard", Active: elptr.ElPtr(false)}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":1,"name":"new_name","rollback":false,"type":"standard","active":false}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleUpdateManifestUpdate) +} diff --git a/services/ota_update_go/handlers/updatemanifestecu_add.go b/services/ota_update_go/handlers/updatemanifestecu_add.go new file mode 100644 index 0000000..e310814 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifestecu_add.go @@ -0,0 +1,52 @@ +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/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleUpdateManifestECUAdd godoc +// @Summary Add update manifest ecu +// @Description Create update manifest ECU +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param manifest body UpdateManifestECURequest true "Manifest ECU data" +// @Success 200 {object} common.UpdateManifestECU +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifestecu [post] +func HandleUpdateManifestECUAdd(w http.ResponseWriter, r *http.Request) { + ecu := common.UpdateManifestECU{} + + err := httphandlers.ParseRequest(r, &ecu) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + um := services.GetDB().GetUpdateManifests() + ecu.Files = nil + _, err = um.ECUInsert(&ecu) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + utils.RespJSON(w, http.StatusOK, ecu) +} + +type UpdateManifestECURequest struct { + UpdateManifestID int64 `json:"manifest_id,omitempty"` + ECU string `json:"name,omitempty"` + Version string `json:"version"` + HWVersions string `json:"hw_versions,omitempty"` + SelfDownload bool `json:"self_download,omitempty"` + Mode string `json:"mode,omitempty"` + InstallPriority int `json:"install_priority,omitempty" pg:"install_priority"` +} diff --git a/services/ota_update_go/handlers/updatemanifestecu_add_test.go b/services/ota_update_go/handlers/updatemanifestecu_add_test.go new file mode 100644 index 0000000..814b4b9 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifestecu_add_test.go @@ -0,0 +1,40 @@ +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 TestUpdateManifestECUAdd(t *testing.T) { + mock := mo.MockUpdateManifests{} + services.GetDB().SetUpdateManifests(&mock) + + tests := []mo.DBHttpTest{ + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/manifest", common.UpdateManifestECU{ + UpdateManifestID: 1, + ECU: "ADAS", + Version: "2112", + ConfigurationMask: "9999999", + Mode: "D", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"id":1,"manifest_id":1,"name":"ADAS","version":"2112","configuration_mask":"9999999","mode":"D"}`, + }, + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/manifest", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"UpdateManifestID required. ECU required. Version required. Mode required","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleUpdateManifestECUAdd, &mock) +} diff --git a/services/ota_update_go/handlers/updatemanifestfile_add.go b/services/ota_update_go/handlers/updatemanifestfile_add.go new file mode 100644 index 0000000..8bcb8d4 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifestfile_add.go @@ -0,0 +1,162 @@ +package handlers + +import ( + "fmt" + "io" + "mime" + "net/http" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/s3" + u "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "otaupdate/messages" + "otaupdate/services" + "otaupdate/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +const fileFieldName string = "file" + +var reURLSafe *regexp.Regexp + +func init() { + reURLSafe, _ = regexp.Compile(`[^\w\d\-\_\.]`) +} + +// HandleUpdateManifestFileAdd godoc +// @Summary Add update manifest +// @Description Upload update manifest +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param manifest_ecu_id formData int true "Manifest ECU id" +// @Param version formData string true "ECU file version" +// @Param offset formData string true "ECU file offset" +// @Param checksum formData string true "ECU file checksum" +// @Param type formData string false "ECU file type" +// @Param order formData string false "ECU file order" +// @Param file formData file true "Update file" +// @Success 200 {object} common.UpdateManifestFile +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifestfile [post] +func HandleUpdateManifestFileAdd(w http.ResponseWriter, r *http.Request) { + info, manifestfile, err := parseUpdateManifestAddRequest(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + result, encryptor, err := uploadFile(info) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + err = saveUpdateManifestFile(manifestfile, result, info) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + err = encryptor.SaveFileKey() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + u.RespJSON(w, http.StatusOK, manifestfile) +} + +func parseUpdateManifestAddRequest(r *http.Request) (*u.FileInfo, *common.UpdateManifestFile, error) { + mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return nil, nil, errors.WithStack(err) + } + if !strings.HasPrefix(mediaType, "multipart/") { + return nil, nil, errors.New(messages.RequiresMultipart) + } + if len(params["boundary"]) > 70 { + return nil, nil, errors.New(messages.BoundaryTooLong) + } + + parser, file := utils.GetUpdateManifestFileFormParser() + info, err := u.FindFilePart(r, params["boundary"], fileFieldName, parser) + if err != nil { + return nil, nil, err + } + if info.Part == nil { + return nil, nil, errors.New(messages.RequiresFile) + } + + err = validator.ValidateStruct(file) + if err != nil { + return nil, nil, err + } + + return info, file, nil +} + +func saveUpdateManifestFile(um *common.UpdateManifestFile, link *common.JSONLink, info *u.FileInfo) error { + um.FileID = info.FileID + um.Filename = info.Filename + um.URL = link.Link + um.FileSize = info.FileSize + um.WriteRegion.Length = info.OrigFileSize + + err := validator.ValidateStruct(um) + if err != nil { + return err + } + + _, err = services.GetDB().GetUpdateManifests().FileInsert(um) + + return err +} + +func uploadFile(info *u.FileInfo) (link *common.JSONLink, encryptor *utils.FileEncryptor, err error) { + encryptor, err = utils.NewEncryptor() + if err != nil { + return nil, nil, errors.WithStack(err) + } + defer encryptor.Close() + + pReader, pWriter := io.Pipe() + defer pReader.Close() + defer info.Part.Close() + + go u.ChunkFilePart(pWriter, info, encryptor.Encrypt) + url, err := putFile(pReader, urlSafeStr(info.Filename), "application/octet-stream") + if err != nil { + return + } + + info.FileID = encryptor.FileID + p := u.LinkResult(url, time.Now().Unix()) + link = &p + return +} + +func putFile(reader io.Reader, filename string, contentType string) (string, error) { + uid, err := uuid.NewRandom() + if err != nil { + return "", errors.WithStack(err) + } + key := fmt.Sprintf("%s/%s", uid, filename) + url, err := s3.GetS3().PutBucket(key, reader, contentType) + if err != nil { + return "", errors.WithStack(err) + } + + return url, nil +} + +func urlSafeStr(value string) string { + return reURLSafe.ReplaceAllLiteralString(value, "") +} diff --git a/services/ota_update_go/handlers/updatemanifestfile_add_test.go b/services/ota_update_go/handlers/updatemanifestfile_add_test.go new file mode 100644 index 0000000..ca90557 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifestfile_add_test.go @@ -0,0 +1,156 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/s3" + smock "github.com/fiskerinc/cloud-services/pkg/s3/mock" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestUpdateManifestFileAdd(t *testing.T) { + formData := `--XXXXXX +Content-Disposition: form-data; name="manifest_ecu_id" + +1000 +--XXXXXX +Content-Disposition: form-data; name="offset" + +1111 +--XXXXXX +Content-Disposition: form-data; name="version" + +2000 +--XXXXXX +Content-Disposition: form-data; name="checksum" + +CE114E45 +--XXXXXX +Content-Disposition: form-data; name="order" + +1 +--XXXXXX +Content-Disposition: form-data; name="type" + +01 Bootloader +--XXXXXX +Content-Disposition: form-data; name="signature" + +53e976bd61161e4f425186e560f0797767d10fd6f0116e224efc444fa014754cb299e51827468c03c9b06508740ed738b1ec14b9cc753a1875971e335408660e +` + + missingFile := fmt.Sprintf(`%v--XXXXXX-- +`, formData) + + badFile := fmt.Sprintf(`%v--XXXXXX +Content-Disposition: form-data; name="file"; filename="test.txt" +Content-Type: text/plain +Content-Length: 0 + +--XXXXXX-- +`, formData) + + goodData := fmt.Sprintf(`%v--XXXXXX +Content-Disposition: form-data; name="file"; filename="test.txt" +Content-Type: text/plain + +THIS IS A TEST FILE +--XXXXXX-- +`, formData) + + compare := th.JSONComparer{ + IgnoreProps: []string{"file_id", "url"}, + } + defer compare.Close() + + type TestCase struct { + th.BasicHttpTest + hasFileContent bool + jsonCompare bool + } + + tests := []TestCase{ + { + BasicHttpTest: th.BasicHttpTest{ + Name: "No data", + Request: th.MakeTestMultipartRequest("http://example.com/manifestfile", "", "XXXXXX", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"requires file","error":"Bad Request"}`, + }, + hasFileContent: false, + }, + { + BasicHttpTest: th.BasicHttpTest{ + Name: "Good data", + Request: th.MakeTestMultipartRequest("http://example.com/manifestfile", goodData, "XXXXXX", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"file_id":"303ca940c940c645","manifest_ecu_id":1000,"filename":"test.txt","url":"https://upload-dev.fiskerdps.com/f453ae14-d89a-4e4f-9eae-fe7bcc456b55/test.txt","file_size":55,"checksum":"CE114E45","type":"bootloader","order":1,"write_region":{"offset":1111,"length":19}}`, + }, + jsonCompare: true, + hasFileContent: true, + }, + { + BasicHttpTest: th.BasicHttpTest{ + Name: "Missing file", + Request: th.MakeTestMultipartRequest("http://example.com/manifestfile", missingFile, "XXXXXX", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"requires file","error":"Bad Request"}`, + }, + hasFileContent: false, + }, + { + BasicHttpTest: th.BasicHttpTest{ + Name: "Bad file", + Request: th.MakeTestMultipartRequest("http://example.com/manifestfile", badFile, "XXXXXX", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Length gt 0","error":"Bad Request"}`, + }, + hasFileContent: false, + }, + } + + for _, test := range tests { + setup() + w := th.ExecHTTPHandler(handlers.HandleUpdateManifestFileAdd, test.Request) + + if w.Result().StatusCode != test.ExpectedStatus { + t.Errorf(th.TestErrorTemplate, test.Name, test.ExpectedStatus, w.Result().StatusCode) + } + + if !test.jsonCompare && w.Body.String() != test.ExpectedResponse { + t.Errorf(th.TestErrorTemplate, test.Name, test.ExpectedResponse, w.Body.String()) + } + + if test.jsonCompare { + diff, err := compare.GetDiff([]byte(test.ExpectedResponse), w.Body.Bytes()) + if err != nil { + t.Errorf(th.TestErrorTemplate, test.Name, nil, err) + } + for key, value := range diff { + body := w.Body.String() + fmt.Println(body) + t.Errorf(th.TestErrorTemplate, test.Name, key, value) + } + } + + if test.hasFileContent { + s3 := s3.GetS3().(*smock.S3Mock) + content := string(s3.GetData()) + if len(content) == 0 { + t.Errorf(th.TestErrorTemplate, test.Name, test.hasFileContent, fmt.Sprintf("Content size %v", len(content))) + } + } + } +} + +func setup() { + // Setup mocks + s3.SetS3Instance(&smock.S3Mock{}) + services.GetDB().SetFileKeys(&mocks.MockFileKeys{}) + services.GetDB().SetUpdateManifests(&mocks.MockUpdateManifests{}) +} diff --git a/services/ota_update_go/handlers/updatemanifests_get.go b/services/ota_update_go/handlers/updatemanifests_get.go new file mode 100644 index 0000000..9f9e4ca --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifests_get.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/authproviders" + 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" +) + +// HandleUpdateManifestsGet godoc +// @Summary Search update manifests +// @Description Get update manifests filtered by id, name, version, description +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param search query string false "Text search" +// @Param id query string false "Update manifest id" +// @Param name query string false "Update manifest name" +// @Param version query string false "Update manifest version" +// @Param desc query string false "Update manifest description" +// @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_type query int false "Manifest type (1=software, 2=configuration)" +// @Param active query boolean false "Filter by activate or inactive manifests" +// @Success 200 {object} common.JSONDBQueryResult{data=[]common.UpdateManifest} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /manifests [get] +func HandleUpdateManifestsGet(w http.ResponseWriter, r *http.Request) { + var total int + filter, err := parseUpdateManifestFilter(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + if provider := utils.AUTHGetProviderFromRequest(r); provider == authproviders.Magna || provider == authproviders.FiskerQA { + filter.ManifestType = common.MagnaManifestUpdateType + } + + options, err := orm.ParsePageQuery(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + if options.Order == "" { + options.Order = "created_at DESC" + } + + um := services.GetDB().GetUpdateManifests() + ums, err := um.Search(filter, options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + if options.Offset == 0 && filter.ID == 0 { + total, err = um.SearchCount(filter) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: ums, + Total: total, + }) +} + +func parseUpdateManifestFilter(r *http.Request) (common.UpdateManifestSearch, error) { + qs := r.URL.Query() + filter := common.UpdateManifestSearch{ + Search: qs.Get("search"), + UpdateManifest: common.UpdateManifest{ + ID: urlhelper.GetQueryInt64(qs, "id"), + Name: qs.Get("name"), + Version: qs.Get("version"), + Description: qs.Get("desc"), + ManifestType: common.UpdateManifestType(urlhelper.GetQueryInt(qs, "manifest_type")), + }, + } + + active, ok := urlhelper.GetQueryBool(qs, "active") + if ok { + filter.UpdateManifest.Active = &active + } + + err := validator.ValidateNonRequired(filter) + + return filter, err +} diff --git a/services/ota_update_go/handlers/updatemanifests_get_test.go b/services/ota_update_go/handlers/updatemanifests_get_test.go new file mode 100644 index 0000000..50b9cf4 --- /dev/null +++ b/services/ota_update_go/handlers/updatemanifests_get_test.go @@ -0,0 +1,151 @@ +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 TestUpdateManifestGet(t *testing.T) { + mock := mo.MockUpdateManifests{} + services.GetDB().SetUpdateManifests(&mock) + expectedResp := `{"data":[{"id":1,"name":"Test","version":"1.1","description":"bla bla keyword","rollback":false,"type":"forced","country":"US","powertrain":"MD23","restraint":"None","model":"Ocean","trim":"Sport","year":2022,"body_type":"truck"}],"total":1}` + expectedRespNoTotal := `{"data":[{"id":1,"name":"Test","version":"1.1","description":"bla bla keyword","rollback":false,"type":"forced","country":"US","powertrain":"MD23","restraint":"None","model":"Ocean","trim":"Sport","year":2022,"body_type":"truck"}]}` + defaultOrder := "created_at DESC" + listData := []common.UpdateManifest{ + { + ID: 1, + Name: "Test", + Version: "1.1", + Description: "bla bla keyword", + Type: "forced", + RollbackEnabled: false, + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + }, + } + + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.UpdateManifest{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests?id=1", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.UpdateManifest{ + ID: 1, + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Name, version, description, type parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests?name=Test&version=1.1&desc=keyword&manifest_type=2", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.UpdateManifest{ + Name: "Test", + Version: "1.1", + Description: "keyword", + ManifestType: common.SoftwareUpdateType, + Country: "US", + PowerTrain: "MD23", + Restraint: "None", + Model: "Ocean", + Trim: "Sport", + Year: 2022, + BodyType: "truck", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: orm.PageQueryOptionsLimitMaximum, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.UpdateManifest{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: &common.UpdateManifest{}, + 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/manifests?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/manifests?limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + { + Name: "SQL Injection Test Simulation", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests?order=CASE WHEN ('1'='1') THEN vin ELSE year END asc", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Order sqlorder ","error":"Bad Request"}`, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleUpdateManifestsGet, &mock) +} diff --git a/services/ota_update_go/handlers/vehicle_add.go b/services/ota_update_go/handlers/vehicle_add.go new file mode 100644 index 0000000..7187e74 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_add.go @@ -0,0 +1,77 @@ +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/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + mon "go.mongodb.org/mongo-driver/mongo" +) + +var defaultFleet = envtool.GetEnv("DEFAULT_FLEET", "Default-Ocean") + +// HandleVehicleAdd godoc +// @Summary Adds vehicle +// @Description Creates vehicle data +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param car body common.AddCarRequest true "Car data" +// @Success 200 {object} common.AddCarRequest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle [post] +func HandleVehicleAdd(w http.ResponseWriter, r *http.Request) { + var acr common.AddCarRequest + err := httphandlers.ParseRequest(r, &acr) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + if acr.CANBus == nil { + acr.CANBus = &common.CANBus{Enabled: true, DTCEnabled: elptr.ElPtr(true)} + } + + c, err := utils.ParseVIN(acr.VIN) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + _, err = services.GetDB().GetCars().Insert(c) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + client, err := services.GetMongoClient() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + v := &mongo.Vehicle{VIN: acr.VIN, CANBus: *acr.CANBus, LogLevel: common.UnmarshalLogLevelString(logLevel), IDPSEnabled: acr.IDPSEnabled} + err = client.GetVehicles().AddVehicle(v) + if mon.IsDuplicateKeyError(err) { + logger.Err(err).Msgf("when adding vehicle %s to mongo, received duplicate error from mongodb. car added to postgres but no fleet", acr.VIN) + return + } + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + logger.Err(err).Msgf("when adding vehicle %s to mongo, received error, car added to postgres but no fleet", acr.VIN) + return + } + + err = client.GetFleets().AddVehiclesToFleet(defaultFleet, []string{acr.VIN}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, &acr) +} diff --git a/services/ota_update_go/handlers/vehicle_add_test.go b/services/ota_update_go/handlers/vehicle_add_test.go new file mode 100644 index 0000000..4db2cdd --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_add_test.go @@ -0,0 +1,85 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + m "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/mongo" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +func TestAddVehicle(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mock := mocks.MockCars{} + services.GetDB().SetCars(&mock) + client.SetVehicles(mongo.NewVehiclesCollection(&mongo.MockCollection{})) + + tests := []mocks.DBHttpTest{ + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required","error":"Bad Request"}`, + }, + { + Name: "No VIN", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", m.AddCarRequest{}), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required","error":"Bad Request"}`, + }, + { + Name: "Bad data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", m.AddCarRequest{ + VIN: "XXXXXXXXXXXXXXXXXXXXX", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN 'XXXXXXXXXXXXXXXXXXXXX' invalid","error":"Bad Request"}`, + }, + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", m.AddCarRequest{ + VIN: "1G1FP87S3GN100062", + CANBus: &m.CANBus{ + Enabled: true, + DataLogger: false, + DTCEnabled: elptr.ElPtr(false), + }, + IDPSEnabled: true, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":true}`, + }, + { + Name: "Good data, no CANBus", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", m.AddCarRequest{ + VIN: "1G1FP87S3GN100062", + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle", m.AddCarRequest{ + VIN: "1G1FP87S3GN100062", + }), + 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.HandleVehicleAdd, &mock) +} diff --git a/services/ota_update_go/handlers/vehicle_command.go b/services/ota_update_go/handlers/vehicle_command.go new file mode 100644 index 0000000..594c6f6 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_command.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/carcommand" + "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/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/utils" + "google.golang.org/protobuf/proto" +) + +// HandleVehicleCommand godoc +// @Summary Send command to car +// @Description Send command to car +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param Command body common.BulkCarCommands true "Command to send to cars" +// @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 /vehiclecommand [post] +func HandleVehicleCommand(w http.ResponseWriter, r *http.Request) { + var request common.BulkCarCommands + err := httphandlers.ParseRequest(r, &request) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + err = carcommand.ValidateCommand(request.Command) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + k := services.GetKafkaProducer() + + alDB := services.GetDB().GetActionLog() + + // Throwing away error, do not need + description, _ := json.Marshal(request.RemoteCommandSource) + + for _, vin := range request.VINs { + go func(vin string) { + 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") + } + }(vin) + + data := common.RemoteCommandRequest{ + VIN: vin, + Source: httphandlers.GetClientID(r), + RemoteCommandSource: request.RemoteCommandSource, + } + dataBytes, _ := json.Marshal(data) + var cmd kafka_grpc.RemoteCommand + err := json.Unmarshal(dataBytes, &cmd) + // I don't know if there is any reason to stick these in kafka. Like I think they are just + // inserted into the redis pub sub + if err == nil { + payload := kafka_grpc.GRPC_ValetPayload{ + Handler: "remote_command", + Data: &kafka_grpc.GRPC_ValetPayload_RemoteCmd{ + RemoteCmd: &cmd, + }, + } + binaryPayload, _ := proto.Marshal(&payload) + err = k.ProduceBinary( + kafka.ValetServiceGRPCKafka, + common.Service.Key(vin), + binaryPayload, + nil, + ) + } + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, &common.JSONMessage{ + Message: fmt.Sprintf("remote command sent to %d vehicles", len(request.VINs)), + }) +} diff --git a/services/ota_update_go/handlers/vehicle_command_test.go b/services/ota_update_go/handlers/vehicle_command_test.go new file mode 100644 index 0000000..bc28520 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_command_test.go @@ -0,0 +1,121 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + km "github.com/fiskerinc/cloud-services/pkg/kafka/mock" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/fiskerinc/cloud-services/pkg/testrunner" + + "otaupdate/handlers" + "otaupdate/services" +) + +func TestVehicleCommand(t *testing.T) { + vin := "1G1FP87S3GN100062" + mockKafka := km.KafkaMock{} + services.SetKafkaProducer(&mockKafka) + + tests := []testrunner.TestCase{ + { + Name: "Unlock command", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{vin}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "doors_unlock", + }, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"remote command sent to 1 vehicles"}`, + }, + }, + { + Name: "No data", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs required. Command required","error":"Bad Request"}`, + }, + }, + { + Name: "Bad VIN data", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{vin, "XXXXX"}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "TEST", + Data: stringPointer("TEST_PARAMETERS"), + }, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs[1] 'XXXXX' invalid","error":"Bad Request"}`, + }, + }, + { + Name: "Bad command", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{vin}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "TEST", + Data: stringPointer("TEST_PARAMETERS"), + }, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"unknown command","error":"Bad Request"}`, + }, + }, + { + Name: "Lock command", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{vin}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "doors_lock", + }, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"remote command sent to 1 vehicles"}`, + }, + }, + { + Name: "Vent windows", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{"1G1FP87S3GN100062"}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "vent_windows", + }, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"remote command sent to 1 vehicles"}`, + }, + }, + { + Name: "Close window", + HttpTestCase: &tester.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehiclecommand", common.BulkCarCommands{ + VINs: []string{"1G1FP87S3GN100062"}, + RemoteCommandSource: common.RemoteCommandSource{ + Command: "close_windows", + }, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"remote command sent to 1 vehicles"}`, + }, + }, + } + + for _, test := range tests { + w := test.HttpTestCase.Test(handlers.HandleVehicleCommand) + test.HttpTestCase.ValidateHttp(t, test.Name, w) + } +} + +func stringPointer(s string) *string { + return &s +} diff --git a/services/ota_update_go/handlers/vehicle_connectionstatuses_test.go b/services/ota_update_go/handlers/vehicle_connectionstatuses_test.go new file mode 100644 index 0000000..895780a --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_connectionstatuses_test.go @@ -0,0 +1,59 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/redis" + rm "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleVehicleConnectionStatuses(t *testing.T) { + redis.MockRedisConnection() + mockRedis := rm.MockRedis{ + SISMEMBEResults: map[string]map[string]interface{}{ + redis.CarSessionsKey(): { + "1G1FP87S3GN100062": int64(0), + "3C4PDCBG0ET127145": int64(1), + }, + redis.HMISessionsKey(): { + "1G1FP87S3GN100062": int64(0), + "3C4PDCBG0ET127145": int64(1), + }, + }, + } + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + + tests := []mocks.DBHttpTest{ + { + Name: "Good data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carsconnected", common.VINs{ + VINs: []string{"1G1FP87S3GN100062", "3C4PDCBG0ET127145"}, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"1G1FP87S3GN100062":false,"2:1G1FP87S3GN100062":false,"2:3C4PDCBG0ET127145":true,"3C4PDCBG0ET127145":true}`, + }, + { + Name: "No data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carsconnected", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs required","error":"Bad Request"}`, + }, + { + Name: "Bad VIN data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carsconnected", common.VINs{ + VINs: []string{"1G1FP87S3GN100062", "XXXXXX"}, + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VINs[1] 'XXXXXX' invalid","error":"Bad Request"}`, + }, + } + + mocks.RunDBTests(t, tests, handlers.HandleVehicleConnectionStatuses, nil) +} diff --git a/services/ota_update_go/handlers/vehicle_connnectionstatuses.go b/services/ota_update_go/handlers/vehicle_connnectionstatuses.go new file mode 100644 index 0000000..810b04d --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_connnectionstatuses.go @@ -0,0 +1,76 @@ +package handlers + +import ( + "fmt" + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + re "github.com/fiskerinc/cloud-services/pkg/redis" + rutils "github.com/fiskerinc/cloud-services/pkg/redis/redisutils" + "github.com/fiskerinc/cloud-services/pkg/utils" + + "github.com/pkg/errors" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehicleConnectionStatuses godoc +// @Summary Gets connection statuses car vins +// @Description Returns hash object of car connection statuses +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vins body common.VINs true "VINs" +// @Success 200 {object} map[string]bool "Car update statuses" +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /carsconnected [post] +func HandleVehicleConnectionStatuses(w http.ResponseWriter, r *http.Request) { + var request common.VINs + err := httphandlers.ParseRequest(r, &request) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + count := len(request.VINs) + + conn := services.RedisClientPool().GetFromPool() + defer conn.Close() + cars, err := rutils.CheckSet(conn, re.CarSessionsKey(), request.VINs) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + if len(cars) != count { + err = errors.New("returned statuses not equal in length to vins") + loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) + return + } + + hmis, err := rutils.CheckSet(conn, re.HMISessionsKey(), request.VINs) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + if len(hmis) != count { + err = errors.New("returned HMI statuses not equal in length to vins") + loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) + return + } + + result := make(map[string]bool, count*2) + for i, vin := range request.VINs { + result[vin] = cars[i] + result[fmt.Sprintf("2:%s", vin)] = hmis[i] + } + + utils.RespJSON(w, http.StatusOK, result) +} + +type CarConnectionStatus struct { + VIN string `json:"vin"` + Connected bool `json:"connected"` +} diff --git a/services/ota_update_go/handlers/vehicle_delete.go b/services/ota_update_go/handlers/vehicle_delete.go new file mode 100644 index 0000000..52ce2ca --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_delete.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/julienschmidt/httprouter" + + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +// Need this so the swagger can use m +var _ = common.APIToken{} + +// HandleVehicleDelete godoc +// @Summary Delete vehicle +// @Description Delete vehicle data. Requires delete permissions +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @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 /vehicle/{vin} [delete] +func HandleVehicleDeleteHoneyPot(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + vin := params.ByName("vin") + username := httphandlers.GetClientID(r) + + logger.Error().Msgf("user %s attempted to delete car %s", username, vin) +} + +/* func HandleVehicleDelete(w http.ResponseWriter, r *http.Request) { + var car common.Car + params := httprouter.ParamsFromContext(r.Context()) + car.VIN = params.ByName("vin") + + err := validator.ValidateNonRequired(car) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + client, err := services.GetMongoClient() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + removeVinFromFleets(car.VIN, client) + + _, err = services.GetDB().GetCars().Delete(&car) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + err = client.GetVehicles().DeleteVehicle(&mongo.Vehicle{VIN: car.VIN}) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, logger.MongoDeleteErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "Deleted", + }) +} */ + +/* func removeVinFromFleets(vin string, client mongo.Client) error { + fleets, err := client.GetVehicles().GetFleetsForVehicle(vin, "", &queries.PageQueryOptions{}) + if err != nil { + return err + } + + if len(fleets) == 0 { + return nil + } + + for _, fleet := range fleets { + err = client.GetFleets().DeleteVehiclesFromFleet(fleet, []string{vin}) + if err != nil { + return err + } + + } + ResetVehiclesConfigCache([]string{vin}) + + return nil +} */ diff --git a/services/ota_update_go/handlers/vehicle_delete_test.go b/services/ota_update_go/handlers/vehicle_delete_test.go new file mode 100644 index 0000000..5a5b38d --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_delete_test.go @@ -0,0 +1,59 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/mongo" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestVehicleDelete(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + services.GetDB().SetCars(&mocks.MockCars{}) + client.SetVehicles(mongo.NewVehiclesCollection(&mongo.MockCollection{})) + + client.SetFleets(mongo.NewFleetsCollection(&mongo.MockCollection{ + AggregateObject: []mongo.Fleet{ + { + Name: "US-TEST", + Vehicles: []string{ + "2HNYD18936H520501", + }, + }, + }, + })) + + tests := []th.BasicHttpTest{ + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/vehicle/0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN '0' invalid","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/vehicle/1G1FP87S3GN100062", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + { + Name: "VIN with fleet", + Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/vehicle/2HNYD18936H520501", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + _ = tests + _ = handlers.HandleVehicleDeleteHoneyPot + //th.RunParamHttpTests(t, tests, handlers.HandleVehicleDelete, "/vehicle/:vin") +} diff --git a/services/ota_update_go/handlers/vehicle_diagnostic_command.go b/services/ota_update_go/handlers/vehicle_diagnostic_command.go new file mode 100644 index 0000000..e82778e --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_diagnostic_command.go @@ -0,0 +1,185 @@ +package handlers + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + + "otaupdate/services" + + "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/utils" +) + +var OTAUpdateECUReplacement = map[string]string{ + "TBOX": "MCU", + "PDU": "OBC", + "EPS": "EPS1", +} + +// Replace PDX ECUs to the names known by T.Rex. +func transformECUNames(pdxEcu []string) { + for i := 0; i < len(pdxEcu); i++ { + ecu := pdxEcu[i] + if replacementECU, exists := OTAUpdateECUReplacement[ecu]; exists { + pdxEcu[i] = replacementECU + } + } +} + +var ModelToECUSecOC = map[string][]string{ + // '1' - Fisker Ocean One. + "1": {"TBOX" /*T.Rex's MCU*/, "GW", "MCU_R" /*It's called EDU 'Electtric Drive Unit'*/, "MCU_F", + "BMS", "PDU" /*T.Rex's OBC*/, "VCU", "ECC", "ADAS", "EPS", "BCM", + "PKC", "FCM", "MRR", "CMRR_FL", "CMRR_FR", "CMRR_RL", "CMRR_RR"}, +} +var parseRequest = func(request common.RemoteDiagnosticCommandRequest, w http.ResponseWriter) (interface{}, error) { + if request.Command == "can_network" { + var args common.RemoteCANNetworkCommandArgs + args.Action = request.CANNetAction + args.Timeout = request.Timeout + return args, nil + } + if request.Command == "remote_ignition" { + var args common.RemoteIgnitionCommandArgs + args.Action = request.IgnitionAction + args.Timeout = request.Timeout + return args, nil + } + if request.Command == "read_ecu_versions" { + var args common.RemoteReadVersionsCommandArgs + args.ECUName = request.ECU + return args, nil + } + var args common.RemoteResetDiagnosticCommandArgs + args.ECUName = request.ECU + //Don't need any keys for reset TBOX + if request.ECU != "TBOX" { + //XXX TBOX ECUs were divided in to MCU, T.Rex, and NAD. + //Keys had already been inserted in DB with the name 'TBOX' for what is referred to as 'MCU' and so we must pull the key by that name. + if request.ECU == "MCU" { + request.ECU = "TBOX" + } + eccKeys, err := services.GetDB().GetECCKeys().SelectPrivateKeysByECUsEnv([]string{request.ECU}, "") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound, loggerdataresp.PostgresNoRowsErrorCheck) { + return nil, errors.New("") + } + if len(eccKeys) != 1 { + utils.RespError(w, http.StatusNotFound, "ECC keys not found") + return nil, errors.New("") + } + args.UDSKeys = &eccKeys[0] + args.UDSKeys.ECU = "" + } + return args, nil +} + +// HandleVehicleDiagnosticCommand godoc +// @Summary Send diagnostic command to car +// @Description Send diagnostic command to car +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param Command body common.RemoteDiagnosticCommandRequest true "Diagnostic command to send to cars" +// @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 /vehiclediagnosticcommand [post] +func HandleVehicleDiagnosticCommand(w http.ResponseWriter, r *http.Request) { + var request common.RemoteDiagnosticCommandRequest + err := httphandlers.ParseRequest(r, &request) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + client := services.RedisClientPool().GetFromPool() + defer client.Close() + + b, _ := json.Marshal(request) + 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/vehicle_diagnostic_command.go", + Description: string(b), + } + + for _, vin := range request.VINs { + actionLog.VIN = vin + err = alDB.Insert(actionLog) + if err != nil { + logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateCancel") + } + } + }() + + // Since this uses publish, if the car is not awake, the message is never received by the car + SendToVin := func(vin string, data interface{}) { + err = client.SafePublishMessage( + common.TRex.Key(vin), + common.Message{ + Handler: request.Command, + Data: data, + }, + ) + loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) + } + + if request.Command == "write_secoc_key" { + for _, vin := range request.VINs { + var args common.RemoteUpdateSecOCCommandArgs + + // Fourth character in VIN is model. + var ecus []string + for _, ecu := range ModelToECUSecOC[string(vin[3])] { + ecus = append(ecus, ecu) + } + eccKeys, err := services.GetDB().GetECCKeys().SelectPrivateKeysByECUsEnv(ecus, "") + transformECUNames(ecus) + args.ECUs = ecus + if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound, loggerdataresp.PostgresNoRowsErrorCheck) { + continue + } + args.UDSKeys = eccKeys + for i := 0; i < len(args.UDSKeys); i++ { + ecu := args.UDSKeys[i].ECU + if replacementECU, exists := OTAUpdateECUReplacement[ecu]; exists { + args.UDSKeys[i].ECU = replacementECU + } + } + symKeys, err := services.GetDB().GetSymKeys().SelectByVIN(vin) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound, loggerdataresp.PostgresNoRowsErrorCheck) { + continue + } + args.KeyBase64 = base64.StdEncoding.EncodeToString(symKeys.SecOC.Bytes()) + SendToVin(vin, args) + } + utils.RespJSON(w, http.StatusOK, &common.JSONMessage{ + Message: fmt.Sprintf("remote diagnostic command sent to %d vehicles", len(request.VINs)), + }) + return + } + + args, err := parseRequest(request, w) + if err != nil {// TODO: need to write an error to request + return + } + + for _, vin := range request.VINs { + SendToVin(vin, args) + } + + utils.RespJSON(w, http.StatusOK, &common.JSONMessage{ + Message: fmt.Sprintf("remote diagnostic command sent to %d vehicles", len(request.VINs)), + }) +} diff --git a/services/ota_update_go/handlers/vehicle_ecus_get.go b/services/ota_update_go/handlers/vehicle_ecus_get.go new file mode 100644 index 0000000..5a226bc --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_ecus_get.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "net/http" + "regexp" + "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/fiskerinc/cloud-services/pkg/validator" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +var rxSWVersion = regexp.MustCompile(`\bsw_version\b`) + +// HandleVehicleECUsGet godoc +// @Summary Get car ECUs +// @Description Returns ECUs for car +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin query string true "Car VIN" +// @Param search query string false "Text search" +// @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 unique query bool false "Get only latest ECU versions" +// @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 /vehicleecus [get] +func HandleVehicleECUsGet(w http.ResponseWriter, r *http.Request) { + var total int + c := services.GetDB().GetCars() + filter, err := parseCarECUsFilter(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + options, err := queries.ParsePageQuery(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + updateCarECUsOptions(options) + + ecus, err := c.GetCarECUs( + filter, + options, + ) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + if options.Offset == 0 { + total, err = c.GetCarECUsCount(filter) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: ecus, + Total: total, + }) +} + +func updateCarECUsOptions(options *queries.PageQueryOptions) { + if options.Order == "" { + options.Order = "ecu" + } else { + options.Order = rxSWVersion.ReplaceAllString(options.Order, "version") + } +} + +func parseCarECUsFilter(r *http.Request) (common.CarECUFilter, error) { + qs := r.URL.Query() + + unique, err := strconv.ParseBool(qs.Get("unique")) + if err != nil { + unique = false + } + + filter := common.CarECUFilter{ + Search: qs.Get("search"), + Unique: unique, + VIN: qs.Get("vin"), + } + + err = validator.ValidateField(filter.VIN, "vin") + + return filter, err +} diff --git a/services/ota_update_go/handlers/vehicle_ecus_get_test.go b/services/ota_update_go/handlers/vehicle_ecus_get_test.go new file mode 100644 index 0000000..52d0026 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_ecus_get_test.go @@ -0,0 +1,76 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + + "otaupdate/handlers" + "otaupdate/services" +) + +func TestHandleVehicleECUsGet(t *testing.T) { + services.GetDB().SetCars(&mocks.MockCars{ + SelectCarECUs: []common.CarECU{ + { + ECU: "ECUA", + Version: "1000", + SerialNumber: "serial", + HWVersion: "AAAAA", + }, + { + VIN: "JH4KA3240LC800239", + ECU: "ECUB", + Version: "1001", + SerialNumber: "serial", + HWVersion: "BBBBB", + Epoch_usec: 1689352536, + }, + { + VIN: "JH4KA3240LC800239", + ECU: "ECUB", + Version: "1001", + SerialNumber: "serial", + HWVersion: "BBBBB", + }, + }, + }) + + tests := []th.BasicHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleecus", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"vin '' invalid","error":"Bad Request"}`, + }, + { + Name: "Bad VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleecus?vin=XXXXXXX", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"vin 'XXXXXXX' invalid","error":"Bad Request"}`, + }, + { + Name: "Good VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleecus?vin=1G1FP87S3GN100062", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"ecu":"ECUA","sw_version":"1000","serial_number":"serial","hw_version":"AAAAA","epoch_usec":0},{"vin":"JH4KA3240LC800239","ecu":"ECUB","sw_version":"1001","serial_number":"serial","hw_version":"BBBBB","epoch_usec":1689352536},{"vin":"JH4KA3240LC800239","ecu":"ECUB","sw_version":"1001","serial_number":"serial","hw_version":"BBBBB","epoch_usec":0}],"total":3}`, + }, + { + Name: "Filter on text search", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleecus?vin=1G1FP87S3GN100062&search=SOMETEST", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"ecu":"ECUA","sw_version":"1000","serial_number":"serial","hw_version":"AAAAA","epoch_usec":0}],"total":1}`, + }, + { + Name: "Filter on most recent VIN/ECU record", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleecus?vin=1G1FP87S3GN100062&unique=true", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"ecu":"ECUA","sw_version":"1000","serial_number":"serial","hw_version":"AAAAA","epoch_usec":0},{"vin":"JH4KA3240LC800239","ecu":"ECUB","sw_version":"1001","serial_number":"serial","hw_version":"BBBBB","epoch_usec":1689352536}],"total":2}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleVehicleECUsGet) +} diff --git a/services/ota_update_go/handlers/vehicle_filter_add.go b/services/ota_update_go/handlers/vehicle_filter_add.go new file mode 100644 index 0000000..ca10f59 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_add.go @@ -0,0 +1,148 @@ +package handlers + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "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/validator" + + "github.com/julienschmidt/httprouter" +) + +// HandleVehicleFilterAdd godoc +// @Summary Add CAN filter for vehicle +// @Description Add CAN filter for vehicle +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param config body VehicleFilterRequest 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 /vehicle/{vin}/filter [post] +func HandleVehicleFilterAdd(w http.ResponseWriter, r *http.Request) { + vehicleFilterAdd.Handle(w, r) +} + +var vehicleFilterAdd = controllers.NewMongoUpdate(&vehicleFilterAddHelper{}) + +type vehicleFilterAddHelper struct { + vehicleFiltersHelper +} + +func (h *vehicleFilterAddHelper) ParseUpdateURLParams(r *http.Request) interface{} { + req := &VehicleFilterParams{} + + params := httprouter.ParamsFromContext(r.Context()) + req.VIN = params.ByName("vin") + + return req +} + +func (h *vehicleFilterAddHelper) ValidateFields(model interface{}) error { + p, ok := model.(*VehicleFilterParams) + if !ok { + return nil + } + + if p.VIN == "" { + return ErrInvalidURLParams + } + return nil +} + +func (h *vehicleFilterAddHelper) ParseRequestBody(r *http.Request, model interface{}) error { + if err := httphandlers.ParseRequest(r, model); err != nil { + return errors.WithMessagef(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 *vehicleFilterAddHelper) QueryUpdate(filter interface{}, model interface{}) error { + client, err := services.GetMongoClient() + if err != nil { + return err + } + + err = client.GetVehicles().AddFilterToVehicle(filter.(*VehicleFilterParams).VIN, model.(*common.CANFilter)) + if err != nil { + return err + } + + return ResetVehiclesConfigCache([]string{filter.(*VehicleFilterParams).VIN}) +} + +func ResetVehiclesConfigCache(vins []string) error { + r := services.RedisClientPool().GetFromPool() + defer r.Close() + + if err := cache.RemoveCacheConfigForVehicles(r, vins); err != nil { + if len(vins) == 1 { + logger.Warn().Msgf("Failed to remove cache for %d vehicle", len(vins)) + } else { + logger.Warn().Msgf("Failed to remove cache for %d vehicles", len(vins)) + } + } + + return nil +} + +type VehicleFilterRequest struct { + CANID string `json:"can_id"` + Interval *int `json:"interval"` + EdgeMask *common.BinaryHex `json:"edge_mask"` +} + +type vehicleFiltersHelper struct{} + +func (h *vehicleFiltersHelper) NewModel() interface{} { + return &common.CANFilter{} +} + +func (h *vehicleFiltersHelper) HasPK(model interface{}) bool { + return model.(*common.CANFilter).CANID != "" +} + +func (h *vehicleFiltersHelper) ValidatePK(model interface{}) error { + result := model.(*VehicleFilterParams) + + err := validator.ValidateField(result.VIN, "required,vin") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} + +type VehicleFilterParams struct { + VIN string `json:"vin" validate:"required,vin"` + common.CANFilter +} diff --git a/services/ota_update_go/handlers/vehicle_filter_add_test.go b/services/ota_update_go/handlers/vehicle_filter_add_test.go new file mode 100644 index 0000000..51c24e8 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_add_test.go @@ -0,0 +1,72 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + + "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 TestVehicleFilterAdd(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mockMongo := mongo.NewVehiclesCollection(&mongo.MockCollection{}) + client.SetVehicles(mockMongo) + + conn := tester.NewRedisMock() + conn.SetValues = map[string]tester.ExpiringCache{ + redis.CarConfigKey("2D4FV48T95H646760"): { + Value: nil, + }, + } + services.SetRedisClientPool(tester.NewMockClientPool(conn)) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid vin parameter", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle/TESTVIN123/filter", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`, + }, + { + Name: "Invalid data", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle/2D4FV48T95H646760/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/vehicle/2D4FV48T95H646760/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/vehicle/2D4FV48T95H646760/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/vehicle/2D4FV48T95H646760/filter", + common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"can_id":"123","interval":100}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleFilterAdd, "/vehicle/:vin/filter") +} diff --git a/services/ota_update_go/handlers/vehicle_filter_delete.go b/services/ota_update_go/handlers/vehicle_filter_delete.go new file mode 100644 index 0000000..191cf04 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_delete.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "net/http" + + "otaupdate/controllers" + "otaupdate/services" + + "github.com/julienschmidt/httprouter" +) + +// HandleVehicleFilterDelete godoc +// @Summary Delete filter from vehicle +// @Description Delete filter from vehicle +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @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 /vehicle/{vin}/filter/{id} [delete] +func HandleVehicleFilterDelete(w http.ResponseWriter, r *http.Request) { + vehicleFilterDelete.Handle(w, r) +} + +var vehicleFilterDelete = controllers.NewMongoDelete(&vehicleFilterDeleteHelper{}) + +type vehicleFilterDeleteHelper struct { + vehicleFiltersHelper +} + +func (h *vehicleFilterDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} { + req := &VehicleFilterParams{} + + params := httprouter.ParamsFromContext(r.Context()) + req.VIN = params.ByName("vin") + req.CANID = params.ByName("id") + + return req +} + +func (h *vehicleFilterDeleteHelper) ValidateFields(model interface{}) error { + p := model.(*VehicleFilterParams) + if p.VIN == "" || p.CANID == "" { + return ErrInvalidURLParams + } + return nil +} + +func (h *vehicleFilterDeleteHelper) QueryDelete(filter interface{}) error { + client, err := services.GetMongoClient() + if err != nil { + return err + } + + if err = client.GetVehicles().DeleteFilterForVehicle(filter.(*VehicleFilterParams).VIN, filter.(*VehicleFilterParams).CANID); err != nil { + return err + } + + return ResetVehiclesConfigCache([]string{filter.(*VehicleFilterParams).VIN}) +} diff --git a/services/ota_update_go/handlers/vehicle_filter_delete_test.go b/services/ota_update_go/handlers/vehicle_filter_delete_test.go new file mode 100644 index 0000000..6ff2143 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_delete_test.go @@ -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 TestVehicleFilterDelete(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/vehicle/2D4FV48T95H646760/filter/123", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"message":"Deleted"}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleFilterDelete, "/vehicle/:vin/filter/:id") +} diff --git a/services/ota_update_go/handlers/vehicle_filter_get_list.go b/services/ota_update_go/handlers/vehicle_filter_get_list.go new file mode 100644 index 0000000..14d71e1 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_get_list.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "net/http" + + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/julienschmidt/httprouter" +) + +// HandleVehicleFilterGetList godoc +// @Summary Get filters for car +// @Description Get filters for a car +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @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 /vehicle/{vin}/filters [get] +func HandleVehicleFilterGetList(w http.ResponseWriter, r *http.Request) { + vehicleFilterGetList.Handle(w, r) +} + +var vehicleFilterGetList = controllers.NewMongoGetList(&vehicleFilterGetListHelper{}) + +type vehicleFilterGetListHelper struct { + vehicleFiltersHelper +} + +func (h *vehicleFilterGetListHelper) NewModel() interface{} { + return &VehicleFilterParams{} +} + +func (h *vehicleFilterGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) { + filter := model.(*VehicleFilterParams) + + params := httprouter.ParamsFromContext(r.Context()) + filter.VIN = params.ByName("vin") +} + +func (h *vehicleFilterGetListHelper) ValidateStruct(model interface{}) error { + result := model.(*VehicleFilterParams) + + err := validator.ValidateField(result.VIN, "required,vin") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} + +func (h *vehicleFilterGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) { + // does not utilize URL queries so leave this function empty +} + +func (h *vehicleFilterGetListHelper) QueryCount(filter interface{}) (int64, error) { + client, err := services.GetMongoClient() + if err != nil { + return 0, err + } + + return client.GetVehicles().GetFilterCountForVehicle(filter.(*VehicleFilterParams).VIN) +} + +func (h *vehicleFilterGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + return client.GetVehicles().GetFiltersForVehicle(filter.(*VehicleFilterParams).VIN, options) +} diff --git a/services/ota_update_go/handlers/vehicle_filter_get_list_test.go b/services/ota_update_go/handlers/vehicle_filter_get_list_test.go new file mode 100644 index 0000000..8eb5ba4 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_get_list_test.go @@ -0,0 +1,57 @@ +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 TestVehicleFilterGetList(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mockMongo := mongo.NewVehiclesCollection( + &mongo.MockCollection{ + AggregateObject: []mongo.Vehicle{ + { + VIN: "TESTVIN123", + CANBus: common.CANBus{ + Filters: []common.CANFilter{ + { + CANID: "123", + Interval: elptr.ElPtr(100), + }, + }, + }, + }, + }, + }, + ) + client.SetVehicles(mockMongo) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid vin parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/TESTVIN123/filters", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/2D4FV48T95H646760/filters", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"can_id":"123","interval":100}]}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleFilterGetList, "/vehicle/:vin/filters") +} diff --git a/services/ota_update_go/handlers/vehicle_filter_update.go b/services/ota_update_go/handlers/vehicle_filter_update.go new file mode 100644 index 0000000..8f8c6c4 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_update.go @@ -0,0 +1,101 @@ +package handlers + +import ( + "net/http" + + "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "otaupdate/controllers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" +) + +// HandleVehicleFilterUpdate godoc +// @Summary Update a vehicle filter +// @Description Update a vehicle filter +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param id path string true "CAN ID" +// @Param config body VehicleFilterRequest true "Vehicle 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 /vehicle/{vin}/filter/{id} [put] +func HandleVehicleFilterUpdate(w http.ResponseWriter, r *http.Request) { + vehicleFilterUpdate.Handle(w, r) +} + +var vehicleFilterUpdate = controllers.NewMongoUpdate(&vehicleFilterUpdateHelper{}) + +type vehicleFilterUpdateHelper struct { + vehicleFiltersHelper +} + +func (h *vehicleFilterUpdateHelper) ParseUpdateURLParams(r *http.Request) interface{} { + req := &VehicleFilterParams{} + + params := httprouter.ParamsFromContext(r.Context()) + req.VIN = params.ByName("vin") + req.CANID = params.ByName("id") + + return req +} + +func (h *vehicleFilterUpdateHelper) ValidateFields(model interface{}) error { + p, ok := model.(*VehicleFilterParams) + if !ok { + return nil + } + + if p.VIN == "" || p.CANID == "" { + return ErrInvalidURLParams + } + return nil +} + +func (h *vehicleFilterUpdateHelper) 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 *vehicleFilterUpdateHelper) QueryUpdate(filter interface{}, model interface{}) error { + client, err := services.GetMongoClient() + if err != nil { + return err + } + + f := filter.(*VehicleFilterParams) + if err = client.GetVehicles().UpdateFilterForVehicle(f.VIN, f.CANID, model.(*common.CANFilter)); err != nil { + return err + } + + return ResetVehiclesConfigCache([]string{f.VIN}) +} diff --git a/services/ota_update_go/handlers/vehicle_filter_update_test.go b/services/ota_update_go/handlers/vehicle_filter_update_test.go new file mode 100644 index 0000000..01f2103 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_filter_update_test.go @@ -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 TestVehicleFilterUpdate(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: "Invalid data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle/2D4FV48T95H646760/filter/123", + common.CANFilter{Interval: elptr.ElPtr(0)}), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`, + }, + { + Name: "Invalid data with can id", + Request: th.MakeTestRequest(http.MethodPost, "http://example.com/vehicle/2D4FV48T95H646760/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/vehicle/2D4FV48T95H646760/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"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle/2D4FV48T95H646760/filter/123", + common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"can_id":"123","interval":100}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleFilterUpdate, "/vehicle/:vin/filter/:id") +} diff --git a/services/ota_update_go/handlers/vehicle_fleet_get_list.go b/services/ota_update_go/handlers/vehicle_fleet_get_list.go new file mode 100644 index 0000000..b93ebde --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_fleet_get_list.go @@ -0,0 +1,85 @@ +package handlers + +import ( + "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" + "net/http" + "otaupdate/controllers" + "otaupdate/services" +) + +// HandleVehicleFleetGetList godoc +// @Summary Get fleets of car +// @Description Get fleets of a car +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param search query string true "FLEET_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 /vehicle/{vin}/fleets [get] +func HandleVehicleFleetGetList(w http.ResponseWriter, r *http.Request) { + vehicleFleetList.Handle(w, r) +} + +var vehicleFleetList = controllers.NewMongoGetList(&vehicleFleetListModelHelper{}) + +type vehicleFleetListModelHelper struct{} + +func (v vehicleFleetListModelHelper) NewModel() interface{} { + return &mongo.Vehicle{} +} + +func (v vehicleFleetListModelHelper) ParseGetListURLParams(r *http.Request, model interface{}) { + filter := model.(*mongo.Vehicle) + + params := httprouter.ParamsFromContext(r.Context()) + filter.VIN = params.ByName("vin") +} + +func (v vehicleFleetListModelHelper) ParseGetListQueryParams(r *http.Request, model interface{}) { + filter := model.(*mongo.Vehicle) + + filter.SetSearchQuery(r.URL.Query().Get("search")) +} + +func (v vehicleFleetListModelHelper) ValidateStruct(model interface{}) error { + result := model.(*mongo.Vehicle) + + err := validator.ValidateField(result.VIN, "required,vin") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} + +func (v vehicleFleetListModelHelper) QueryCount(filter interface{}) (int64, error) { + f := filter.(*mongo.Vehicle) + + client, err := services.GetMongoClient() + if err != nil { + return 0, err + } + + return client.GetVehicles().GetFleetCountForVehicle(f.VIN, f.SearchQuery()) +} + +func (v vehicleFleetListModelHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) { + f := filter.(*mongo.Vehicle) + + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + return client.GetVehicles().GetFleetsForVehicle(f.VIN, f.SearchQuery(), options) +} diff --git a/services/ota_update_go/handlers/vehicle_fleet_get_list_test.go b/services/ota_update_go/handlers/vehicle_fleet_get_list_test.go new file mode 100644 index 0000000..3bd6011 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_fleet_get_list_test.go @@ -0,0 +1,44 @@ +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 TestVehicleFleetGetList(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mockMongo := mongo.NewVehiclesCollection( + &mongo.MockCollection{ + AggregateObject: []struct { + Fleets []string `bson:"fleets"` + }{{Fleets: []string{"fleet1", "fleet2"}}}}, + ) + client.SetVehicles(mockMongo) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid vin parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/TESTVIN123/fleets", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/2D4FV48T95H646760/fleets", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":["fleet1","fleet2"]}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleFleetGetList, "/vehicle/:vin/fleets") +} diff --git a/services/ota_update_go/handlers/vehicle_get.go b/services/ota_update_go/handlers/vehicle_get.go new file mode 100644 index 0000000..1436e87 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_get.go @@ -0,0 +1,119 @@ +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/envtool" + "github.com/fiskerinc/cloud-services/pkg/validator" + + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" + mo "go.mongodb.org/mongo-driver/mongo" +) + +var logLevel = envtool.GetEnv("TREX_LOG_LEVEL", common.CriticalLabel) + +// HandlevehicleGet godoc +// @Summary Get vehicle +// @Description Get vehicle +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Success 200 {object} mongo.Vehicle +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle/{vin} [get] +func HandleVehicleGet(w http.ResponseWriter, r *http.Request) { + vehicleGet.Handle(w, r) +} + +var vehicleGet = controllers.NewMongoGetModel(&vehicleGetModelHelper{}) + +type vehicleGetModelHelper struct{} + +func (h *vehicleGetModelHelper) ValidatePK(model interface{}) error { + result := model.(*mongo.Vehicle) + + err := validator.ValidateField(result.VIN, "required,vin") + if err != nil { + return err + } + + return nil +} + +func (h *vehicleGetModelHelper) ParseGetURLParams(r *http.Request) interface{} { + req := &mongo.Vehicle{} + + params := httprouter.ParamsFromContext(r.Context()) + req.VIN = params.ByName("vin") + + return req +} + +func (h *vehicleGetModelHelper) ValidateFields(model interface{}) error { + result := model.(*mongo.Fleet) + + err := validator.ValidateField(result.Name, "required,fleet") + if err != nil { + return controllers.ErrorPKRequired + } + + return nil +} + +func (h *vehicleGetModelHelper) Query(filter interface{}) (interface{}, error) { + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + v := filter.(*mongo.Vehicle) + + c, err := services.GetDB().GetCars().SelectByVIN(v.VIN) + if err != nil { + return nil, err + } + + data, err := client.GetVehicles().FindVehicle(v) + + if err != nil && errors.Cause(err) == mo.ErrNoDocuments { + data, err = h.createDefault(v, err) + if err != nil { + return nil, err + } + } + + return &VehicleCar{ + VIN: v.VIN, + Car: c, + Vehicle: data, + }, nil +} + +func (h *vehicleGetModelHelper) createDefault(vehicle *mongo.Vehicle, err error) (*mongo.Vehicle, error) { + client, err := services.GetMongoClient() + if err != nil { + return nil, err + } + + vehicle.CANBus = common.CANBus{Enabled: true} + vehicle.LogLevel = common.UnmarshalLogLevelString(logLevel) + err = client.GetVehicles().AddVehicle(vehicle) + + return vehicle, err +} + +type VehicleCar struct { + VIN string `json:"vin"` + *common.Car + *mongo.Vehicle +} diff --git a/services/ota_update_go/handlers/vehicle_get_test.go b/services/ota_update_go/handlers/vehicle_get_test.go new file mode 100644 index 0000000..ef4972c --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_get_test.go @@ -0,0 +1,40 @@ +package handlers_test + +import ( + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + "github.com/fiskerinc/cloud-services/pkg/mongo" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestVehicleGet(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + services.GetDB().SetCars(&mocks.MockCars{}) + client.SetVehicles(mongo.NewVehiclesCollection(&mongo.MockCollection{})) + + tests := []th.BasicHttpTest{ + { + Name: "Zero id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/0", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"vin '0' invalid","error":"Bad Request"}`, + }, + { + Name: "Good id", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/1G1FP87S3GN100062", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","log_level":"trace","dlt_enabled":false,"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"fleets":null}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehicleGet, "/vehicle/:vin") +} diff --git a/services/ota_update_go/handlers/vehicle_path.go b/services/ota_update_go/handlers/vehicle_path.go new file mode 100644 index 0000000..fcf36ac --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_path.go @@ -0,0 +1,170 @@ +package handlers + +import ( + "fmt" + "io" + "math" + "net/http" + "otaupdate/services" + "strings" + + "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/gorilla/schema" + "github.com/intel-go/fastjson" + "github.com/pkg/errors" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehiclePathsPost godoc +// @Summary Gets paths of vehicles +// @Description Returns vehicle paths +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vins body vehicleBodyParams true "List of VINs" +// @Param lookback_hours query int false "Data lookback window in hours. Default is 24 if not set" +// @Success 200 {object} common.GpsPaths +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle_paths [post] +func HandleVehiclePathsPost(w http.ResponseWriter, r *http.Request) { + query, body, err := parseVehiclePathsParams(r) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + conn, err := services.GetClickhouseClient() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + logger.Error().Err(err).Msg("cannot get clickhouse client") + return + } + + paths, err := selectVehiclePaths(conn, body.VINs, query.LookbackHours) + + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + logger.Error().Err(err).Msg("failed to select vehicle paths from feature_table") + return + } + + utils.RespJSON(w, http.StatusOK, paths) +} + +func parseVehiclePathsParams(r *http.Request) (vehiclePathQuery, vehicleBodyParams, error) { + sch := schema.NewDecoder() + sch.SetAliasTag("json") + queryParams := vehiclePathQuery{} + bodyParams := vehicleBodyParams{} + + //process query params + err := sch.Decode(&queryParams, r.URL.Query()) + if err != nil { + return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) + } + + err = validator.GetValidator().Struct(queryParams) + if err != nil { + return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) + } + + //process body params + err = fastjson.NewDecoder(r.Body).Decode(&bodyParams) + + if err != nil && err != io.EOF { + return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) + } + + err = validator.GetValidator().Struct(bodyParams) + if err != nil { + return vehiclePathQuery{}, vehicleBodyParams{}, errors.WithStack(err) + } + + //set default value for queryParams + if queryParams.LookbackHours == 0 { + queryParams.LookbackHours = 24 + } + + return queryParams, bodyParams, nil +} + +func selectVehiclePaths(conn clickhouse.ClientInterface, vinList []string, lookbackHours int64) (common.GpsPaths, error) { + var vinListSb strings.Builder + //serialize vinList for query + vinListSb.WriteRune('[') + for i, vin := range vinList { + vinListSb.WriteRune('\'') + vinListSb.WriteString(vin) + vinListSb.WriteRune('\'') + + if i < len(vinList)-1 { + vinListSb.WriteRune(',') + } + } + vinListSb.WriteRune(']') + + //query gps routes + queryText := fmt.Sprintf("SELECT VIN, TBOX_GPSLati, TBOX_GPSLongi FROM feature_table WHERE Timestamp > now() - (interval %d hour) AND VIN in %s ORDER BY Timestamp ASC", + lookbackHours, vinListSb.String()) + + logger.Info().Msgf("query: %s", queryText) + + var results []ChGpsRow + err := conn.Select(&results, queryText) + if err != nil { + return nil, err + } + + //transform query results + var pathsMap common.GpsPaths = make(common.GpsPaths, len(vinList)) + + //populate pathsMap + for _, vin := range vinList { + pathsMap[vin] = common.GpsPath{} + } + + for _, row := range results { + _, ok := pathsMap[row.VIN] + if !ok { + pathsMap[row.VIN] = common.GpsPath{} + } + + //ignore invalid GPS points + if math.IsNaN(row.TBOX_GPSLati) || math.IsNaN(row.TBOX_GPSLongi) { + continue + } + if row.TBOX_GPSLati == 0 && row.TBOX_GPSLongi == 0 { + continue + } + if row.TBOX_GPSLati > 90 || row.TBOX_GPSLati < -90 { + continue + } + if row.TBOX_GPSLongi > 180 || row.TBOX_GPSLongi < -180 { + continue + } + + pathsMap[row.VIN] = append(pathsMap[row.VIN], + common.GpsPoint{row.TBOX_GPSLati, row.TBOX_GPSLongi}, + ) + } + + return pathsMap, nil +} + +type ChGpsRow struct { + VIN string `ch:"VIN" json:"VIN"` + TBOX_GPSLati float64 `ch:"TBOX_GPSLati" json:"TBOX_GPSLati"` + TBOX_GPSLongi float64 `ch:"TBOX_GPSLongi" json:"TBOX_GPSLongi"` +} + +type vehiclePathQuery struct { + LookbackHours int64 `json:"lookback_hours"` +} + +type vehicleBodyParams struct { + VINs []string `json:"vins" validate:"required"` +} diff --git a/services/ota_update_go/handlers/vehicle_path_test.go b/services/ota_update_go/handlers/vehicle_path_test.go new file mode 100644 index 0000000..e3d929d --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_path_test.go @@ -0,0 +1,76 @@ +package handlers_test + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" +) + +var chExpectedResult = []handlers.ChGpsRow{ + {"1G1FP87S1GN000445", 33.901499, -118.379155}, + {"1G1FP87S1GN000445", 33.101599, -118.189155}, + {"1G1FP87S1GN000446", 50.231084, 11.344868}, + {"1G1FP87S1GN000446", 50.141084, 11.145868}, +} + +var expectedPathBody string = "" + + "{" + + `"1G1FP87S1GN000445":[[33.901499,-118.379155],[33.101599,-118.189155]],` + + `"1G1FP87S1GN000446":[[50.231084,11.344868],[50.141084,11.145868]]` + + "}" + +func TestHandleVehiclePathPost(t *testing.T) { + validQuery := "?lookback_hours=24" + validBody := "{\"vins\":[\"1G1FP87S1GN000445\", \"1G1FP87S1GN000446\"]}" + validVins := `["1G1FP87S1GN000445","1G1FP87S1GN000446"]` + tests := map[string]struct { + q string + b string + conn clickhouse.ConnInterface + expStatus int + expBody string + }{ + "correct": { + q: validQuery, + b: validBody, + conn: &clickhouse.MockConn{ExpectedResult: chExpectedResult}, + expStatus: http.StatusOK, + expBody: expectedPathBody, + }, + "missing_vins": { + q: "", + b: "", + expStatus: http.StatusBadRequest, + expBody: `{"message":"VINs required","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: "vins", Value: validVins}, + } + ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p) + + reader := bytes.NewReader([]byte(tt.b)) + + r := httptest.NewRequest(http.MethodGet, "http://example.com/ota_update/vehicle_paths"+tt.q, reader). + WithContext(ctx) + + handlers.HandleVehiclePathsPost(w, r) + assert.Equal(t, tt.expStatus, w.Code) + assert.Equal(t, tt.expBody, w.Body.String()) + }) + } +} diff --git a/services/ota_update_go/handlers/vehicle_state.go b/services/ota_update_go/handlers/vehicle_state.go new file mode 100644 index 0000000..697d997 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_state.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehicleState godoc +// @Summary Get state of car +// @Description Returns the state of the car derived from CAN bus messages. +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin query string false "Car vin" +// @Success 200 {object} JSONCarStateMessage +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /carstate [get] +func HandleVehicleState(w http.ResponseWriter, r *http.Request) { + vin := r.URL.Query().Get("vin") + if vin == "" { + loggerdataresp.BadDataErrorResp(w, ErrMissingVIN, http.StatusBadRequest) + return + } + ok := validator.ValidateVINSimple(vin) + if !ok { + loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest) + return + } + + clientPool := services.RedisClientPool() + parser := cache.NewVehicleState(clientPool) + state, err := parser.Get(vin) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + utils.RespJSON(w, http.StatusOK, &JSONCarStateMessage{ + Data: state, + }) +} + +type JSONCarStateMessage struct { + Data common.CarState `json:"data"` +} diff --git a/services/ota_update_go/handlers/vehicle_state_multi.go b/services/ota_update_go/handlers/vehicle_state_multi.go new file mode 100644 index 0000000..deff5f0 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_state_multi.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/utils" +) + +// HandleVehicleStateMulti godoc +// @Summary Get state of a list of cars +// @Description Returns the states from a set of vins. Some vins may be missing in redis, or be invalid, but the other vins will still be successfully returned. Broken vin will be in the error list +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body []string true "List of vins" +// @Success 200 {object} JSONCarStateMultiMessage +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /carstate_multi [post] +func HandleVehicleStateMulti(w http.ResponseWriter, r *http.Request) { + var vins []string + + err := json.NewDecoder(r.Body).Decode(&vins) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + clientPool := services.RedisClientPool() + twins, errorList := cache.GetVINListDigitalTwin(vins, clientPool) + + utils.RespJSON(w, http.StatusOK, &JSONCarStateMultiMessage{ + Data: twins, + ErrorList: ErrorListToString(errorList), + }) +} + +// There is no swagger doc conversion for the error type, so just converting to a string first +type JSONCarStateMultiMessage struct { + Data map[string]common.CarState `json:"data"` + ErrorList []string +} + + +func ErrorListToString(list []error)(res []string){ + for _, err := range list { + res = append(res, err.Error()) + } + return +} \ No newline at end of file diff --git a/services/ota_update_go/handlers/vehicle_state_test.go b/services/ota_update_go/handlers/vehicle_state_test.go new file mode 100644 index 0000000..f9d2c61 --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_state_test.go @@ -0,0 +1,80 @@ +package handlers_test + +import ( + "encoding/json" + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + "time" + + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleVehicleState(t *testing.T) { + timeSerialized, _ := json.Marshal(time.Time{}) + client := tester.NewRedisMock() + client.SISMEMBEResults = map[string]map[string]interface{}{ + redis.CarSessionsKey(): {"1F15K3R45N1234567": int64(0)}, + redis.HMISessionsKey(): {"1F15K3R45N1234567": int64(0)}, + } + client.HGETALLResults = map[string][]interface{}{ + redis.CarStateHashKey("1F15K3R45N1234567"): { + []byte("location"), []byte(`{}`), + []byte("battery"), []byte(`{}`), + []byte("doors"), []byte(`{}`), + []byte("windows"), []byte(`{}`), + []byte("updated"), timeSerialized, + }, + } + services.SetRedisClientPool(tester.NewMockClientPool(client)) + + tests := []th.BasicHttpTest{ + { + Name: "Good request", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carstate?vin=1F15K3R45N1234567", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":{"online":false,"online_hmi":false}}`, + }, + { + Name: "No VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carstate", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"missing VIN","error":"Bad Request"}`, + }, + { + Name: "Invalid VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carstate?vin=TESTVIN123", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"invalid VIN","error":"Bad Request"}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleVehicleState) +} + +func TestHandleVehicleStateEmpty(t *testing.T) { + vin := "1F15K3R45N1234567" + client := tester.NewRedisMock() + client.SISMEMBEResults = map[string]map[string]interface{}{ + redis.CarSessionsKey(): {vin: int64(0)}, + redis.HMISessionsKey(): {vin: int64(0)}, + } + client.HGETALLResults = map[string][]interface{}{ + redis.CarStateHashKey(vin): {}, + } + services.SetRedisClientPool(tester.NewMockClientPool(client)) + + tests := []th.BasicHttpTest{ + { + Name: "Good request", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carstate?vin=1F15K3R45N1234567", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":{"online":false,"online_hmi":false}}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleVehicleState) +} diff --git a/services/ota_update_go/handlers/vehicle_update.go b/services/ota_update_go/handlers/vehicle_update.go new file mode 100644 index 0000000..8943fed --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_update.go @@ -0,0 +1,114 @@ +package handlers + +import ( + "net/http" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + "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/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +// HandleVehicleUpdate godoc +// @Summary Modify vehicle +// @Description Modify vehicle requires vehicle id +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param car body common.UpdateCarRequest true "vehicle data" +// @Success 200 {object} common.UpdateCarRequest +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicle/{vin} [put] +func HandleVehicleUpdate(w http.ResponseWriter, r *http.Request) { + var ucr common.UpdateCarRequest + err := httphandlers.ParseRequest(r, &ucr) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + c := &common.Car{ + VIN: ucr.VIN, + ICCID: ucr.ICCID, + Year: ucr.Year, + Model: ucr.Model, + Trim: ucr.Trim, + Country: ucr.Country, + Powertrain: ucr.Powertrain, + Restraint: ucr.Restraint, + BodyType: ucr.BodyType, + Tags: ucr.Tags, + SUMSVersion: ucr.SUMSVersion, + } + + _, err = services.GetDB().GetCars().Update(c) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + client, err := services.GetMongoClient() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + v := &mongo.Vehicle{ + VIN: ucr.VIN, + LogLevel: ucr.LogLevel, + DebugMask: ucr.DebugMask, + DLTEnabled: ucr.DLTEnabled, + DLTLevel: ucr.DLTLevel, + IDPSEnabled: ucr.IDPSEnabled, + } + if ucr.CANBus != nil { + if ucr.CANBus.DTCEnabled == nil { + ucr.CANBus.DTCEnabled = elptr.ElPtr(true) + } + v.CANBus = *ucr.CANBus + } + + err = client.GetVehicles().UpdateVehicle(v) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.MongoUpdateErrorCheck) { + return + } + + batch := redis.NewRedisBatchCommands() + batch.Add("DEL", redis.CarConfigKey(ucr.VIN)) + + if ucr.CANBus != nil { + data := common.TRexConfigResponse{ + LogLevel: ucr.LogLevel, + CANBus: *ucr.CANBus, + } + if cache.ENABLE_DEBUG_MASK { + data.DebugMask = ucr.DebugMask + } + data.DLTEnabled = ucr.DLTEnabled + data.DLTLevel = ucr.DLTLevel + + err = batch.AddPublish(common.TRex.Key(ucr.VIN), common.Message{ + Handler: "config", + Data: data, + }) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + conn := services.RedisClientPool().GetFromPool() + defer conn.Close() + _, err = conn.ExecuteBatch(batch) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + utils.RespJSON(w, http.StatusOK, &ucr) +} diff --git a/services/ota_update_go/handlers/vehicle_update_test.go b/services/ota_update_go/handlers/vehicle_update_test.go new file mode 100644 index 0000000..56a0d1b --- /dev/null +++ b/services/ota_update_go/handlers/vehicle_update_test.go @@ -0,0 +1,205 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/cache" + m "github.com/fiskerinc/cloud-services/pkg/common" + dbtc "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester" + "github.com/fiskerinc/cloud-services/pkg/mongo" + "github.com/fiskerinc/cloud-services/pkg/redis" + rtc "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + tr "github.com/fiskerinc/cloud-services/pkg/testrunner" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +var schemaToTRex = "file://" + th.GetSchemaDirPath() + "/trex/RXMessage.json" + +func TestVehicleUpdate(t *testing.T) { + client, err := services.GetMongoClient() + if err != nil { + t.Error(err) + return + } + mockMongo := mongo.NewVehiclesCollection(&mongo.MockCollection{}) + client.SetVehicles(mockMongo) + mockRedis := rtc.MockRedis{} + services.SetRedisClientPool(rtc.NewMockClientPool(&mockRedis)) + mockDB := dbtc.MockCars{} + services.GetDB().SetCars(&mockDB) + vin := "1G1FP87S3GN100062" + trexKey := m.TRex.Key(vin) + cacheKey := redis.CarConfigKey(vin) + + tests := []tr.TestCase{ + { + Name: "Good data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.UpdateCarRequest{ + VIN: vin, + Model: "Ocean", + Year: 2021, + Trim: "Basic", + Country: "Germany", + Powertrain: "test pt", + Restraint: "US Spec", + BodyType: "test bt", + CANBus: &m.CANBus{ + Enabled: true, + DataLogger: false, + // DTCEnabled: elptr.ElPtr(true), + }, + IDPSEnabled: true, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","year":2021,"model":"Ocean","trim":"Basic","country":"Germany","powertrain":"test pt","restraint":"US Spec","body_type":"test bt","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true},"idps_enabled":true}`, + }, + RedisTestCase: &rtc.RedisTestCase{ + ExpectedMessages: map[string]string{ + trexKey: `{"data":{"canbus":{"data_logger_enabled":false,"dtc_enabled":true,"enabled":true},"log_level":"trace"},"handler":"config"}`, + }, + ExpectedCaches: map[string]rtc.ExpiringCacheResult{ + cacheKey: {Value: "DELETED"}, + }, + }, + }, + { + Name: "Good data with debug mask", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.UpdateCarRequest{ + VIN: vin, + Model: "Ocean", + Year: 2021, + Trim: "Basic", + Country: "Germany", + Powertrain: "test pt", + Restraint: "US Spec", + BodyType: "test bt", + CANBus: &m.CANBus{ + Enabled: true, + DataLogger: false, + DTCEnabled: elptr.ElPtr(false), + }, + DebugMask: "E", + IDPSEnabled: true, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","year":2021,"model":"Ocean","trim":"Basic","country":"Germany","powertrain":"test pt","restraint":"US Spec","body_type":"test bt","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":true,"debug_mask":"E"}`, + }, + RedisTestCase: &rtc.RedisTestCase{ + ExpectedMessages: map[string]string{ + trexKey: `{"data":{"canbus":{"data_logger_enabled":false,"dtc_enabled":false,"enabled":true},"debug_mask":"E","log_level":"trace"},"handler":"config"}`, + }, + ExpectedCaches: map[string]rtc.ExpiringCacheResult{ + cacheKey: {Value: "DELETED"}, + }, + }, + }, + { + Name: "No CANBus", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.UpdateCarRequest{ + VIN: vin, + Model: "Ocean", + Year: 2021, + Trim: "Basic", + IDPSEnabled: true, + }), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"vin":"1G1FP87S3GN100062","year":2021,"model":"Ocean","trim":"Basic","idps_enabled":true}`, + }, + RedisTestCase: &rtc.RedisTestCase{ + ExpectedMessages: map[string]string{}, + ExpectedCaches: map[string]rtc.ExpiringCacheResult{ + cacheKey: {Value: "DELETED"}, + }, + }, + }, + { + Name: "No data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required. Year required. Model required. Trim required","error":"Bad Request"}`, + }, + }, + { + Name: "No VIN", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.Car{ + Model: "Ocean", + Year: 2021, + Trim: "Basic", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN required","error":"Bad Request"}`, + }, + }, + { + Name: "Bad data", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.Car{ + VIN: "XXXXXXXXXXXXXXXXXXXXX", + Model: "Ocean", + Year: 2021, + Trim: "Basic", + }), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN 'XXXXXXXXXXXXXXXXXXXXX' invalid","error":"Bad Request"}`, + }, + }, + { + Name: "Error", + HttpTestCase: &htc.HttpTestCase{ + Request: th.MakeTestRequest(http.MethodPut, "http://example.com/vehicle", m.Car{ + VIN: "1G1FP87S3GN100062", + Model: "Ocean", + Year: 2021, + Trim: "Basic", + }), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + }, + DBTestCase: &dbtc.DBTestCase{ + MockError: fmt.Errorf("something went wrong"), + }, + }, + } + + schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex) + for _, test := range tests { + mockRedis.Reset() + + if test.DBTestCase != nil { + test.DBTestCase.SetupDB(&mockDB) + } + if test.RedisTestCase != nil { + test.RedisTestCase.SetupRedis(&mockRedis) + } + + if test.HttpTestCase != nil { + w := test.HttpTestCase.Test(handlers.HandleVehicleUpdate) + 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) + // By default in all environments, ENABLE_DEBUG_MASK is true + // so we set that, otherwise unit tests fail. + t.Setenv(cache.ENABLE_DBG_MASK_EV_NAME, cache.ENABLE_DBG_MASK_VAL_TRUE) + cache.ENABLE_DEBUG_MASK = cache.DbgMaskEnabled() + for _, mes := range test.RedisTestCase.ExpectedMessages { + schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes)) + } + } + } +} diff --git a/services/ota_update_go/handlers/vehiclecommand_immobilizer.go b/services/ota_update_go/handlers/vehiclecommand_immobilizer.go new file mode 100644 index 0000000..8bc6506 --- /dev/null +++ b/services/ota_update_go/handlers/vehiclecommand_immobilizer.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "otaupdate/background" + "otaupdate/services" + + "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/utils" +) + +// HandleVehicleImmobilizerList godoc +// @Summary List vehicles in immobilizer list +// @Description +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} map[string]background.CarTrack +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehiclecommand/immobilize [get] +func HandleVehicleImmobilizerList(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(background.GetImmobilizer().GetVINInformation()) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) { + logger.Err(err).Msg("failed to encode immobilizer vin information") + return + } +} + +// HandleVehicleImmobilizerAdd godoc +// @Summary Add vehicles to immobilizer list +// @Description +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body HandleImmobilizerBody true "List of VINs to add to immobilizer" +// @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 /vehiclecommand/immobilize [post] +func HandleVehicleImmobilizerAdd(w http.ResponseWriter, r *http.Request) { + body := HandleImmobilizerBody{} + err := json.NewDecoder(r.Body).Decode(&body) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + go func(body *HandleImmobilizerBody, clientID string) { + alDB := services.GetDB().GetActionLog() + var description string + if body.Immobilize { + description = "immobilizing command" + }else { + description = "mobilizing command" + } + for _, v := range body.VINs { + actionLog := actionlogger.ActionLog{ + VIN: v, + Action: actionlogger.RemoteCommand, + UserIdentifier: clientID, + CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/vehiclecommand_immobilizer.go", + Description: description, + } + err = alDB.Insert(actionLog) + if err != nil { + logger.Err(err).Msg("failed to insert action log inside HandleVehicleCommand") + } + } + + }(&body, httphandlers.GetClientID(r)) + background.GetImmobilizer().AddVINs(body.VINs, body.Immobilize) + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "OK", + }) +} + +// HandleVehicleImmobilizerDelete godoc +// @Summary Delete vehicles from immobilizer list +// @Description +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param data body []string true "List of VINs to remove from immobilizer" +// @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 /vehiclecommand/immobilize [delete] +func HandleVehicleImmobilizerDelete(w http.ResponseWriter, r *http.Request) { + body := []string{} + err := json.NewDecoder(r.Body).Decode(&body) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + background.GetImmobilizer().RemoveVINs(body) + utils.RespJSON(w, http.StatusOK, common.JSONMessage{ + Message: "OK", + }) +} + +type HandleImmobilizerBody struct { + VINs []string `json:"vins"` + Immobilize bool `json:"immobilize"` // True: Immobilize cars, False: Mobilize the car +} diff --git a/services/ota_update_go/handlers/vehiclemodels_get.go b/services/ota_update_go/handlers/vehiclemodels_get.go new file mode 100644 index 0000000..9f12e87 --- /dev/null +++ b/services/ota_update_go/handlers/vehiclemodels_get.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "net/http" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehicleModels godoc +// @Summary Returns vehicle models +// @Description Returns vehicle models +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} JSONModelsResult +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehiclemodels [get] +func HandleVehicleModels(w http.ResponseWriter, r *http.Request) { + c := services.GetDB().GetCars() + + models, err := c.GetModels() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, JSONModelsResult{ + Data: models, + }) +} + +type JSONModelsResult struct { + Data []string `json:"data"` +} diff --git a/services/ota_update_go/handlers/vehiclemodels_get_test.go b/services/ota_update_go/handlers/vehiclemodels_get_test.go new file mode 100644 index 0000000..4b1bbc3 --- /dev/null +++ b/services/ota_update_go/handlers/vehiclemodels_get_test.go @@ -0,0 +1,37 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestGetVehicleModels(t *testing.T) { + mock := mo.MockCars{} + services.GetDB().SetCars(&mock) + tests := []mo.DBHttpTest{ + { + Name: "Call", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehiclemodels", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":["1G1FP87S3GN100062"]}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehiclemodels", nil), + 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.HandleVehicleModels, &mock) +} diff --git a/services/ota_update_go/handlers/vehicles_get.go b/services/ota_update_go/handlers/vehicles_get.go new file mode 100644 index 0000000..4a77ffb --- /dev/null +++ b/services/ota_update_go/handlers/vehicles_get.go @@ -0,0 +1,210 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/fiskerinc/cloud-services/pkg/cache" + re "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/gomodule/redigo/redis" + "github.com/pkg/errors" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + orm "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + "github.com/fiskerinc/cloud-services/pkg/utils/urlhelper" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/fiskerinc/cloud-services/pkg/utils/elptr" +) + +// HandleVehiclesGet godoc +// @Summary Search cars +// @Description Returns cars filtered by id, model, year, and vin +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param search query string false "Text search" +// @Param vins query []string false "csv of Car vin" +// @Param model query int false "Car model" +// @Param year query string false "Car year" +// @Param online query bool false "Car status" +// @Param online_hmi query bool false "HMI status" +// @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.Car} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicles [get] +func HandleVehiclesGet(w http.ResponseWriter, r *http.Request) { + var total int + c := services.GetDB().GetCars() + filter, err := parseCarsFilter(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 = "vin" + } + + err = fillOnline(filter) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + // Do not display EU vins in the list in NA prod UI + if isNAProd() { + filter.NoEU = true + } + + cars, err := c.Search(filter, options) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + if options.Offset == 0 && filter.VIN == "" { + total, err = c.SearchCount(filter) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: cars, + Total: total, + }) +} + +var vehiclesCache cache.VehicleCacher + +func SetVehiclesCache(c cache.VehicleCacher) { + vehiclesCache = c +} + +func GetVehiclesCache() cache.VehicleCacher { + if vehiclesCache == nil { + var err error + vehiclesCache, err = cache.NewVehiclesCache( + envtool.GetEnvDuration("VEHICLES_CACHE_TTL", 5*time.Minute), + envtool.GetEnvInt("VEHICLES_CACHE_LIMIT", 1000)) + if err != nil { + logger.Error().Err(err).Send() + } + } + + return vehiclesCache +} + +func fillOnline(filter *common.CarSearch) error { + if filter == nil || filter.Online == nil { + return nil + } + + var keys []string + if filter.Online.Online != nil && *filter.Online.Online { + keys = append(keys, re.CarSessionsKey()) + } + + if filter.Online.HMI != nil && *filter.Online.HMI { + keys = append(keys, re.HMISessionsKey()) + } + + if len(keys) == 0 { + return nil + } + + vehicles, err := getOnline(keys) + if err != nil { + return errors.WithMessagef(err, "get online vehicles") + } + + filter.Online.VINsOnline = vehicles + + return nil +} + +func getOnline(sessionKeys []string) ([]string, error) { + var onlineVehicles []string + redisCli := services.RedisClientPool().GetFromPool() + defer redisCli.Close() + + batch := re.NewRedisBatchCommands() + + for i := range sessionKeys { + batch.Add("SMEMBERS", sessionKeys[i]) + } + + vinOfVins, err := redis.Values(redisCli.ExecuteBatch(batch)) + if err != nil { + return nil, errors.WithMessagef(err, "redis get multi failed for keys: %v", sessionKeys) + } + + onlineVehicles = make([]string, 0) + + for _, rvins := range vinOfVins { + setVehicles, err := redis.Strings(rvins, nil) + if err != nil { + return nil, errors.WithMessagef(err, "redis cast to string failed: %v", sessionKeys) + } + + onlineVehicles = append(onlineVehicles, setVehicles...) + } + + return onlineVehicles, nil +} + +func parseCarsFilter(r *http.Request) (*common.CarSearch, error) { + qs := r.URL.Query() + + filter := common.CarSearch{ + Search: qs.Get("search"), + VINs: qs.Get("vins"), + Car: common.Car{ + VIN: qs.Get("vin"), + Model: qs.Get("model"), + Year: urlhelper.GetQueryInt(qs, "year"), + }, + } + + var o *common.CarOnlineFilter + + online, ok := urlhelper.GetQueryBool(qs, "online") + if ok { + if o == nil { + o = &common.CarOnlineFilter{} + } + + o.Online = elptr.ElPtr(online) + } + + onlineHMI, ok := urlhelper.GetQueryBool(qs, "online_hmi") + if ok { + if o == nil { + o = &common.CarOnlineFilter{} + } + + o.HMI = elptr.ElPtr(onlineHMI) + } + + filter.Online = o + + err := validator.ValidateNonRequired(filter) + + return &filter, err +} + +func isNAProd() bool { + return envtool.GetEnv("IS_NA_PROD", "") != "" +} diff --git a/services/ota_update_go/handlers/vehicles_get_test.go b/services/ota_update_go/handlers/vehicles_get_test.go new file mode 100644 index 0000000..575e6c0 --- /dev/null +++ b/services/ota_update_go/handlers/vehicles_get_test.go @@ -0,0 +1,843 @@ +package handlers_test + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/redis" + + "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" + r "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/utils/elptr" +) + +type MockCarsCounted struct { + mo.MockCars + searchCounter int + countCounter int +} + +func (m *MockCarsCounted) Search(s *m.CarSearch, q *orm.PageQueryOptions) ([]m.Car, error) { + m.searchCounter++ + return m.MockCars.Search(s, q) +} + +func (m *MockCarsCounted) SearchCount(s *m.CarSearch) (int, error) { + m.countCounter++ + return m.MockCars.SearchCount(s) +} + +func TestGetVehiclesCached(t *testing.T) { + r.MockRedisConnection() + mockRedis := rm.MockRedis{ + GetSetResults: "[]", + GetCommandResult: map[string]map[string]interface{}{ + "SMEMBERS": {redis.CarSessionsKey(): []interface{}{}}, + }, + } + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + mock := MockCarsCounted{} + services.GetDB().SetCars(&mock) + defaultOrder := "vin" + + tests := []mo.DBHttpTest{ + { + Name: "Default Limit 100", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: longListExpectedResultGen(longListData, 0, 0), + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: longListData[:100], + }, + }, + { + Name: "Cached Limit 100", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: longListExpectedResultGen(longListData, 0, 0), + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: longListData[:100], + }, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleVehiclesGet, &mock) + + if mock.searchCounter != 2 && mock.countCounter != 2 { + t.Errorf("Expected search and count to be called only once, got %d and %d", mock.searchCounter, mock.countCounter) + } +} + +func TestGetVehicles(t *testing.T) { + r.MockRedisConnection() + mockRedis := rm.MockRedis{ + GetSetResults: "[]", + GetCommandResult: map[string]map[string]interface{}{ + "SMEMBERS": {redis.CarSessionsKey(): []interface{}{}}, + }, + } + services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis)) + handlers.SetVehiclesCache(&rm.MockVehiclesCache{}) + mock := mo.MockCars{} + services.GetDB().SetCars(&mock) + expectedResp := `{"data":[{"vin":"3C4PDCBG0ET127145","year":2021,"model":"Ocean"}],"total":1}` + expectedRespNoTotal := `{"data":[{"vin":"3C4PDCBG0ET127145","year":2021,"model":"Ocean"}]}` + defaultOrder := "vin" + listData := []m.Car{ + { + VIN: "3C4PDCBG0ET127145", + Model: "Ocean", + Year: 2021, + }, + } + tests := []mo.DBHttpTest{ + { + Name: "No parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Default Limit 100", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: longListExpectedResultGen(longListData, 0, 0), + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: longListData[:100], + }, + }, + { + Name: "Wrong limit, -100", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?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/cars?limit=1000", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`, + }, + { + Name: "Invalid VIN", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?vin=FISKER123", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"VIN 'FISKER123' invalid","error":"Bad Request"}`, + }, + { + Name: "Id parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?vin=3C4PDCBG0ET127145", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{ + Car: m.Car{ + VIN: "3C4PDCBG0ET127145", + }, + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Name, version, description parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?model=Ocean&year=2021", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{ + Car: m.Car{ + Model: "Ocean", + Year: 2021, + }, + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Paging parameters", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?offset=10&limit=5", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedRespNoTotal, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 5, + Offset: 10, + }, + MockListResponse: listData, + }, + }, + { + // TODO: since the logic is on the db layer, we can't check the online/offline test here, + // it's whether we have to have a full mock database, or we have to expect the query to be + // equal to some string. + Name: "Online", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?online=true", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{ + Online: &m.CarOnlineFilter{ + Online: elptr.ElPtr(true), + }, + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Offline", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?online=false", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: expectedResp, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{ + Online: &m.CarOnlineFilter{ + Online: elptr.ElPtr(false), + }, + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: listData, + }, + }, + { + Name: "Multiple VINs", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars?vins=SCAZD42A3HCX11779,SCAZD42A3HCX11850", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"vin":"SCAZD42A3HCX11779","year":2021,"model":"Ocean"},{"vin":"SCAZD42A3HCX11850","year":2021,"model":"Ocean"}],"total":2}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{ + VINs: "SCAZD42A3HCX11779,SCAZD42A3HCX11850", + }, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockListResponse: longListData[:2], + }, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cars", nil), + ExpectedStatus: http.StatusServiceUnavailable, + ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`, + DBTestCase: mo.DBTestCase{ + ExpectedFilter: m.CarSearch{}, + ExpectedPage: &orm.PageQueryOptions{ + Order: defaultOrder, + Limit: 100, + Offset: 0, + }, + MockError: fmt.Errorf("something went wrong"), + }, + }, + } + + mo.RunDBTests(t, tests, handlers.HandleVehiclesGet, &mock) +} + +// 105 entries +var longListData []m.Car = []m.Car{ + { + VIN: "SCAZD42A3HCX11779", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11850", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16041", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12984", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11576", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15688", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17778", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10518", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17196", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17156", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13043", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13784", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19371", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19905", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX18835", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11056", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16812", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13636", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12874", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17360", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13373", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11933", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11286", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16703", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12358", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15157", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17005", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10283", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11247", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17714", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16219", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16036", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19125", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15079", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX18843", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14722", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16906", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11601", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14453", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11028", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17184", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12328", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13860", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15042", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17519", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10309", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10304", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17052", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19086", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15606", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19217", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16092", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17281", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13886", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19772", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15135", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17306", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16200", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12910", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10944", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10117", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16097", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13145", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19594", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10200", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14353", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11049", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10682", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14547", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14368", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12021", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17152", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19291", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11513", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13697", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX18883", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14454", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX16767", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14896", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13477", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14916", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13866", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17261", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15298", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17098", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19128", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX15130", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10156", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19665", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX10477", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX18530", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX18158", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14232", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13238", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17683", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11460", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX11272", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX14306", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX17241", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13865", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX12546", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19404", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19747", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX13450", + Model: "Ocean", + Year: 2021, + }, + { + VIN: "SCAZD42A3HCX19459", + Model: "Ocean", + Year: 2021, + }, +} + +type fakeResponse struct { + Data []m.Car `json:"data"` + Total int `json:"total"` +} + +func longListExpectedResultGen(carList []m.Car, offset, limit int) string { + max := len(carList) + if limit <= 0 { + limit = 100 + } + if offset+limit < max { + max = offset + limit + } + carSubset := carList[offset:max] + fakeRes := fakeResponse{Data: carSubset, Total: len(carSubset)} + + fmt.Println() + b, _ := json.Marshal(fakeRes) + return string(b) +} diff --git a/services/ota_update_go/handlers/vehicles_locations.go b/services/ota_update_go/handlers/vehicles_locations.go new file mode 100644 index 0000000..5484595 --- /dev/null +++ b/services/ota_update_go/handlers/vehicles_locations.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "net/http" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/redis" + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehiclesLocations godoc +// @Summary Get locations of cars +// @Description Returns car locations +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param order query string false "Sort on column with asc or desc" +// @Success 200 {object} JSONCarLocations +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /carslocations [get] +func HandleVehiclesLocations(w http.ResponseWriter, r *http.Request) { + conn := services.RedisClientPool().GetFromPool() + defer conn.Close() + + data, err := conn.GetObjectRaw(redis.CarLocationsKey()) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + locations := make([]JSONCarLocation, len(data)) + i := 0 + for key, value := range data { + var location common.Location + err = location.Unmarshal(value) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + locations[i] = JSONCarLocation{ + Location: common.Location{ + Altitude: location.Altitude, + Longitude: location.Longitude, + Latitude: location.Latitude, + }, + VIN: key, + } + i++ + } + + utils.RespJSON(w, http.StatusOK, JSONCarLocations{ + Data: locations, + Total: len(locations), + }) +} + +// JSONCarsResult wraps query in json +type JSONCarLocations struct { + Data []JSONCarLocation `json:"data"` + Total int `json:"total"` +} + +type JSONCarLocation struct { + common.Location + VIN string `json:"vin"` +} diff --git a/services/ota_update_go/handlers/vehicles_locations_test.go b/services/ota_update_go/handlers/vehicles_locations_test.go new file mode 100644 index 0000000..2fb0d4c --- /dev/null +++ b/services/ota_update_go/handlers/vehicles_locations_test.go @@ -0,0 +1,47 @@ +package handlers_test + +import ( + "net/http" + "otaupdate/handlers" + "otaupdate/services" + "testing" + + "github.com/fiskerinc/cloud-services/pkg/redis/tester" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestHandleVehiclesLocations(t *testing.T) { + conn := tester.NewRedisMock() + conn.GetObjectRawResults = map[string][]byte{ + "1F15K3R45N1234567": []byte(`{"altitude":5,"longitude":10,"latitude":15}`), + } + services.SetRedisClientPool(tester.NewMockClientPool(conn)) + + tests := []th.BasicHttpTest{ + { + Name: "Good request", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carslocations", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"altitude":5,"longitude":10,"latitude":15,"heading":0,"vin":"1F15K3R45N1234567"}],"total":1}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleVehiclesLocations) +} + +func TestHandleVehiclesLocationsEmpty(t *testing.T) { + conn := tester.NewRedisMock() + conn.GetObjectRawResults = map[string][]byte{} + services.SetRedisClientPool(tester.NewMockClientPool(conn)) + + tests := []th.BasicHttpTest{ + { + Name: "Good request", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carslocations", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[],"total":0}`, + }, + } + + th.RunBasicHttpTests(t, tests, handlers.HandleVehiclesLocations) +} diff --git a/services/ota_update_go/handlers/vehiclesignals_get.go b/services/ota_update_go/handlers/vehiclesignals_get.go new file mode 100644 index 0000000..570bb47 --- /dev/null +++ b/services/ota_update_go/handlers/vehiclesignals_get.go @@ -0,0 +1,83 @@ +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" + "github.com/fiskerinc/cloud-services/pkg/utils/urlhelper" + "github.com/fiskerinc/cloud-services/pkg/validator" + "github.com/julienschmidt/httprouter" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +const SqlCHSelectSignals = `SELECT Timestamp, Name, Value + FROM vehicle_signal_lv + WHERE + VIN=$1 and + Timestamp > $2 + ORDER BY Timestamp DESC LIMIT $3` + +// HandleVehiclesSignals godoc +// @Summary Get locations of cars +// @Description Returns car locations +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Param vin path string true "VIN" +// @Param limit query int false "Max number of records. Default is 20 if not set" +// @Param after_utc query int false "Time from which we want to see the can signals from" +// @Param after_timestamp query string false "Time from which we want to see the can signals from" +// @Success 200 {object} common.JSONDBQueryResult{data=[]common.ClickHouseSignal} +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /cansignals/{vin} [get] +func HandleVehiclesSignals(w http.ResponseWriter, r *http.Request) { + params := httprouter.ParamsFromContext(r.Context()) + vin := params.ByName("vin") + + err := validator.ValidateField(vin, "required,vin") + if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) { + return + } + + qs := r.URL.Query() + limit := urlhelper.GetQueryInt(qs, "limit") + if limit == 0 { + limit = 20 + } + + after := urlhelper.GetQueryUnix(qs, "after_utc") + a := urlhelper.GetQueryTimeStamp(qs, "after_timestamp") + if !a.IsZero() { + after = a + } + + conn, err := services.GetClickhouseConn() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + logger.Error().Err(err).Msg("cannot get clickhouse client") + + return + } + + var results []common.ClickHouseSignal + + err = conn.Select(context.Background(), &results, SqlCHSelectSignals, vin, after, limit) + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) { + return + } + + // If there are no results, Clickhouse returns nil slice + if len(results) == 0 { + results = []common.ClickHouseSignal{} + } + + utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{ + Data: results, + }) +} diff --git a/services/ota_update_go/handlers/vehiclesignals_get_test.go b/services/ota_update_go/handlers/vehiclesignals_get_test.go new file mode 100644 index 0000000..429e7f3 --- /dev/null +++ b/services/ota_update_go/handlers/vehiclesignals_get_test.go @@ -0,0 +1,103 @@ +package handlers_test + +import ( + "context" + "net/http" + "reflect" + "testing" + "time" + + "otaupdate/handlers" + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + "github.com/pkg/errors" +) + +type CHMockConn struct { + query string + args []interface{} + ret interface{} +} + +func (c *CHMockConn) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error { + if query != c.query { + return errors.Errorf("queries are not equal: %s != %s", query, c.query) + } + + if len(args) != len(c.args) { + return errors.Errorf("args are not of the same length, %d != %d", len(args), len(c.args)) + } + + for i := 0; i < len(args); i++ { + if !reflect.DeepEqual(args[i], c.args[i]) { + return errors.Errorf("args[i] != mock.args[i], %v, %v", args[i], c.args[i]) + } + } + + if reflect.TypeOf(dest) != reflect.TypeOf(c.ret) { + return errors.Errorf("type of dest is not equal to the needed return type, %v, %v", dest, c.ret) + } + + valOfPtr := c.ret.(*[]common.ClickHouseSignal) + + val := reflect.ValueOf(dest) + val.Elem().Set(reflect.ValueOf(*valOfPtr)) + + return nil +} + +func (c *CHMockConn) PrepareBatch(ctx context.Context, query string) (driver.Batch, error) { + return nil, nil +} + +func (c *CHMockConn) AsyncInsert(ctx context.Context, query string, wait bool) error { + return nil +} + +func (c *CHMockConn) QueryRow(ctx context.Context, query string, args ...interface{}) driver.Row { + return nil +} + +func (c *CHMockConn) Query(ctx context.Context, query string, args ...interface{}) (driver.Rows, error) { + return nil, nil +} + +func (c *CHMockConn) Exec(ctx context.Context, query string, args ...interface{}) error { + return nil +} + +func floatPointer(num float64) *float64 { + return &num +} + +func TestHandleVehicleSignals(t *testing.T) { + services.SetClickhouseConn(&CHMockConn{ + query: handlers.SqlCHSelectSignals, + args: []interface{}{"2D4FV48T95H646760", time.UnixMilli(0), 20}, + ret: &[]common.ClickHouseSignal{{ + Timestamp: time.Date(2022, 9, 1, 0, 0, 0, 0, time.UTC), + Name: "Signal", + Value: floatPointer(123), + }}, + }) + + tests := []th.BasicHttpTest{ + { + Name: "Invalid vin parameter", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cansignals/TESTVIN123", nil), + ExpectedStatus: http.StatusBadRequest, + ExpectedResponse: `{"message":"vin 'TESTVIN123' invalid","error":"Bad Request"}`, + }, + { + Name: "Valid data", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/cansignals/2D4FV48T95H646760", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[{"timestamp":"2022-09-01T00:00:00Z","name":"Signal","value":123}]}`, + }, + } + + th.RunParamHttpTests(t, tests, handlers.HandleVehiclesSignals, "/cansignals/:vin") +} diff --git a/services/ota_update_go/handlers/vehicleyears_get.go b/services/ota_update_go/handlers/vehicleyears_get.go new file mode 100644 index 0000000..3f5019d --- /dev/null +++ b/services/ota_update_go/handlers/vehicleyears_get.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "net/http" + + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/utils" + "github.com/fiskerinc/cloud-services/pkg/loggerdataresp" +) + +// HandleVehicleYears godoc +// @Summary Returns vehicle years +// @Description Returns vehicle years +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer " +// @Param Api-Key header string false "" +// @Success 200 {object} JSONYearsResult +// @Failure 400 {object} common.JSONError "Bad request" +// @Failure 401 {object} common.JSONError "Unauthorized" +// @Failure 503 {object} common.JSONError "Service unavailable" +// @Router /vehicleyears [get] +func HandleVehicleYears(w http.ResponseWriter, r *http.Request) { + c := services.GetDB().GetCars() + + years, err := c.GetYears() + if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) { + return + } + + utils.RespJSON(w, http.StatusOK, JSONYearsResult{ + Data: years, + }) +} + +type JSONYearsResult struct { + Data []int `json:"data"` +} diff --git a/services/ota_update_go/handlers/vehicleyears_get_test.go b/services/ota_update_go/handlers/vehicleyears_get_test.go new file mode 100644 index 0000000..cf8488a --- /dev/null +++ b/services/ota_update_go/handlers/vehicleyears_get_test.go @@ -0,0 +1,37 @@ +package handlers_test + +import ( + "fmt" + "net/http" + "testing" + + "otaupdate/handlers" + "otaupdate/services" + + mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks" + th "github.com/fiskerinc/cloud-services/pkg/testhelper" +) + +func TestGetVehicleYears(t *testing.T) { + mock := mo.MockCars{} + services.GetDB().SetCars(&mock) + tests := []mo.DBHttpTest{ + { + Name: "Call", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleyears", nil), + ExpectedStatus: http.StatusOK, + ExpectedResponse: `{"data":[3000]}`, + }, + { + Name: "Error", + Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicleyears", nil), + 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.HandleVehicleYears, &mock) +} diff --git a/services/ota_update_go/main.go b/services/ota_update_go/main.go new file mode 100644 index 0000000..dda8092 --- /dev/null +++ b/services/ota_update_go/main.go @@ -0,0 +1,324 @@ +package main + +import ( + "net/http" + + "otaupdate/controllers" + "otaupdate/handlers" + "otaupdate/services" + + a "github.com/fiskerinc/cloud-services/pkg/adminroles" + "github.com/fiskerinc/cloud-services/pkg/common/authproviders" + h "github.com/fiskerinc/cloud-services/pkg/httphandlers" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/tracer" + "github.com/fiskerinc/cloud-services/pkg/utils/app" + + "gopkg.in/DataDog/dd-trace-go.v1/contrib/julienschmidt/httprouter" +) + +var authChecker h.AuthCheckerInterface + +func init() { + app.Setup("ota update go", cleanup) + + handlers.InitSwaggerDoc() + + authChecker = &h.AuthAPIToken{ + APITokens: services.GetDB().GetAPITokens(), + APICalls: services.GetDB().GetAPICalls(), + JWTAuth: true, + } +} + +func main() { + defer cleanup() + + tracer.Start() + defer tracer.Stop() + + const port string = ":8077" + + // Enables profiling of application. Run `go get github.com/pkg/profile` for profiling package + // defer profile.Start(profile.MemProfile, profile.MemProfileRate(1), profile.ProfilePath(".")).Stop() + + swagger := h.GetSwaggerHandler() + router := httprouter.New() + router.PanicHandler = h.HttpRouterPanicHandler + readRoles := []a.RoleID{a.RoleCreate, a.RoleReadOnly} + createRole := []a.RoleID{a.RoleCreate} + deleteRole := []a.RoleID{a.RoleDelete} + supplierAdminRole := []a.RoleID{a.RoleSupplierApprover} + updateDeployRole := []a.RoleID{a.RoleUpdateDeploy} + + // this means only Fisker AD and API Key can access + permissionRead := a.RoleMap{ + authproviders.FiskerAD: readRoles, + authproviders.FiskerAPIKey: readRoles, + } + permissionCreate := a.RoleMap{ + authproviders.FiskerAD: createRole, + authproviders.FiskerAPIKey: createRole, + } + permissionDelete := a.RoleMap{ + authproviders.FiskerAD: deleteRole, + authproviders.FiskerAPIKey: deleteRole, + } + permissionSupplierAdmin := a.RoleMap{ + authproviders.FiskerAD: supplierAdminRole, + authproviders.FiskerAPIKey: supplierAdminRole, + } + permissionManufacture := a.RoleMap{ + authproviders.FiskerAPIKey: {a.RoleManufacture}, + } + permissionMigrate := a.RoleMap{ + authproviders.FiskerAD: {a.RoleManifestMigration}, + authproviders.FiskerAPIKey: {a.RoleManifestMigration}, + } + + permissionUpdateDeploy := a.RoleMap{ + authproviders.FiskerAD: updateDeployRole, + authproviders.FiskerAPIKey: updateDeployRole, + } + + // Change it to Magna as soon as you get groupIDs. + magnaAccess := a.RoleMap{ + authproviders.Magna: []a.RoleID{a.RoleMagna}, + authproviders.FiskerQA: []a.RoleID{a.RoleManufacture}, + } + + carDiagnostic := a.RoleMap{ + authproviders.Default: []a.RoleID{a.RoleCarDiagnostic}, + authproviders.FiskerAPIKey: []a.RoleID{a.RoleCarDiagnostic}, + } + + addHandler(router, nil, http.MethodGet, "/docs/*path", swagger) + + // ### APICALL ### + addHandler(router, permissionRead, http.MethodGet, "/apicalls", handlers.HandleAPICallsGet) + + // ### APITOKEN ### + addHandler(router, permissionRead, http.MethodGet, "/apitokens", handlers.HandleAPITokensGetList) + addHandler(router, permissionCreate, http.MethodPost, "/apitoken", handlers.HandleAPITokenAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/apitoken", handlers.HandleAPITokenDelete) + addHandler(router, permissionCreate, http.MethodPut, "/apitoken", handlers.HandleAPITokenUpdate) + + // ### CANSIGNAL ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/cansignals/:vin", handlers.HandleVehiclesSignals) + addHandler(router, permissionRead, http.MethodGet, "/can_signals_export", handlers.HandleCanSignalVINGet) + addHandler(router, permissionRead, http.MethodGet, "/can_signals_list", handlers.HandleCanSignalListGet) + addHandler(router, permissionRead, http.MethodGet, "/can_signals/:dbc", handlers.HandleDBCSignalsGetList) + + // ### CARCONFIG ### + addHandler(router, permissionCreate, http.MethodGet, "/car_config/:vin", handlers.GetCarConfiguration) + addHandler(router, permissionUpdateDeploy, http.MethodPost, "/car_config/:vin", handlers.CarConfigurationUpdate) + + // ### CARSCONNECTED ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodPost, "/carsconnected", handlers.HandleVehicleConnectionStatuses) + + // ### CARSLOCATION ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/carslocations", handlers.HandleVehiclesLocations) + + // ### CARSTATE ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/carstate", handlers.HandleVehicleState) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodPost, "/carstate_multi", handlers.HandleVehicleStateMulti) + + // ### CARUPDATE ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/carupdates", handlers.HandleCarUpdatesGet) + addHandler(router, permissionUpdateDeploy.CopyAndMerge(magnaAccess), http.MethodPost, "/carupdate", handlers.HandleCarUpdatesAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/carupdate", handlers.HandleCarUpdateDelete) + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPost, "/carupdate/:id/cancel", handlers.HandleCarUpdateCancel) + addHandler(router, carDiagnostic, http.MethodPost, "/carupdate/:id/vehicle-cancel", handlers.HandleCarUpdateVehicleCancel) + addHandler(router, permissionUpdateDeploy.CopyAndMerge(magnaAccess), http.MethodPost, "/carupdate/:id/deploy", handlers.HandleCarUpdateDeploy) + + // ### CARUPDATESLOG ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/carupdateslog", handlers.HandleCarUpdatesLog) + + // ### CARUPDATESTATUS ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/carupdatesstatuses", handlers.HandleCarUpdatesStatuses) + + // ### DASHBOARD ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/dashboard/guest-token", handlers.HandleDashboardToken) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/dashboard/embedded-dashboards", handlers.HandleSupersetEmbeddedDashboardsGet) + + // ### DITTO ### + addHandler(router, permissionRead, http.MethodGet, "/ditto/carstate", handlers.HandleDigitalTwinSignal) + + // ### DTC ### + addHandler(router, permissionRead, http.MethodGet, "/dtcs/:vin", handlers.HandleECUDTCGet) + + // ### ECUSTATS ### + addHandler(router, permissionRead, http.MethodGet, "/ecu_stats", handlers.HandleECUStatsGetList) + addHandler(router, permissionRead, http.MethodGet, "/ecu_stats/:vin/:dbc", handlers.HandleVINECUStatsGetList) + + // ### FLEET ### + addHandler(router, permissionRead, http.MethodGet, "/fleets", handlers.HandleFleetGetList) + addHandler(router, permissionCreate, http.MethodPost, "/fleet", handlers.HandleFleetAdd) + addHandler(router, permissionRead, http.MethodGet, "/fleet/:name", handlers.HandleFleetGet) + addHandler(router, permissionCreate, http.MethodPut, "/fleet/:name", handlers.HandleFleetUpdate) + addHandler(router, permissionDelete, http.MethodDelete, "/fleet/:name", handlers.HandleFleetDelete) + addHandler(router, permissionRead, http.MethodGet, "/fleet/:name/filters", handlers.HandleFleetFilterGetList) + addHandler(router, permissionCreate, http.MethodPost, "/fleet/:name/filter", handlers.HandleFleetFilterAdd) + addHandler(router, permissionCreate, http.MethodPut, "/fleet/:name/filter/:id", handlers.HandleFleetFilterUpdate) + addHandler(router, permissionDelete, http.MethodDelete, "/fleet/:name/filter/:id", handlers.HandleFleetFilterDelete) + addHandler(router, permissionRead, http.MethodGet, "/fleet/:name/vehicles", handlers.HandleFleetVehicleGetList) + addHandler(router, permissionRead, http.MethodPost, "/fleet/:name/vehicles/add", handlers.HandleFleetVehicleAdd) + addHandler(router, permissionRead, http.MethodPost, "/fleet/:name/vehicles/delete", handlers.HandleFleetVehicleDelete) + + // ### FLEETUPDATE ### + addHandler(router, permissionCreate, http.MethodPost, "/fleetupdate", handlers.HandleFleetUpdatesAdd) + + // ### ISSUE ### + addHandler(router, permissionRead, http.MethodGet, "/issues", handlers.HandleIssuesGet) + addHandler(router, permissionRead, http.MethodGet, "/issues/:id", handlers.HandleIssueGet) + addHandler(router, permissionDelete, http.MethodDelete, "/issues/:id", handlers.HandleIssuesDelete) + + // ### MANIFEST ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/manifests", handlers.HandleUpdateManifestsGet) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/manifest", handlers.HandleUpdateManifestGet) + addHandler(router, permissionCreate, http.MethodPost, "/manifest", handlers.HandleUpdateManifestAdd) + addHandler(router, permissionCreate, http.MethodPut, "/manifest", handlers.HandleUpdateManifestUpdate) + addHandler(router, permissionDelete, http.MethodDelete, "/manifest", handlers.HandleUpdateManifestDelete) + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPut, "/manifests/:id/sums", handlers.HandleUpdateManifestSUMSUpdate) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/manifests/:manifest_id/vehicles", handlers.HandleGetCarsByManifest) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/manifest/sums", handlers.HandleUpdateManifestSUMSGet) + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPost, "/manifest/sums", handlers.HandleUpdateManifestSUMSAdd) + addHandler(router, permissionDelete.CopyAndMerge(magnaAccess), http.MethodDelete, "/manifest/sums/:version", handlers.HandleUpdateManifestSUMSDelete) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/manifest/sums/:version/rxswins", handlers.HandleUpdateManifestSUMSRxSwinsGet) + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPost, "/manifest/sums/:version/rxswins", handlers.HandleUpdateManifestSUMSRxSwinsAdd) + addHandler(router, permissionDelete.CopyAndMerge(magnaAccess), http.MethodDelete, "/manifest/sums/:version/rxswins/:rxswin", handlers.HandleUpdateManifestSUMSRxSwinsDelete) + + // ### MANIFESTECU ### + addHandler(router, permissionCreate, http.MethodPost, "/manifestecu", handlers.HandleUpdateManifestECUAdd) + + // ### MANIFESTFILE ### + addHandler(router, permissionCreate, http.MethodPost, "/manifestfile", handlers.HandleUpdateManifestFileAdd) + + // ### MANIFESTMIGRATE ### + // Need a special permission + // Like HandleUpdateManifestAdd, but receives a manifest itself instead of the CreateUpdateManifest object + addHandler(router, permissionMigrate, http.MethodPost, "/manifestmigrate", handlers.HandleUpdateManifestMigrateReceive) + // Sends the manifest to another API + addHandler(router, permissionMigrate, http.MethodPost, "/manifestmigrate/:manifest_id", handlers.HandleUpdateManifestMigrate) + addHandler(router, permissionMigrate, http.MethodGet, "/manifestmigrate-version", handlers.HandleUpdateManifestMigrateVersion) + + // ### MANUFACTURECERT ### + addHandler(router, permissionManufacture.CopyAndMerge(magnaAccess), http.MethodPost, "/manufacture-certs", handlers.HandleGetDLLManufactureCerts) + + // ### SMS ### + addHandler(router, permissionCreate, http.MethodPost, "/sms", handlers.HandleSMSSend) + + // ### SUBSCRIPTION ### + addHandler(router, permissionDelete, http.MethodDelete, "/subscription", handlers.HandleSubscriptionDelete) + + // ### SUBSCRIPTIONCONFIG ### + addHandler(router, permissionRead, http.MethodGet, "/subscriptionconfigs", handlers.HandleSubscriptionConfigsGetList) + addHandler(router, permissionCreate, http.MethodPost, "/subscriptionconfig", handlers.HandleSubscriptionConfigAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/subscriptionconfig", handlers.HandleSubscriptionConfigDelete) + addHandler(router, permissionCreate, http.MethodPut, "/subscriptionconfig", handlers.HandleSubscriptionConfigUpdate) + + // ### SUBSCRIPTIONFEATURE ### + addHandler(router, permissionRead, http.MethodGet, "/subscriptionfeatures", handlers.HandleSubscriptionFeaturesGet) + addHandler(router, permissionCreate, http.MethodPost, "/subscriptionfeature", handlers.HandleSubscriptionFeatureAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/subscriptionfeature", handlers.HandleSubscriptionFeatureDelete) + addHandler(router, permissionCreate, http.MethodPut, "/subscriptionfeature", handlers.HandleSubscriptionFeatureUpdate) + addHandler(router, permissionRead, http.MethodGet, "/subscriptionfeature", handlers.HandleSubscriptionFeatureGet) + + // ### SUBSCRIPTIONPACKAGE ### + addHandler(router, permissionRead, http.MethodGet, "/subscriptionpackages", handlers.HandleSubscriptionPackagesGetList) + addHandler(router, permissionCreate, http.MethodPost, "/subscriptionpackage", handlers.HandleSubscriptionPackageAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/subscriptionpackage", handlers.HandleSubscriptionPackageDelete) + addHandler(router, permissionCreate, http.MethodPut, "/subscriptionpackage", handlers.HandleSubscriptionPackageUpdate) + addHandler(router, permissionRead, http.MethodGet, "/subscriptionpackage", handlers.HandleSubscriptionPackageGet) + + // ### SUBSCRIPTIONPACKAGEFEATURE ### + addHandler(router, permissionCreate, http.MethodPost, "/subscriptionpackagefeature", handlers.HandleSubscriptionFeatureAssign) + + // ### SUPPLIER ### + addHandler(router, permissionSupplierAdmin, http.MethodGet, "/suppliers", handlers.HandleSuppliersGetList) + addHandler(router, permissionSupplierAdmin, http.MethodPost, "/supplier/activate/:email", handlers.HandleSupplierActivate) + addHandler(router, permissionSupplierAdmin, http.MethodPut, "/supplier/:email", handlers.HandleSupplierUpdate) + addHandler(router, permissionSupplierAdmin, http.MethodDelete, "/supplier/:email", handlers.HandleSupplierDelete) + + // ### TAG ### + addHandler(router, permissionCreate, http.MethodPut, "/tags", handlers.HandleTagsUpdate) + addHandler(router, permissionCreate, http.MethodPost, "/tags", handlers.HandleTagsAppend) + + // ### VEHICLE ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicles", handlers.HandleVehiclesGet) + addHandler(router, permissionCreate, http.MethodPut, "/vehicles/archive", handlers.HandleUpdateManifestsArchive) + addHandler(router, permissionCreate, http.MethodPost, "/vehicle", handlers.HandleVehicleAdd) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicle/:vin", handlers.HandleVehicleGet) + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPut, "/vehicle/:vin", handlers.HandleVehicleUpdate) + addHandler(router, permissionDelete, http.MethodDelete, "/vehicle/:vin", handlers.HandleVehicleDeleteHoneyPot) + addHandler(router, permissionRead, http.MethodGet, "/vehicle/:vin/filters", handlers.HandleVehicleFilterGetList) + addHandler(router, permissionCreate, http.MethodPost, "/vehicle/:vin/filter", handlers.HandleVehicleFilterAdd) + addHandler(router, permissionCreate, http.MethodPut, "/vehicle/:vin/filter/:id", handlers.HandleVehicleFilterUpdate) + addHandler(router, permissionDelete, http.MethodDelete, "/vehicle/:vin/filter/:id", handlers.HandleVehicleFilterDelete) + addHandler(router, permissionRead, http.MethodGet, "/vehicle/:vin/fleets", handlers.HandleVehicleFleetGetList) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicle/:vin/trex-logs", handlers.HandleTrexLogsGet) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicle/:vin/trex-logs-link", handlers.HandleTrexLogsLinkGet) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicle/:vin/version", handlers.HandleVersionsGet) + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicle/:vin/version/logs", handlers.HandleVersionLogsGet) + + // ### VEHICLECOMMAND ### + addHandler(router, permissionCreate.CopyAndMerge(magnaAccess), http.MethodPost, "/vehiclecommand", handlers.HandleVehicleCommand) + // immobilizer routes + addHandler(router, permissionCreate, http.MethodGet, "/vehiclecommand/immobilize", handlers.HandleVehicleImmobilizerList) + addHandler(router, permissionCreate, http.MethodPost, "/vehiclecommand/immobilize", handlers.HandleVehicleImmobilizerAdd) + addHandler(router, permissionCreate, http.MethodDelete, "/vehiclecommand/immobilize", handlers.HandleVehicleImmobilizerDelete) + + // ### VEHICLEDIAGNOSTICCOMMAND ### + addHandler(router, carDiagnostic, http.MethodPost, "/vehiclediagnosticcommand", handlers.HandleVehicleDiagnosticCommand) + + // ### VEHICLEECU ### + addHandler(router, permissionRead.CopyAndMerge(magnaAccess), http.MethodGet, "/vehicleecus", handlers.HandleVehicleECUsGet) + + // ### VEHICLEMODEL ### + addHandler(router, permissionRead, http.MethodGet, "/vehiclemodels", handlers.HandleVehicleModels) + + // ### FLASHPACK TO ECU VERSION MAPPINGS ### + addHandler(router, permissionRead, http.MethodGet, "/flashpack_version_ecu_mappings/:model/:trim/:year/:flashpack", handlers.HandleFlashpackVersionECUMappingsGet) + addHandler(router, permissionRead, http.MethodGet, "/flashpack_versions/:model/:trim/:year", handlers.HandleFlashpackVersionsGetAll) + addHandler(router, permissionCreate, http.MethodPost, "/flashpack_version", handlers.HandleFlashpackVersionAdd) + addHandler(router, permissionDelete, http.MethodDelete, "/flashpack_version", handlers.HandleFlashpackVersionDelete) + addHandler(router, permissionRead, http.MethodGet, "/flashpack_version_info/:vin", handlers.HandleFlashpackVersionInfoGet) + + // ### VEHICLEPATH ### + addHandler(router, permissionRead, http.MethodPost, "/vehicle_paths", handlers.HandleVehiclePathsPost) + + // ### VEHICLEYEAR ### + addHandler(router, permissionRead, http.MethodGet, "/vehicleyears", handlers.HandleVehicleYears) + + // ### CUSTOMER EMAILS ### + addHandler(router, permissionRead, http.MethodPut, "/customer_ota_emails", handlers.HandleCustomerOtaEmails) + + // ### OVLoop Data Sources ### + addHandler(router, permissionRead, http.MethodGet, "/cars/allowed_access", handlers.HandleCarsAllowedAccess) + addHandler(router, permissionRead, http.MethodPost, "/cars/allowed_access", handlers.HandleCarAllowedAccess) + addHandler(router, permissionCreate, http.MethodPost, "/cars/change_access", handlers.HandleCarChangeAccess) + addHandler(router, permissionCreate, http.MethodGet, "/cars/hmi_key", handlers.HandleGetCarsHMIKey) + addHandler(router, permissionCreate, http.MethodPost, "/drivers/add_external", handlers.HandleVehicleExternalDriverAdd) + addHandler(router, permissionCreate, http.MethodDelete, "/drivers/delete_external", handlers.HandleExternalDriverDelete) + addHandler(router, permissionRead, http.MethodGet, "/car/software_information", handlers.HandleCarSoftwareInformation) + addHandler(router, permissionRead, http.MethodGet, "/car/software_information/v2", handlers.HandleCarSoftwareInformationV2) + addHandler(router, permissionRead, http.MethodPost, "/car/wake", handlers.HandleSendWakeSMSToVIN) + + // Experimental text feature + addHandler(router, nil, http.MethodGet, "/expirment", handlers.HandleExperiment) + + go controllers.HealthCheck() + + logger.Info().Msgf("Listening on http://0.0.0.0%s", port) + logger.Fatal().Err(http.ListenAndServe(port, router)).Send() +} + +func addHandler(router *httprouter.Router, roles a.RoleMap, method string, path string, handler http.HandlerFunc) { + router.HandlerFunc(method, h.HttpRouterHandleBaseURL(path), authChecker.GetHandler(roles, h.LogRequest(handler))) +} + +func cleanup() { + authChecker.Close() + services.GetDB().Close() + logger.Close() +} diff --git a/services/ota_update_go/messages/response_messages.go b/services/ota_update_go/messages/response_messages.go new file mode 100644 index 0000000..87aea24 --- /dev/null +++ b/services/ota_update_go/messages/response_messages.go @@ -0,0 +1,11 @@ +package messages + +// Response messages +const ( + NotPostRequest string = "not a POST request" + RequireMediaType string = "mime: no media type" + RequiresMultipart string = "requires multipart" + RequiresFile string = "requires file" + BoundaryTooLong string = "multipart boundary too long" + S3KeyTooLong string = "S3 key too long" +) diff --git a/services/ota_update_go/services/clickhouse.go b/services/ota_update_go/services/clickhouse.go new file mode 100644 index 0000000..a913f8d --- /dev/null +++ b/services/ota_update_go/services/clickhouse.go @@ -0,0 +1,60 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/clickhouse" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/pkg/errors" +) + +var ( + clickhouseClient clickhouse.ClientInterface + clickhouseConn clickhouse.ConnInterface + clickhouseConnLock sync.Mutex +) + +func initClickhouseConn() (clickhouse.ConnInterface, error) { + logger.Info().Msg("Init clickhouse connection") + + clickhouseConnLock.Lock() + defer clickhouseConnLock.Unlock() + + clClient, err := clickhouse.NewConn() + if err != nil { + logger.Error().Err(err).Msg("cannot connect to clickhouse") + return nil, errors.WithMessage(err, "cannot connect to clickhouse") + } + + clickhouseConn = clClient + + return clickhouseConn, nil +} + +func GetClickhouseConn() (clickhouse.ConnInterface, error) { + if clickhouseConn != nil { + return clickhouseConn, nil + } + + return initClickhouseConn() +} + +func SetClickhouseConn(conn clickhouse.ConnInterface) { + clickhouseConnLock.Lock() + defer clickhouseConnLock.Unlock() + + clickhouseConn = conn +} + +func GetClickhouseClient() (clickhouse.ClientInterface, error) { + if clickhouseClient != nil { + return clickhouseClient, nil + } + + conn, err := GetClickhouseConn() + if err != nil { + return nil, err + } + + return clickhouse.NewClient(conn) +} diff --git a/services/ota_update_go/services/config.go b/services/ota_update_go/services/config.go new file mode 100644 index 0000000..b161b2e --- /dev/null +++ b/services/ota_update_go/services/config.go @@ -0,0 +1,29 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/logger" + + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +var ( + configOnce sync.Once + configInstance vconfig.ConfigServiceInterface +) + +func GetVehicleConfig() vconfig.ConfigServiceInterface { + configOnce.Do(func() { + if configInstance != nil { + return + } + logger.Info().Msg("init vehicle config instance") + configInstance = vconfig.NewConfigService() + }) + return configInstance +} + +func SetVehicleConfig(c vconfig.ConfigServiceInterface) { + configInstance = c +} diff --git a/services/ota_update_go/services/db.go b/services/ota_update_go/services/db.go new file mode 100644 index 0000000..09f889c --- /dev/null +++ b/services/ota_update_go/services/db.go @@ -0,0 +1,495 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/common/actionlogger" + "github.com/fiskerinc/cloud-services/pkg/db" + q "github.com/fiskerinc/cloud-services/pkg/db/queries" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var ( + dbOnce sync.Once + dbInstance *DB +) + +type DB struct { + client *db.DBClient + actionLog q.ActionLogInterface + cars q.CarsInterface + ecckeys q.EccKeysInterface + symKeys q.SymKeysInterface + carupdates q.CarUpdatesInterface + filekeys q.FileKeysInterface + updatemanifest q.UpdateManifestsInterface + apiTokens q.APITokensInterface + apiCalls q.APICallsInterface + subpackages q.SubscriptionPackagesInterface + subfeatures q.SubscriptionFeaturesInterface + subconfigs q.SubscriptionConfigurationsInterface + subscriptions q.SubscriptionsInterface + supplierAccounts q.SupplierAccountsInterface + issues q.IssuesInterface + carVersionsLog q.CarVersionsLogInterface + updateManifestVersions q.SUMSVersionsInterface + swVersionRxSwin q.SwVersionRxSwinInterface + tags q.TagsInterface + dtcecu q.ECUInterface + carConfigData q.CarConfigDataInterface + driverEmails q.DriverEmailsInterface + onceActionLog sync.Once + onceDTCECU sync.Once + onceIssues sync.Once + onceClient sync.Once + onceCars sync.Once + onceCarUpdates sync.Once + onceFileKeys sync.Once + onceUpdateManifest sync.Once + onceAPITokens sync.Once + onceAPICalls sync.Once + onceSubPackages sync.Once + onceSubFeatures sync.Once + onceSubConfig sync.Once + onceSubscriptions sync.Once + onceSupplierForm sync.Once + onceCarVersionsLog sync.Once + onceUpdateManifestVersions sync.Once + onceSwVersionRxSwin sync.Once + onceTags sync.Once + onceECCKeys sync.Once + onceSymKeys sync.Once + onceCarConfigData sync.Once + onceDriverEmails sync.Once +} + +func GetDB() *DB { + dbOnce.Do(func() { + if dbInstance != nil { + return + } + logger.Info().Msg("Init DB instance") + dbInstance = &DB{} + }) + return dbInstance +} + +func SetDB(db *DB) { + if dbInstance != nil { + dbInstance.Close() + } + dbInstance = db +} + +func (d *DB) GetDBClient() *db.DBClient { + d.onceClient.Do(func() { + if d.client != nil { + return + } + logger.Info().Msg("Init DBClient instance") + client := &db.DBClient{} + err := client.InitSchema([]interface{}{ + (*actionlogger.ActionLog)(nil), + (*common.SupplierAccount)(nil), + (*common.APIToken)(nil), + (*common.UpdateManifest)(nil), + (*common.UpdateManifestECU)(nil), + (*common.UpdateManifestFile)(nil), + (*common.FileKey)(nil), + (*common.ECCKeys)(nil), + (*common.SymKeys)(nil), + (*common.CarECU)(nil), + (*common.Car)(nil), + (*common.CarUpdate)(nil), + (*common.Driver)(nil), + (*common.CarToDriver)(nil), + (*common.CarSetting)(nil), + (*common.CarVersionLogs)(nil), + (*common.SUMSVersion)(nil), + (*common.SwVersionRxSwin)(nil), + }) + if err != nil { + logger.Error().Err(err).Send() + } + + client.RegisterManyToManyRel([]interface{}{ + (*common.SubscriptionPackageToFeature)(nil), + }) + d.client = client + }) + + // d.client.GetConn().AddQueryHook(db.SQLLogger{}) + return d.client +} + +func (d *DB) SetDBClient(client *db.DBClient) { + if d.client != nil { + d.client.Close() + } + d.client = client +} + +func (d *DB) GetCars() q.CarsInterface { + d.onceCars.Do(func() { + if d.cars != nil { + return + } + logger.Debug().Msg("Init Cars instance") + cars := &q.Cars{} + cars.SetClient(d.GetDBClient()) + d.cars = cars + }) + return d.cars +} + +func (d *DB) SetCars(cars q.CarsInterface) { + d.cars = cars +} + +func (d *DB) GetCarUpdates() q.CarUpdatesInterface { + d.onceCarUpdates.Do(func() { + if d.carupdates != nil { + return + } + logger.Debug().Msg("Init CarUpdates instance") + carupdates := &q.CarUpdates{} + carupdates.SetClient(d.GetDBClient()) + d.carupdates = carupdates + }) + return d.carupdates +} + +func (d *DB) SetCarUpdates(carupdates q.CarUpdatesInterface) { + d.carupdates = carupdates +} + +func (d *DB) GetECCKeys() q.EccKeysInterface { + d.onceECCKeys.Do(func() { + if d.ecckeys != nil { + return + } + logger.Debug().Msg("Init ECCKeys instance") + ecckeys := &q.EccKeys{} + ecckeys.SetClient(d.GetDBClient()) + d.ecckeys = ecckeys + }) + return d.ecckeys +} + +func (d *DB) GetSymKeys() q.SymKeysInterface { + d.onceSymKeys.Do(func() { + if d.symKeys != nil { + return + } + logger.Debug().Msg("Init SymKeys instance") + symKeys := &q.SymKeys{} + symKeys.SetClient(d.GetDBClient()) + d.symKeys = symKeys + }) + return d.symKeys +} + +func (d *DB) GetFileKeys() q.FileKeysInterface { + d.onceFileKeys.Do(func() { + if d.filekeys != nil { + return + } + logger.Debug().Msg("Init FileKeys instance") + filekeys := &q.FileKeys{} + filekeys.SetClient(d.GetDBClient()) + d.filekeys = filekeys + }) + return d.filekeys +} + +func (d *DB) SetFileKeys(filekeys q.FileKeysInterface) { + d.filekeys = filekeys +} + +func (d *DB) GetUpdateManifests() q.UpdateManifestsInterface { + d.onceUpdateManifest.Do(func() { + if d.updatemanifest != nil { + return + } + logger.Debug().Msg("Init UpdateManifest instance") + updatemanifest := q.NewUpdateManifest(nil) + updatemanifest.SetClient(d.GetDBClient()) + d.updatemanifest = updatemanifest + }) + return d.updatemanifest +} + +func (d *DB) SetUpdateManifests(updatemanifest q.UpdateManifestsInterface) { + d.updatemanifest = updatemanifest +} + +func (d *DB) GetTags() q.TagsInterface { + d.onceTags.Do(func() { + if d.tags != nil { + return + } + logger.Debug().Msg("Init Tags instance") + tags := q.NewTags() + tags.SetClient(d.GetDBClient()) + d.tags = tags + }) + return d.tags +} + +func (d *DB) SetTags(t q.TagsInterface) { + d.tags = t +} + +func (d *DB) GetAPITokens() q.APITokensInterface { + d.onceAPITokens.Do(func() { + if d.apiTokens != nil { + return + } + logger.Debug().Msg("Init APITokens instance") + apitokens := &q.APITokens{} + apitokens.SetClient(d.GetDBClient()) + d.apiTokens = apitokens + }) + return d.apiTokens +} + +func (d *DB) SetAPITokens(apitokens q.APITokensInterface) { + d.apiTokens = apitokens +} + +func (d *DB) GetAPICalls() q.APICallsInterface { + d.onceAPICalls.Do(func() { + if d.apiCalls != nil { + return + } + logger.Debug().Msg("Init APITokens instance") + apiCalls := &q.APICalls{} + apiCalls.SetClient(d.GetDBClient()) + d.apiCalls = apiCalls + }) + return d.apiCalls +} + +func (d *DB) SetAPICalls(apiCalls q.APICallsInterface) { + d.apiCalls = apiCalls +} + +func (d *DB) GetSubPackages() q.SubscriptionPackagesInterface { + d.onceSubPackages.Do(func() { + if d.subpackages != nil { + return + } + logger.Debug().Msg("Init Subscription Packages instance") + subpackages := &q.SubscriptionPackages{} + subpackages.SetClient(d.GetDBClient()) + d.subpackages = subpackages + }) + return d.subpackages +} + +func (d *DB) SetSubPackages(instance q.SubscriptionPackagesInterface) { + d.subpackages = instance +} + +func (d *DB) GetSubFeatures() q.SubscriptionFeaturesInterface { + d.onceSubFeatures.Do(func() { + if d.subfeatures != nil { + return + } + logger.Debug().Msg("Init Subscription Features instance") + subfeatures := &q.SubscriptionFeatures{} + subfeatures.SetClient(d.GetDBClient()) + d.subfeatures = subfeatures + }) + return d.subfeatures +} + +func (d *DB) SetSubFeatures(instance q.SubscriptionFeaturesInterface) { + d.subfeatures = instance +} + +func (d *DB) GetSubConfigurations() q.SubscriptionConfigurationsInterface { + d.onceSubConfig.Do(func() { + if d.subconfigs != nil { + return + } + logger.Debug().Msg("Init Subscription Configurations instance") + subconfigs := &q.SubscriptionConfigurations{} + subconfigs.SetClient(d.GetDBClient()) + d.subconfigs = subconfigs + }) + return d.subconfigs +} + +func (d *DB) SetSubConfigurations(instance q.SubscriptionConfigurationsInterface) { + d.subconfigs = instance +} + +func (d *DB) GetSubscriptions() q.SubscriptionsInterface { + d.onceSubscriptions.Do(func() { + if d.subscriptions != nil { + return + } + logger.Debug().Msg("Init Subscriptions instance") + subscriptions := &q.Subscriptions{} + subscriptions.SetClient(d.GetDBClient()) + d.subscriptions = subscriptions + }) + return d.subscriptions +} + +func (d *DB) SetSubscriptions(instance q.SubscriptionsInterface) { + d.subscriptions = instance +} + +func (d *DB) GetSupplierAccounts() q.SupplierAccountsInterface { + d.onceSupplierForm.Do(func() { + if d.supplierAccounts != nil { + return + } + logger.Debug().Msg("Init SupplierForm instance") + supplierForm := &q.SupplierAccounts{} + supplierForm.SetClient(d.GetDBClient()) + d.supplierAccounts = supplierForm + }) + return d.supplierAccounts +} + +func (d *DB) SetSupplierAccount(accounts q.SupplierAccountsInterface) { + d.supplierAccounts = accounts +} + +func (d *DB) GetIssues() q.IssuesInterface { + d.onceIssues.Do(func() { + if d.issues != nil { + return + } + instance := &q.Issues{} + instance.SetClient(d.GetDBClient()) + d.issues = instance + }) + return d.issues +} + +func (d *DB) SetIssues(issues q.IssuesInterface) { + d.issues = issues +} + +func (d *DB) GetCarVersionsLog() q.CarVersionsLogInterface { + d.onceCarVersionsLog.Do(func() { + if d.carVersionsLog != nil { + return + } + instance := &q.CarVersionsLog{} + instance.SetClient(d.GetDBClient()) + d.carVersionsLog = instance + }) + return d.carVersionsLog +} + +func (d *DB) SetCarVersionsLog(log q.CarVersionsLogInterface) { + d.carVersionsLog = log +} + +func (d *DB) GetUpdateManifestVersions() q.SUMSVersionsInterface { + d.onceUpdateManifestVersions.Do(func() { + if d.updateManifestVersions != nil { + return + } + instance := &q.SUMSVersions{} + instance.SetClient(d.GetDBClient()) + d.updateManifestVersions = instance + }) + return d.updateManifestVersions +} + +func (d *DB) SetUpdateManifestVersions(umv q.SUMSVersionsInterface) { + d.updateManifestVersions = umv +} + +func (d *DB) GetSwVerRxSwin() q.SwVersionRxSwinInterface { + d.onceSwVersionRxSwin.Do(func() { + if d.swVersionRxSwin != nil { + return + } + instance := &q.SwVersionRxSwin{} + instance.SetClient(d.GetDBClient()) + d.swVersionRxSwin = instance + }) + return d.swVersionRxSwin +} + +func (d *DB) SetSwVersionRxSwin(svrs q.SwVersionRxSwinInterface) { + d.swVersionRxSwin = svrs +} + +func (d *DB) SetDTCECU(dtcecu q.ECUInterface) { + d.dtcecu = dtcecu +} + +func (d *DB) GetDTCECU() q.ECUInterface { + d.onceDTCECU.Do(func() { + if d.dtcecu != nil { + return + } + logger.Debug().Msg("Init DTC ECU instance") + dtcecu := &q.ECU{} + dtcecu.SetClient(d.GetDBClient()) + d.dtcecu = dtcecu + }) + return d.dtcecu +} + +func (d *DB) GetActionLog() q.ActionLogInterface { + d.onceActionLog.Do(func() { + if d.actionLog != nil { + return + } + instance := &q.ActionLogDB{} + instance.SetClient(d.GetDBClient()) + d.actionLog = instance + }) + return d.actionLog +} + +func (d *DB) GetCarConfigData() q.CarConfigDataInterface { + d.onceCarConfigData.Do(func() { + if d.carConfigData != nil { + return + } + logger.Debug().Msg("Init CarConfigData instance") + carConfigData := &q.CarConfigData{} + carConfigData.SetClient(d.GetDBClient()) + d.carConfigData = carConfigData + }) + return d.carConfigData +} + +func (d *DB) SetCarConfigData(carConfigData q.CarConfigDataInterface) { + d.carConfigData = carConfigData +} + +func (d *DB) GetDriverEmails() q.DriverEmailsInterface { + d.onceDriverEmails.Do(func() { + if d.driverEmails != nil { + return + } + logger.Debug().Msg("Init DriverEmails instance") + driverEmails := &q.DriverEmails{} + driverEmails.SetClient(d.GetDBClient()) + d.driverEmails = driverEmails + }) + return d.driverEmails +} + +func (d *DB) SetDriverEmails(driverEmails q.DriverEmailsInterface) { + d.driverEmails = driverEmails +} + +func (d *DB) Close() { + if d.client == nil { + return + } + d.client.Close() +} diff --git a/services/ota_update_go/services/foa.go b/services/ota_update_go/services/foa.go new file mode 100644 index 0000000..5add44b --- /dev/null +++ b/services/ota_update_go/services/foa.go @@ -0,0 +1,84 @@ +package services + +import ( + "net/http" + "net/url" + "slices" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/common" + s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus" + "github.com/fiskerinc/cloud-services/pkg/foa" + "github.com/fiskerinc/cloud-services/pkg/httpclient" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA = []int64{816, 817, 818, 819, 820} + +var ( + foaService FoaServiceInterface + foaOnce sync.Once +) + +func GetFoaService() FoaServiceInterface { + foaOnce.Do(func() { + if foaService != nil { + return + } + foaService = NewFoaService() + }) + + return foaService +} + +func SetFoaService(foa FoaServiceInterface) { + foaService = foa +} + +func NewFoaService() FoaServiceInterface { + return &FoaService{ + foaURL: envtool.GetEnv("FOA_URL", "REPLACE_ME"), + foaAPIToken: envtool.GetEnv("FOA_API_KEY", "REPLACE_ME"), + } +} + +type FoaServiceInterface interface { + OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) +} + +type FoaService struct { + foaURL string + foaAPIToken string +} + +func (f *FoaService) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) { + if !slices.Contains(UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA, carUpdate.UpdateManifestID) { + // Nothing to send if the manifest is not one of the specified IDs + return nil, nil + } + + var body interface{} = nil + + switch status.Status { + case s.Pending: + body = foa.BuildOtaUpdateStatusInProgressRequest(vin, carUpdate.UpdateManifestID) + } + + if body == nil { + return nil, nil + } + + logger.Info().Msgf("Notifying FOA for %s of update %d status %s", vin, carUpdate.UpdateManifestID, status.Status) + + urlString, err := url.JoinPath(f.foaURL, "ota/update_status") + if err != nil { + return nil, err + } + + postHeader := http.Header{} + postHeader.Add("Authorization", "Bearer "+f.foaAPIToken) + postHeader.Add("Content-Type", "application/json") + + return httpclient.Post(urlString, body, postHeader) +} diff --git a/services/ota_update_go/services/kafka.go b/services/ota_update_go/services/kafka.go new file mode 100644 index 0000000..4a5c47d --- /dev/null +++ b/services/ota_update_go/services/kafka.go @@ -0,0 +1,34 @@ +package services + +import ( + "context" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/kafka" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +var producer kafka.ProducerInterface +var producerOnce sync.Once + +// GetKafkaProducer returns singleton instance of kafka producer +func GetKafkaProducer() kafka.ProducerInterface { + producerOnce.Do(func() { + if producer != nil { + return + } + ctx := context.Background() + var err error + producer, err = kafka.NewProducer(ctx) + if err != nil { + logger.Error().Err(err).Send() + } + go producer.ReadEvents() + }) + + return producer +} + +func SetKafkaProducer(p kafka.ProducerInterface) { + producer = p +} diff --git a/services/ota_update_go/services/mongo.go b/services/ota_update_go/services/mongo.go new file mode 100644 index 0000000..b2bacd5 --- /dev/null +++ b/services/ota_update_go/services/mongo.go @@ -0,0 +1,32 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/mongo" +) + +var ( + clientOnce sync.Once + client mongo.Client +) + +// GetMongoClient returns singleton instance of mongo client +func GetMongoClient() ( mongo.Client, error) { + var err error + clientOnce.Do(func() { + client, err = initMongoClient() + }) + + return client, err +} + +func initMongoClient() (mongo.Client, error) { + var err error + + if client == nil { + client, err = mongo.NewClient(mongo.StandardDB) + } + + return client, err +} diff --git a/services/ota_update_go/services/ota.go b/services/ota_update_go/services/ota.go new file mode 100644 index 0000000..9ffcd29 --- /dev/null +++ b/services/ota_update_go/services/ota.go @@ -0,0 +1,187 @@ +package services + +import ( + "errors" + "net/http" + "net/url" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/httpclient" + "github.com/fiskerinc/cloud-services/pkg/logger" +) + +// Not mockable +func NewOtaService(url string, token string) OtaServiceInterface { + return &OtaService{ + otaURL: url, + otaAPIToken: token, + } +} + +type OtaServiceInterface interface { + FlashpackVersionAdd(req common.CarFlashpackVersionAddRequest) (*http.Response, error) + FlashpackVersionDelete(req common.CarFlashpackVersionRequest) (*http.Response, error) + UpdateManifestSUMSAdd(req common.SUMSVersionCreate) (*http.Response, error) + UpdateManifestSUMSDelete(version string) (*http.Response, error) + UpdateManifestSUMSRxSwinsAdd(version string, req common.SwVersionRxSwinCreate) (*http.Response, error) + UpdateManifestSUMSRxSwinsDelete(version string, rxswin string) (*http.Response, error) +} + +type OtaService struct { + otaURL string + otaAPIToken string +} + +func (o *OtaService) FlashpackVersionAdd(req common.CarFlashpackVersionAddRequest) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "flashpack_version") + if err != nil { + return nil, err + } + + postHeader := http.Header{} + postHeader.Add("Api-Key", o.otaAPIToken) + postHeader.Add("accept", "application/json") + postHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Post(url, req, postHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to migrate flashpack version ECU mappings:\n Status: %s\n Model: %s Year: %d Flashpack: %s\n", resp.Status, req.CarModel, req.CarYear, req.Flashpack) + } + + return resp, nil +} + +func (o *OtaService) FlashpackVersionDelete(req common.CarFlashpackVersionRequest) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "flashpack_version") + if err != nil { + return nil, err + } + + deleteHeader := http.Header{} + deleteHeader.Add("Api-Key", o.otaAPIToken) + deleteHeader.Add("accept", "application/json") + deleteHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Delete(url, req, deleteHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to delete flashpack version ECU mappings:\n Status: %s\n Model: %s Year: %d Flashpack: %s\n", resp.Status, req.CarModel, req.CarYear, req.Flashpack) + } + + return resp, nil +} + +func (o *OtaService) UpdateManifestSUMSAdd(req common.SUMSVersionCreate) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "manifest/sums") + if err != nil { + return nil, err + } + + if len(req.SUMSVersions) < 1 { + return nil, errors.New("cannot call /manifest/sums/ POST with no sums versions to add") + } + + postHeader := http.Header{} + postHeader.Add("Api-Key", o.otaAPIToken) + postHeader.Add("accept", "application/json") + postHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Post(url, req, postHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to migrate SUMS:\n Status: %s\n Version: %s\n", resp.Status, req.SUMSVersions[0].Version) + } + + return resp, nil +} + +func (o *OtaService) UpdateManifestSUMSDelete(version string) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "manifest/sums/"+url.PathEscape(version)) + if err != nil { + return nil, err + } + + deleteHeader := http.Header{} + deleteHeader.Add("Api-Key", o.otaAPIToken) + deleteHeader.Add("accept", "application/json") + deleteHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Delete(url, nil, deleteHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to delete SUMS:\n Status: %s\n Version: %s\n", resp.Status, version) + } + + return resp, nil +} + +func (o *OtaService) UpdateManifestSUMSRxSwinsAdd(version string, req common.SwVersionRxSwinCreate) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "/manifest/sums/"+url.PathEscape(version)+"/rxswins") + if err != nil { + return nil, err + } + + postHeader := http.Header{} + postHeader.Add("Api-Key", o.otaAPIToken) + postHeader.Add("accept", "application/json") + postHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Post(url, req, postHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to add RXSWINs to SUMS:\n Status: %s\n Version: %s\n", resp.Status, version) + } + + return resp, nil +} + +func (o *OtaService) UpdateManifestSUMSRxSwinsDelete(version string, rxswin string) (*http.Response, error) { + url, err := url.JoinPath(o.otaURL, "manifest/sums/"+url.PathEscape(version)+"/rxswins/"+url.PathEscape(rxswin)) + if err != nil { + return nil, err + } + + deleteHeader := http.Header{} + deleteHeader.Add("Api-Key", o.otaAPIToken) + deleteHeader.Add("accept", "application/json") + deleteHeader.Add("Content-Type", "application/json") + + resp, err := httpclient.Delete(url, nil, deleteHeader) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Error().Msgf("Failed to delete RXSWINs from SUMS:\n Status: %s\n Version: %s\n", resp.Status, version) + } + + return resp, nil +} diff --git a/services/ota_update_go/services/redis.go b/services/ota_update_go/services/redis.go new file mode 100644 index 0000000..06cbf0b --- /dev/null +++ b/services/ota_update_go/services/redis.go @@ -0,0 +1,26 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/redis" +) + +var ( + clientPoolOnce sync.Once + clientPool redis.ClientPoolInterface +) + +func RedisClientPool() redis.ClientPoolInterface { + clientPoolOnce.Do(func() { + if clientPool == nil { + clientPool = redis.NewClientPool() + } + }) + + return clientPool +} + +func SetRedisClientPool(cp redis.ClientPoolInterface) { + clientPool = cp +} diff --git a/services/ota_update_go/services/redisV2.go b/services/ota_update_go/services/redisV2.go new file mode 100644 index 0000000..40da6d3 --- /dev/null +++ b/services/ota_update_go/services/redisV2.go @@ -0,0 +1,26 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/redisv2" +) + +var ( + redisClientOnce sync.Once + redisClient *redisv2.Connection +) + +func GetRedisV2Client() *redisv2.Connection { + redisClientOnce.Do(func() { + if redisClient == nil { + redisClient = redisv2.NewClient(nil) + } + }) + + return redisClient +} + +func SetRedisV2Client(cp *redisv2.Connection) { + redisClient = cp +} \ No newline at end of file diff --git a/services/ota_update_go/services/sap.go b/services/ota_update_go/services/sap.go new file mode 100644 index 0000000..4eed48d --- /dev/null +++ b/services/ota_update_go/services/sap.go @@ -0,0 +1,28 @@ +package services + +import ( + "sync" + + vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig" +) + +var ( + sapService vconfig.SAPServiceInterface + sapOnce sync.Once +) + +func GetSapService() vconfig.SAPServiceInterface { + sapOnce.Do(func() { + if sapService != nil { + return + } + sapService = vconfig.NewSAPService() + }) + + return sapService +} + +// SetSapService is supposed t be used for testing. +func SetSapService(sap vconfig.SAPServiceInterface) { + sapService = sap +} diff --git a/services/ota_update_go/services/sms.go b/services/ota_update_go/services/sms.go new file mode 100644 index 0000000..860a32f --- /dev/null +++ b/services/ota_update_go/services/sms.go @@ -0,0 +1,50 @@ +package services + +import ( + "fmt" + "sync" + + "github.com/fiskerinc/cloud-services/pkg/grpc/sms" + "github.com/fiskerinc/cloud-services/pkg/logger" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" +) + +var smsClient sms.SMSServiceClient +var smsClientOnce sync.Once + +func newSmsClient() { + logger.Info().Msg("Init SMS client") + target := fmt.Sprintf("%s:%s", + envtool.GetEnv("SMS_HOST", "sms"), + envtool.GetEnv("SMS_PORT", "8077")) + // Create the client interceptor using the grpc trace package. + si := grpctrace.StreamClientInterceptor(grpctrace.WithServiceName("sms-grpc-client")) + ui := grpctrace.UnaryClientInterceptor(grpctrace.WithServiceName("sms-grpc-client")) + c, err := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStreamInterceptor(si), grpc.WithUnaryInterceptor(ui)) + + if err != nil { + logger.Error().Err(err).Send() + } + + smsClient = sms.NewSMSServiceClient(c) +} + +func GetSMSClient() sms.SMSServiceClient { + smsClientOnce.Do(func() { + if smsClient != nil { + return + } + newSmsClient() + }) + + return smsClient +} + +func SetSmsClient(c sms.SMSServiceClient) { + smsClient = c +} diff --git a/services/ota_update_go/services/smtp.go b/services/ota_update_go/services/smtp.go new file mode 100644 index 0000000..97da78e --- /dev/null +++ b/services/ota_update_go/services/smtp.go @@ -0,0 +1,33 @@ +package services + +import ( + "sync" + + "github.com/fiskerinc/cloud-services/pkg/smtpclient" + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" +) + +var smtpOnce sync.Once +var smtpInstance smtpclient.SMTPInterface + +func GetSMTP() smtpclient.SMTPInterface { + smtpOnce.Do(func() { + if smtpInstance != nil { + return + } + + host := envtool.GetEnv("SMTP_HOST", "") + port := envtool.GetEnvInt("SMTP_PORT", 587) + user := envtool.GetEnv("SMTP_USER", "") + pass := envtool.GetEnv("SMTP_PASSWORD", "") + + smtpInstance = smtpclient.NewSMTP(host, port) + smtpInstance.Auth(user, pass) + }) + + return smtpInstance +} + +func SetSMTP(smtp smtpclient.SMTPInterface) { + smtpInstance = smtp +} diff --git a/services/ota_update_go/utils/constants.go b/services/ota_update_go/utils/constants.go new file mode 100644 index 0000000..fa06ccf --- /dev/null +++ b/services/ota_update_go/utils/constants.go @@ -0,0 +1,46 @@ +package utils + +import ( + "strings" + + "github.com/fiskerinc/cloud-services/pkg/utils/envtool" + + az "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/pkg/errors" +) + +type Direction int + +const ( + Up Direction = iota + Down +) + +var ( + cursorDirection = map[string]Direction{ + "up": Up, + "down": Down, + } +) + +func ParseCursorDirection(str string) (Direction, bool) { + c, ok := cursorDirection[strings.ToLower(str)] + return c, ok +} + +var ( + AzureAccount = envtool.GetEnv("AZURE_STORAGE_ACCOUNT", "REPLACE_ME") + AzureAccountKey = envtool.GetEnv("AZURE_STORAGE_ACCESS_KEY", "REPLACE_ME") + AzureTRexLogsContainerName = envtool.GetEnv("AZURE_TREX_LOGS_STORAGE_CONTAINER_NAME", "trex-logs") + ReadFileName = "raw.log" + + AzureLogsBlobPath = "https://%s.blob.core.windows.net/%s" +) + +func AzureStorageCredential() (*az.SharedKeyCredential, error) { + cred, err := az.NewSharedKeyCredential(AzureAccount, AzureAccountKey) + if err != nil { + return nil, errors.WithStack(err) + } + return cred, nil +} diff --git a/services/ota_update_go/utils/fileencryptor.go b/services/ota_update_go/utils/fileencryptor.go new file mode 100644 index 0000000..0eeedf2 --- /dev/null +++ b/services/ota_update_go/utils/fileencryptor.go @@ -0,0 +1,104 @@ +package utils + +import ( + "otaupdate/services" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/security" + "github.com/fiskerinc/cloud-services/pkg/utils/randomvalues" +) + +type FileEncryptor struct { + FileID string + encrypter security.IEncryptor + streamer security.IEncryptedStream + fileKey *common.FileKey +} + +func NewEncryptor() (*FileEncryptor, error) { + instance := FileEncryptor{} + err := instance.init() + return &instance, err +} + +func (fe *FileEncryptor) SaveFileKey()(err error){ + err = fe.saveData(fe.fileKey) + return +} + +func (fe *FileEncryptor) init() error { + filekey, err := fe.getKey() + if err != nil { + return err + } + + fe.fileKey = filekey + /* err = fe.saveData(filekey) + if err != nil { + return err + } */ + + encrypter, _, err := security.NewEncryptor(filekey.Key, filekey.Auth, filekey.Nonce) + if err != nil { + return err + } + + streamer, err := security.NewEncryptedStream(encrypter, security.WithUniqueId([]byte(filekey.FileID))) + if err != nil { + return err + } + + fe.FileID = filekey.FileID + fe.encrypter = encrypter + fe.streamer = streamer + + return nil +} + +func (fe *FileEncryptor) saveData(filekey *common.FileKey) error { + _, err := services.GetDB().GetFileKeys().Insert(*filekey) + return err +} + +func (fe *FileEncryptor) getKey() (*common.FileKey, error) { + var err error + generator := randomvalues.NewGenerator("") + filekey := common.FileKey{} + + filekey.FileID, err = generator.GetUniformDistHex() + if err != nil { + return nil, err + } + + value, err := generator.GetBytes(32) + if err != nil { + return nil, err + } + filekey.Key = value + + value, err = generator.GetBytes(16) + if err != nil { + return nil, err + } + filekey.Auth = value + + value, err = generator.GetBytes(12) + if err != nil { + return nil, err + } + filekey.Nonce = value + + return &filekey, nil + +} + +func (fe *FileEncryptor) Encrypt(input []byte) []byte { + return fe.streamer.Write(input) +} + +func (fe *FileEncryptor) Close() { + fe.FileID = "" + fe.encrypter.Close() + fe.encrypter = nil + fe.streamer = nil +} diff --git a/services/ota_update_go/utils/formparser.go b/services/ota_update_go/utils/formparser.go new file mode 100644 index 0000000..40b521f --- /dev/null +++ b/services/ota_update_go/utils/formparser.go @@ -0,0 +1,73 @@ +package utils + +import ( + "mime/multipart" + "strconv" + "strings" + + "github.com/fiskerinc/cloud-services/pkg/common" + "github.com/fiskerinc/cloud-services/pkg/utils" +) + +// GetUpdatePackgeFormParser returns form parser function to parse FormData +func GetUpdateManifestFileFormParser() (utils.FormParser, *common.UpdateManifestFile) { + data := common.UpdateManifestFile{ + WriteRegion: common.MemoryRegion{ + Length: 1, // Set to non-zero to pass init validation, actual value will be set and checked later + }, + } + + return func(part *multipart.Part) error { + var err error + switch part.FormName() { + case "manifest_ecu_id": + data.UpdateManifestECUID, err = utils.PartReadInt64(part, 2048, 64) + case "url": + data.URL, err = utils.PartReadAll(part, 500) + case "offset": + data.WriteRegion.Offset, err = utils.PartReadUInt64(part, 255, 64) + case "checksum": + data.Checksum, err = utils.PartReadAll(part, 32) + case "order": + data.FileOrder, err = utils.PartReadInt(part, 255) + case "signature": + data.Signature, err = utils.PartReadAll(part, 129) + case "type": + value, err := utils.PartReadAll(part, 255) + if err != nil { + return err + } + data.FileType = convertFileType(value) + case "parsed_file": + stringBool, err := utils.PartReadAll(part, 32) + if err != nil { + return err + } + aBool, err := strconv.ParseBool(stringBool) + if err != nil { + return err + } + data.Parsed = &aBool + } + + return err + }, &data +} + +func convertFileType(filetype string) common.ManifestFileType { + value := strings.ToLower(filetype) + + if strings.Contains(value, string(common.Bootloader)) { + return common.Bootloader + } + + if strings.Contains(value, string(common.Software)) || strings.Contains(value, "sw") { + return common.Software + } + + if strings.Contains(value, string(common.Calibration)) { + return common.Calibration + } + + return common.Other +} diff --git a/services/ota_update_go/utils/reader.go b/services/ota_update_go/utils/reader.go new file mode 100644 index 0000000..4544274 --- /dev/null +++ b/services/ota_update_go/utils/reader.go @@ -0,0 +1,62 @@ +package utils + +import ( + "context" + "fmt" + "io" + "net/url" + + "github.com/fiskerinc/cloud-services/pkg/remotefileupload" + "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/pkg/errors" +) + +func ReadAzureBlob(azureContainer string, vin string, year, month, day int, offset, count int64, readDirection Direction) ([]byte, int64, error) { + creds, err := AzureStorageCredential() + if err != nil { + return nil, -1, err + } + + link, err := remotefileupload.AzureFilePathLink(AzureAccount, azureContainer, vin, fmt.Sprintf("%04d", year), fmt.Sprintf("%02d", month), fmt.Sprintf("%02d", day), ReadFileName) + if err != nil { + return nil, 0, err + } + u, err := url.Parse(link) + if err != nil { + return nil, -1, errors.WithStack(err) + } + + url := azblob.NewBlobURL(*u, azblob.NewPipeline(creds, azblob.PipelineOptions{Retry: azblob.RetryOptions{MaxTries: 100}})) + + prop, err := url.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, -1, err + } + if offset >= prop.ContentLength() { + return nil, -1, errors.New("offset out of range") + } + //count from the end of the blob + readOffset := prop.ContentLength() - offset + + begin := readOffset + if readDirection == Up { + begin -= count + } + //just read the rest + if begin <= 0 { + begin = 0 + count = readOffset + } + if count == 0 { + count = azblob.CountToEnd + } + reader, err := url.Download(context.Background(), begin, count, + azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, -1, err + } + buf := make([]byte, reader.ContentLength()) + + _, err = io.ReadAtLeast(reader.Body(azblob.RetryReaderOptions{}), buf, len(buf)) + return buf, prop.ContentLength(), err +}