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

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

View File

@@ -0,0 +1,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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}