diff --git a/.gitignore b/.gitignore index ded2bd9..b8a066b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ local-build-commands.txt .idea +config/*test* + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e09f6d..d6cecd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Upcoming changes... -- + +## [0.7.0] - 2026-01-30 +### Added +- Added database version info (`schema_version`, `created_at`) to `StatusResponse` across all component service endpoints +- Added server version to `StatusResponse` +- Log database version info on service startup +### Changed +- Moved server version from constructor parameter to `ServerConfig.App.Version`, configurable via `APP_VERSION` env var (defaults to embedded binary version) +- Log error when querying db version fails with an error other than `ErrTableNotFound` +- Updated `github.com/scanoss/go-models` to v0.3.0 +- Updated `github.com/scanoss/go-grpc-helper` to v0.11.0 + ## [0.6.0] - 2025-09-18 ### Added - Added `name` field to component search and version response DTOs @@ -28,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - ? +[0.7.0]: https://github.com/scanoss/components/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/scanoss/components/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/scanoss/components/compare/v0.4.0...v0.5.0 [0.0.1]: https://github.com/scanoss/components/compare/v0.0.0...v0.0.1 diff --git a/go.mod b/go.mod index 12e7561..db385fb 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,10 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 - github.com/scanoss/go-grpc-helper v0.9.0 + github.com/scanoss/go-grpc-helper v0.11.0 + github.com/scanoss/go-models v0.4.0 github.com/scanoss/go-purl-helper v0.2.1 - github.com/scanoss/papi v0.21.0 + github.com/scanoss/papi v0.28.0 github.com/scanoss/zap-logging-helper v0.4.0 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/metric v1.38.0 diff --git a/go.sum b/go.sum index 82b10f5..b4c5901 100644 --- a/go.sum +++ b/go.sum @@ -616,14 +616,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/scanoss/go-grpc-helper v0.9.0 h1:lK9JtKtyOqR9XxjnYR0wbW5KCNDH82P1E1LJwwP5Xb4= -github.com/scanoss/go-grpc-helper v0.9.0/go.mod h1:EPI1NBg+DJ+krWehxC9eHyNpM5Pii5odOJcNdNG9qA0= +github.com/scanoss/go-grpc-helper v0.11.0 h1:DifUX7KrQObTo9ta/vc4vqSzAdDEy1yNl+zWKuX5iOc= +github.com/scanoss/go-grpc-helper v0.11.0/go.mod h1:p2lhQTs6X5Y4E2F50qG6DbGpATtX/YYMycEcFwo9XVE= +github.com/scanoss/go-models v0.4.0 h1:TPAWgFzseChYe12RHVcsfdouZH8AleiPphKA7TwOd04= +github.com/scanoss/go-models v0.4.0/go.mod h1:Dq8ag9CI/3h0sqDWYUrTjW/jO8l5L6oopWJRKtJxzqA= github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDfNSu/9As= github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= -github.com/scanoss/papi v0.21.0 h1:aVt0q9pxaPHMq3QsFFnlIXju1NYpou/ziweWdIIkkPs= -github.com/scanoss/papi v0.21.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= +github.com/scanoss/papi v0.28.0 h1:uvevFYoxwzvSH1hvgBoAkScIGTK2U1+rLzHSoJdnARk= +github.com/scanoss/papi v0.28.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/pkg/cmd/server.go b/pkg/cmd/server.go index a84711f..e82992c 100644 --- a/pkg/cmd/server.go +++ b/pkg/cmd/server.go @@ -27,6 +27,7 @@ import ( "github.com/scanoss/go-grpc-helper/pkg/files" gd "github.com/scanoss/go-grpc-helper/pkg/grpc/database" gs "github.com/scanoss/go-grpc-helper/pkg/grpc/server" + gomodels "github.com/scanoss/go-models/pkg/models" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" _ "modernc.org/sqlite" "net/http" @@ -38,6 +39,9 @@ import ( "strings" ) +//TODO: Now the config includes the app version. +// This might be worth moving to the file pkg/config/server_config.go + //go:generate bash ../../get_version.sh //go:embed version.txt var version string @@ -96,7 +100,11 @@ func RunServer() error { return err } - zlog.S.Infof("Starting SCANOSS Component Service: %v", strings.TrimSpace(version)) + // Set the default version from the embedded binary version if not overridden by config/env + if len(cfg.App.Version) == 0 { + cfg.App.Version = strings.TrimSpace(version) + } + zlog.S.Infof("Starting SCANOSS Component Service: %v", cfg.App.Version) // Setup database connection pool db, err := gd.OpenDBConnection(cfg.Database.Dsn, cfg.Database.Driver, cfg.Database.User, cfg.Database.Passwd, @@ -108,6 +116,17 @@ func RunServer() error { return err } defer gd.CloseDBConnection(db) + // Log database version info + dbVersionModel := gomodels.NewDBVersionModel(db) + dbVersion, dbVersionErr := dbVersionModel.GetCurrentVersion(context.Background()) + if dbVersionErr != nil { + zlog.S.Warnf("Could not read db_version table: %v", dbVersionErr) + } else if len(dbVersion.SchemaVersion) > 0 { + zlog.S.Infof("Loaded decoration DB: package=%s, schema=%s, created_at=%s", + dbVersion.PackageName, dbVersion.SchemaVersion, dbVersion.CreatedAt) + } else { + zlog.S.Warn("db_version table is empty") + } // Setup dynamic logging (if necessary) zlog.SetupAppDynamicLogging(cfg.Logging.DynamicPort, cfg.Logging.DynamicLogging) // Register the component service @@ -121,7 +140,7 @@ func RunServer() error { } } // Start the gRPC service - server, err := grpc.RunServer(cfg, v2API, cfg.App.GRPCPort, allowedIPs, deniedIPs, startTLS, version) + server, err := grpc.RunServer(cfg, v2API, cfg.App.GRPCPort, allowedIPs, deniedIPs, startTLS) if err != nil { return err } diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index d0e0f4c..58c10ea 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -30,6 +30,7 @@ const ( type ServerConfig struct { App struct { Name string `env:"APP_NAME"` + Version string `env:"APP_VERSION"` GRPCPort string `env:"APP_PORT"` RESTPort string `env:"REST_PORT"` Debug bool `env:"APP_DEBUG"` // true/false diff --git a/pkg/protocol/grpc/server.go b/pkg/protocol/grpc/server.go index 7ced095..9ccf02e 100644 --- a/pkg/protocol/grpc/server.go +++ b/pkg/protocol/grpc/server.go @@ -28,12 +28,12 @@ import ( // RunServer runs gRPC service to publish. func RunServer(config *myconfig.ServerConfig, v2API pb.ComponentsServer, port string, - allowedIPs, deniedIPs []string, startTLS bool, version string) (*grpc.Server, error) { + allowedIPs, deniedIPs []string, startTLS bool) (*grpc.Server, error) { // Start up Open Telemetry is requested var oltpShutdown = func() {} if config.Telemetry.Enabled { var err error - oltpShutdown, err = otel.InitTelemetryProviders(config.App.Name, "scanoss-components", version, + oltpShutdown, err = otel.InitTelemetryProviders(config.App.Name, "scanoss-components", config.App.Version, config.Telemetry.OltpExporter, otel.GetTraceSampler(config.App.Mode), false) if err != nil { return nil, err diff --git a/pkg/service/component_service.go b/pkg/service/component_service.go index d8f78b0..ab08a16 100644 --- a/pkg/service/component_service.go +++ b/pkg/service/component_service.go @@ -19,26 +19,34 @@ package service import ( "context" + "errors" + "time" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/jmoiron/sqlx" "github.com/scanoss/go-grpc-helper/pkg/grpc/database" + gomodels "github.com/scanoss/go-models/pkg/models" common "github.com/scanoss/papi/api/commonv2" pb "github.com/scanoss/papi/api/componentsv2" myconfig "scanoss.com/components/pkg/config" se "scanoss.com/components/pkg/errors" "scanoss.com/components/pkg/usecase" - "time" ) type componentServer struct { pb.ComponentsServer - db *sqlx.DB - config *myconfig.ServerConfig + db *sqlx.DB + config *myconfig.ServerConfig + dbVersionModel *gomodels.DBVersionModel } func NewComponentServer(db *sqlx.DB, config *myconfig.ServerConfig) pb.ComponentsServer { setupMetrics() - return &componentServer{db: db, config: config} + return &componentServer{ + db: db, + config: config, + dbVersionModel: gomodels.NewDBVersionModel(db), + } } // Echo sends back the same message received @@ -54,30 +62,47 @@ func (d componentServer) SearchComponents(ctx context.Context, request *pb.CompS s := ctxzap.Extract(ctx).Sugar() s.Info("Processing component name request...") if len(request.Search) == 0 && len(request.Component) == 0 && len(request.Vendor) == 0 { - return &pb.CompSearchResponse{Status: se.HandleServiceError(ctx, s, se.NewBadRequestError("No data supplied", nil))}, nil + status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No data supplied", nil)) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompSearchResponse{Status: status}, nil } dtoRequest, err := convertSearchComponentInput(s, request) // Convert to internal DTO for processing if err != nil { - return &pb.CompSearchResponse{Status: se.HandleServiceError(ctx, s, err)}, nil + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompSearchResponse{Status: status}, nil } // Search the KB for information about the components compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) dtoComponents, err := compUc.SearchComponents(dtoRequest) if err != nil { - return &pb.CompSearchResponse{Status: se.HandleServiceError(ctx, s, err)}, nil + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompSearchResponse{Status: status}, nil } s.Debugf("Parsed Components: %+v", dtoComponents) componentsResponse, err := convertSearchComponentOutput(s, dtoComponents) // Convert the internal data into a response object if err != nil { s.Errorf("Failed to convert parsed components: %v", err) - statusResp := common.StatusResponse{Status: common.StatusCode_FAILED, Message: "Problems encountered extracting components data"} - return &pb.CompSearchResponse{Status: &statusResp}, nil + return &pb.CompSearchResponse{Status: &common.StatusResponse{ + Status: common.StatusCode_FAILED, + Message: "Problems encountered extracting components data", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }}, nil } telemetryCompNameRequestTime(ctx, d.config, requestStartTime) // Record the request processing time // Set the status and respond with the data - statusResp := common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success"} - return &pb.CompSearchResponse{Components: componentsResponse.Components, Status: &statusResp}, nil + return &pb.CompSearchResponse{Components: componentsResponse.Components, Status: &common.StatusResponse{ + Status: common.StatusCode_SUCCESS, + Message: "Success", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }}, nil } func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.CompVersionRequest) (*pb.CompVersionResponse, error) { @@ -87,30 +112,47 @@ func (d componentServer) GetComponentVersions(ctx context.Context, request *pb.C s.Info("Processing component versions request...") //Verify the input request if len(request.Purl) == 0 { - return &pb.CompVersionResponse{Status: se.HandleServiceError(ctx, s, se.NewBadRequestError("No purl supplied", nil))}, nil + status := se.HandleServiceError(ctx, s, se.NewBadRequestError("No purl supplied", nil)) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompVersionResponse{Status: status}, nil } //Convert the request to internal DTO dtoRequest, err := convertCompVersionsInput(s, request) if err != nil { - return &pb.CompVersionResponse{Status: se.HandleServiceError(ctx, s, err)}, nil + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompVersionResponse{Status: status}, nil } // Creates the use case compUc := usecase.NewComponents(ctx, s, d.db, database.NewDBSelectContext(s, d.db, nil, d.config.Database.Trace)) dtoOutput, err := compUc.GetComponentVersions(dtoRequest) if err != nil { - return &pb.CompVersionResponse{Status: se.HandleServiceError(ctx, s, err)}, nil + status := se.HandleServiceError(ctx, s, err) + status.Db = d.getDBVersion() + status.Server = &common.StatusResponse_Server{Version: d.config.App.Version} + return &pb.CompVersionResponse{Status: status}, nil } reqResponse, err := convertCompVersionsOutput(s, dtoOutput) if err != nil { s.Errorf("Failed to convert parsed components: %v", err) - statusResp := common.StatusResponse{Status: common.StatusCode_FAILED, Message: "Problems encountered extracting components data"} - return &pb.CompVersionResponse{Status: &statusResp}, nil + return &pb.CompVersionResponse{Status: &common.StatusResponse{ + Status: common.StatusCode_FAILED, + Message: "Problems encountered extracting components data", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }}, nil } telemetryCompVersionRequestTime(ctx, d.config, requestStartTime) // Set the status and respond with the data - statusResp := common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success"} - return &pb.CompVersionResponse{Component: reqResponse.Component, Status: &statusResp}, nil + return &pb.CompVersionResponse{Component: reqResponse.Component, Status: &common.StatusResponse{ + Status: common.StatusCode_SUCCESS, + Message: "Success", + Db: d.getDBVersion(), + Server: &common.StatusResponse_Server{Version: d.config.App.Version}, + }}, nil } // telemetryCompNameRequestTime records the name request time to telemetry. @@ -128,3 +170,23 @@ func telemetryCompVersionRequestTime(ctx context.Context, config *myconfig.Serve oltpMetrics.compVersionHistogram.Record(ctx, elapsedTime) // Record dep request time } } + +// getDBVersion fetches the database version from the db_version table. +// Returns nil if the table doesn't exist or query fails (backward compatibility). +func (d componentServer) getDBVersion() *common.StatusResponse_DB { + dbVersion, err := d.dbVersionModel.GetCurrentVersion(context.Background()) + if err != nil { + if !errors.Is(err, gomodels.ErrTableNotFound) { + s := ctxzap.Extract(context.Background()).Sugar() + s.Errorf("Failed to get db version: %v", err) + } + return nil + } + if len(dbVersion.SchemaVersion) == 0 { + return nil + } + return &common.StatusResponse_DB{ + SchemaVersion: dbVersion.SchemaVersion, + CreatedAt: dbVersion.CreatedAt, + } +} diff --git a/pkg/service/component_service_test.go b/pkg/service/component_service_test.go index dd7d4b3..a5452fb 100644 --- a/pkg/service/component_service_test.go +++ b/pkg/service/component_service_test.go @@ -48,6 +48,7 @@ func TestComponentServer_Echo(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } + myConfig.App.Version = "test-version" s := NewComponentServer(db, myConfig) type args struct { @@ -106,6 +107,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } + myConfig.App.Version = "test-version" s := NewComponentServer(db, myConfig) var compRequestData = `{ @@ -137,7 +139,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &compReq, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria"}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No components found matching the search criteria", Server: &common.StatusResponse_Server{Version: "test-version"}}}, }, { name: "Search for a empty request", @@ -146,7 +148,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { ctx: ctx, req: &pb.CompSearchRequest{}, }, - want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied"}}, + want: &pb.CompSearchResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No data supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, wantErr: false, }, } @@ -159,7 +161,7 @@ func TestComponentServer_SearchComponents(t *testing.T) { return } if err == nil && !reflect.DeepEqual(got.Status, tt.want.Status) { - t.Errorf("service.SearchComponents() = %v, want %v", got, tt.want) + t.Errorf("service.SearchComponents() status = %v, want %v", got.Status, tt.want.Status) } }) } @@ -186,6 +188,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { if err != nil { t.Fatalf("failed to load Config: %v", err) } + myConfig.App.Version = "test-version" s := NewComponentServer(db, myConfig) var compVersionRequestData = `{ @@ -216,7 +219,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &compVersionReq, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success"}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_SUCCESS, Message: "Success", Server: &common.StatusResponse_Server{Version: "test-version"}}}, }, { name: "Search for a empty request", @@ -225,7 +228,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { ctx: ctx, req: &pb.CompVersionRequest{}, }, - want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied"}}, + want: &pb.CompVersionResponse{Status: &common.StatusResponse{Status: common.StatusCode_FAILED, Message: "No purl supplied", Server: &common.StatusResponse_Server{Version: "test-version"}}}, wantErr: false, }, } @@ -238,7 +241,7 @@ func TestComponentServer_GetComponentVersions(t *testing.T) { return } if err == nil && !reflect.DeepEqual(got.Status, tt.want.Status) { - t.Errorf("service.SearchComponents() = %v, want %v", got, tt.want) + t.Errorf("service.GetComponentVersions() status = %v, want %v", got.Status, tt.want.Status) } }) }