From 45a0cb17dd6d79cd8153753a8581471179101da0 Mon Sep 17 00:00:00 2001 From: Sven Urbanski Date: Mon, 7 Oct 2024 18:53:10 +0200 Subject: [PATCH] feat: new endpoint to get app details (#2004) Ref: SRX-RH7CD5 --------- Co-authored-by: Miguel Crespo Co-authored-by: Miguel Crespo <162333021+miguel-crespo-fdc@users.noreply.github.com> --- pkg/api/v1/api.proto | 39 +++ pkg/db/db.go | 311 ++++++++++++++++++ services/cd-service/pkg/cmd/server.go | 1 + services/cd-service/pkg/service/overview.go | 206 ++++++++++++ .../cd-service/pkg/service/overview_test.go | 182 ++++++++++ services/frontend-service/pkg/cmd/server.go | 6 + .../pkg/handler/commit_deployments_test.go | 7 +- .../pkg/versions/versions_test.go | 22 +- 8 files changed, 765 insertions(+), 9 deletions(-) diff --git a/pkg/api/v1/api.proto b/pkg/api/v1/api.proto index 461519da2..f205ddcdc 100644 --- a/pkg/api/v1/api.proto +++ b/pkg/api/v1/api.proto @@ -377,12 +377,51 @@ service VersionService { service OverviewService { rpc GetOverview (GetOverviewRequest) returns (GetOverviewResponse) {} rpc StreamOverview (GetOverviewRequest) returns (stream GetOverviewResponse) {} + + rpc GetAppDetails (GetAppDetailsRequest) returns (GetAppDetailsResponse) {} } service EnvironmentService { rpc GetEnvironmentConfig(GetEnvironmentConfigRequest) returns (GetEnvironmentConfigResponse) {} } +message GetAppDetailsRequest { + string app_name = 1; +} + +message GetAppDetailsResponse { + Application application = 1; //General Application information + map deployments = 2; // Env -> Release + map app_locks = 3; //EnvName -> []AppLocks + map team_locks= 4; //EnvName -> []TeamLocks +} + +//Wrapper over array of locks +message Locks { + repeated Lock locks = 2; +} + +message Deployment { + message DeploymentMetaData { + string deploy_author = 1; + // we use a string here, because the UI cannot handle int64 as a type. + // the string contains the unix timestamps in seconds (utc) + string deploy_time = 2; + + string ci_link = 3; + } + // version=0 means "nothing is deployed" + uint64 version = 2; + // "version" describes the currently deployed version. "queuedVersion" describes a version that was to be deployed, but a lock stopped the deployment: + // "queuedVersion" has nothing to do with queue.go + // queued_version=0 means "nothing is queued" + uint64 queued_version = 4; + // google.protobuf.Timestamp deploy_date = 5; // This is never used + bool undeploy_version = 6; + DeploymentMetaData deployment_meta_data = 7; +} + + message GetOverviewRequest { // Retrieve the overview at a certain state of the repository. If it's empty, the latest commit will be used. string git_revision = 1; diff --git a/pkg/db/db.go b/pkg/db/db.go index bc2548946..aabb95a81 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -1668,6 +1668,55 @@ func (h *DBHandler) DBSelectLatestDeployment(ctx context.Context, tx *sql.Tx, ap return processDeployment(rows) } +func (h *DBHandler) DBSelectAllLatestDeploymentsForApplication(ctx context.Context, tx *sql.Tx, appName string) (map[string]Deployment, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectAllLatestDeployments") + defer span.Finish() + + selectQuery := h.AdaptQuery( + ` + SELECT + deployments.eslVersion, + deployments.created, + deployments.appname, + deployments.releaseVersion, + deployments.envName, + deployments.metadata + FROM ( + SELECT + MAX(eslVersion) AS latest, + appname, + envname + FROM + deployments + GROUP BY + envName, appname + ) AS latest + JOIN + deployments AS deployments + ON + latest.latest=deployments.eslVersion + AND latest.appname=deployments.appname + AND latest.envName=deployments.envName + WHERE deployments.appname = (?) AND deployments.releaseVersion IS NOT NULL ;`) + + span.SetTag("query", selectQuery) + rows, err := tx.QueryContext( + ctx, + selectQuery, + appName, + ) + if err != nil { + return nil, fmt.Errorf("could not select deployment from DB. Error: %w\n", err) + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("deployments: row closing error: %v", err) + } + }(rows) + return processAllLatestDeploymentsForApp(rows) +} + func (h *DBHandler) DBSelectAllLatestDeployments(ctx context.Context, tx *sql.Tx, envName string) (map[string]*int64, error) { span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectAllLatestDeployments") defer span.Finish() @@ -1740,6 +1789,48 @@ func processAllLatestDeployments(rows *sql.Rows) (map[string]*int64, error) { return result, nil } +func processAllLatestDeploymentsForApp(rows *sql.Rows) (map[string]Deployment, error) { + result := make(map[string]Deployment) + for rows.Next() { + var curr = Deployment{ + EslVersion: 0, + Created: time.Time{}, + Env: "", + App: "", + Version: nil, + Metadata: DeploymentMetadata{ + DeployedByName: "", + DeployedByEmail: "", + CiLink: "", + }, + TransformerID: 0, + } + var releaseVersion sql.NullInt64 + var jsonMetadata string + err := rows.Scan(&curr.EslVersion, &curr.Created, &curr.App, &releaseVersion, &curr.Env, &jsonMetadata) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("Error scanning deployments row from DB. Error: %w\n", err) + } + + if releaseVersion.Valid { + curr.Version = &releaseVersion.Int64 + } + result[curr.Env] = curr + } + err := rows.Close() + if err != nil { + return nil, fmt.Errorf("deployments: row closing error: %v\n", err) + } + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("deployments: row has error: %v\n", err) + } + return result, nil +} + func (h *DBHandler) DBSelectSpecificDeployment(ctx context.Context, tx *sql.Tx, appSelector string, envSelector string, releaseVersion uint64) (*Deployment, error) { span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectSpecificDeployment") defer span.Finish() @@ -3472,7 +3563,116 @@ func (h *DBHandler) DBSelectAppLock(ctx context.Context, tx *sql.Tx, environment return nil, err } return nil, nil // no rows, but also no error +} + +func (h *DBHandler) DBSelectAllActiveAppLocksForApp(ctx context.Context, tx *sql.Tx, appName string) ([]ApplicationLock, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectAllActiveAppLocksForApp") + defer span.Finish() + + if h == nil { + return nil, nil + } + if tx == nil { + return nil, fmt.Errorf("DBSelectAllActiveAppLocksForApp: no transaction provided") + } + var appLocks []ApplicationLock + var rows *sql.Rows + defer func(rows *sql.Rows) { + if rows == nil { + return + } + err := rows.Close() + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("row closing error: %v", err) + } + }(rows) + //Get the latest change to each lock + var err error + selectQuery := h.AdaptQuery( + ` + SELECT + app_locks.eslversion, + app_locks.appname, + app_locks.envName, + app_locks.lockid, + app_locks.deleted, + app_locks.created, + app_locks.metadata + FROM ( + SELECT + MAX(eslVersion) AS latest, + appname, + envName, + lockid + FROM + "app_locks" + GROUP BY + envName, appName, lockid + ) AS latest + JOIN + app_locks AS app_locks + ON + latest.latest=app_locks.eslVersion + AND latest.appname=app_locks.appname + AND latest.envName=app_locks.envName + AND latest.lockid=app_locks.lockid + WHERE deleted = false + AND app_locks.appName = (?); + `) + rows, err = tx.QueryContext(ctx, selectQuery, appName) + if err != nil { + return nil, fmt.Errorf("could not query application locks table from DB. Error: %w\n", err) + } + + if err != nil { + return nil, fmt.Errorf("could not query releases table from DB. Error: %w\n", err) + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("releases: row could not be closed: %v", err) + } + }(rows) + + for rows.Next() { + var row = ApplicationLock{ + EslVersion: 0, + Created: time.Time{}, + LockID: "", + Env: "", + App: "", + Deleted: false, + Metadata: LockMetadata{ + CreatedAt: time.Time{}, + CreatedByEmail: "", + CreatedByName: "", + Message: "", + CiLink: "", + }, + } + var metadataJson string + err := rows.Scan(&row.EslVersion, &row.App, &row.Env, &row.LockID, &row.Deleted, &row.Created, &metadataJson) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("Error scanning releases row from DB. Error: %w\n", err) + } + + err = json.Unmarshal(([]byte)(metadataJson), &row.Metadata) + if err != nil { + return nil, fmt.Errorf("Error during json unmarshal. Error: %w. Data: %s\n", err, row.Metadata) + } + + appLocks = append(appLocks, row) + } + + err = closeRows(rows) + if err != nil { + return nil, err + } + return appLocks, nil } func (h *DBHandler) DBSelectAppLockSet(ctx context.Context, tx *sql.Tx, environment, appName string, lockIDs []string) ([]ApplicationLock, error) { @@ -3983,6 +4183,117 @@ func (h *DBHandler) DBWriteAllTeamLocks(ctx context.Context, transaction *sql.Tx return nil } +func (h *DBHandler) DBSelectAllActiveTeamLocksForTeam(ctx context.Context, tx *sql.Tx, teamName string) ([]TeamLock, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectAllActiveAppLocksForApp") + defer span.Finish() + + if h == nil { + return nil, nil + } + if tx == nil { + return nil, fmt.Errorf("DBSelectAllActiveAppLocksForApp: no transaction provided") + } + + var appLocks []TeamLock + var rows *sql.Rows + + defer func(rows *sql.Rows) { + if rows == nil { + return + } + err := rows.Close() + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("row closing error: %v", err) + } + }(rows) + //Get the latest change to each lock + var err error + selectQuery := h.AdaptQuery( + ` + SELECT + team_locks.eslversion, + team_locks.teamName, + team_locks.envName, + team_locks.lockid, + team_locks.deleted, + team_locks.created, + team_locks.metadata + FROM ( + SELECT + MAX(eslVersion) AS latest, + teamName, + envName, + lockid + FROM + "team_locks" + GROUP BY + envName, teamName, lockid + ) AS latest + JOIN + team_locks AS team_locks + ON + latest.latest=team_locks.eslVersion + AND latest.teamName=team_locks.teamName + AND latest.envName=team_locks.envName + AND latest.lockid=team_locks.lockid + WHERE deleted = false + AND team_locks.teamName = (?); + `) + rows, err = tx.QueryContext(ctx, selectQuery, teamName) + if err != nil { + return nil, fmt.Errorf("could not query application locks table from DB. Error: %w\n", err) + } + + if err != nil { + return nil, fmt.Errorf("could not query releases table from DB. Error: %w\n", err) + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("releases: row could not be closed: %v", err) + } + }(rows) + + for rows.Next() { + var row = TeamLock{ + EslVersion: 0, + Created: time.Time{}, + LockID: "", + Env: "", + Team: "", + Deleted: false, + Metadata: LockMetadata{ + CreatedAt: time.Time{}, + CreatedByEmail: "", + CreatedByName: "", + Message: "", + CiLink: "", + }, + } + var metadataJson string + err := rows.Scan(&row.EslVersion, &row.Team, &row.Env, &row.LockID, &row.Deleted, &row.Created, &metadataJson) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("Error scanning releases row from DB. Error: %w\n", err) + } + + err = json.Unmarshal(([]byte)(metadataJson), &row.Metadata) + if err != nil { + return nil, fmt.Errorf("Error during json unmarshal. Error: %w. Data: %s\n", err, row.Metadata) + } + + appLocks = append(appLocks, row) + } + + err = closeRows(rows) + if err != nil { + return nil, err + } + return appLocks, nil +} + func (h *DBHandler) DBSelectTeamLock(ctx context.Context, tx *sql.Tx, environment, teamName, lockID string) (*TeamLock, error) { span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectTeamLock") defer span.Finish() diff --git a/services/cd-service/pkg/cmd/server.go b/services/cd-service/pkg/cmd/server.go index 41da97c83..ac0e477cd 100755 --- a/services/cd-service/pkg/cmd/server.go +++ b/services/cd-service/pkg/cmd/server.go @@ -434,6 +434,7 @@ func RunServer() { RepositoryConfig: cfg, Shutdown: shutdownCh, Context: ctx, + DBHandler: dbHandler, } api.RegisterOverviewServiceServer(srv, overviewSrv) api.RegisterGitServiceServer(srv, &service.GitServer{Config: cfg, OverviewService: overviewSrv, PageSize: 10}) diff --git a/services/cd-service/pkg/service/overview.go b/services/cd-service/pkg/service/overview.go index 3ae118dc8..665fee2a6 100644 --- a/services/cd-service/pkg/service/overview.go +++ b/services/cd-service/pkg/service/overview.go @@ -21,6 +21,9 @@ import ( "database/sql" "errors" "fmt" + "github.com/freiheit-com/kuberpult/pkg/mapper" + "google.golang.org/protobuf/types/known/timestamppb" + "os" "sync" "sync/atomic" @@ -47,6 +50,180 @@ type OverviewServiceServer struct { Context context.Context init sync.Once response atomic.Value + + DBHandler *db.DBHandler +} + +func (o *OverviewServiceServer) GetAppDetails( + ctx context.Context, + in *api.GetAppDetailsRequest) (*api.GetAppDetailsResponse, error) { + + var appName = in.AppName + var response = &api.GetAppDetailsResponse{ + Application: &api.Application{ + UndeploySummary: 0, + Warnings: nil, + Name: appName, + Releases: []*api.Release{}, + SourceRepoUrl: "", + Team: "", + }, + AppLocks: make(map[string]*api.Locks), + Deployments: make(map[string]*api.Deployment), + TeamLocks: make(map[string]*api.Locks), + } + if !o.DBHandler.ShouldUseOtherTables() { + panic("DB") + } + resultApp, err := db.WithTransactionT(o.DBHandler, ctx, 2, true, func(ctx context.Context, transaction *sql.Tx) (*api.Application, error) { + var rels []int64 + var result = &api.Application{ + UndeploySummary: 0, + Warnings: nil, + Name: appName, + Releases: []*api.Release{}, + SourceRepoUrl: "", + Team: "", + } + + // Releases + result.Name = appName + retrievedReleasesOfApp, err := o.DBHandler.DBSelectAllReleasesOfApp(ctx, transaction, appName) + if err != nil { + logger.FromContext(ctx).Sugar().Warnf("app without releases: %v", err) + } + if retrievedReleasesOfApp != nil { + rels = retrievedReleasesOfApp.Metadata.Releases + } + + for _, id := range rels { + uid := uint64(id) + // we could optimize this by making one query that does return multiples: + if rel, err := o.DBHandler.DBSelectReleaseByVersion(ctx, transaction, appName, uid, false); err != nil { + return nil, err + } else { + if rel == nil { + // ignore + } else { + var tmp = &repository.Release{ + Version: rel.ReleaseNumber, + UndeployVersion: rel.Metadata.UndeployVersion, + SourceAuthor: rel.Metadata.SourceAuthor, + SourceCommitId: rel.Metadata.SourceCommitId, + SourceMessage: rel.Metadata.SourceMessage, + CreatedAt: rel.Created, + DisplayVersion: rel.Metadata.DisplayVersion, + IsMinor: rel.Metadata.IsMinor, + IsPrepublish: rel.Metadata.IsPrepublish, + } + release := tmp.ToProto() + release.Version = uid + release.UndeployVersion = tmp.UndeployVersion + result.Releases = append(result.Releases, release) + } + } + } + + if app, err := o.DBHandler.DBSelectApp(ctx, transaction, appName); err != nil { + return nil, err + } else { + if app == nil { + return nil, fmt.Errorf("could not find app details of app: %s", appName) + } + result.Team = app.Metadata.Team + } + if response == nil { + return nil, fmt.Errorf("app not found: '%s'", appName) + } + envConfigs, err := o.Repository.State().GetAllEnvironmentConfigs(ctx, transaction) + if err != nil { + return nil, fmt.Errorf("could not find environments: %w", err) + } + envGroups := mapper.MapEnvironmentsToGroups(envConfigs) + + result.UndeploySummary = deriveUndeploySummary(appName, envGroups) + result.Warnings = db.CalculateWarnings(ctx, appName, envGroups) + + // App Locks + appLocks, err := o.DBHandler.DBSelectAllActiveAppLocksForApp(ctx, transaction, appName) + if err != nil { + return nil, fmt.Errorf("could not find application locks for app %s: %w", appName, err) + } + for _, currentLock := range appLocks { + if _, ok := response.AppLocks[currentLock.Env]; !ok { + response.AppLocks[currentLock.Env] = &api.Locks{Locks: make([]*api.Lock, 0)} + } + response.AppLocks[currentLock.Env].Locks = append(response.AppLocks[currentLock.Env].Locks, &api.Lock{ + LockId: currentLock.LockID, + Message: currentLock.Metadata.Message, + CreatedAt: timestamppb.New(currentLock.Metadata.CreatedAt), + CreatedBy: &api.Actor{ + Name: currentLock.Metadata.CreatedByName, + Email: currentLock.Metadata.CreatedByEmail, + }, + }) + } + + // Team Locks + teamLocks, err := o.DBHandler.DBSelectAllActiveTeamLocksForTeam(ctx, transaction, result.Team) + if err != nil { + return nil, fmt.Errorf("could not find team locks for app %s: %w", appName, err) + } + for _, currentTeamLock := range teamLocks { + if _, ok := response.TeamLocks[currentTeamLock.Env]; !ok { + response.TeamLocks[currentTeamLock.Env] = &api.Locks{Locks: make([]*api.Lock, 0)} + } + response.TeamLocks[currentTeamLock.Env].Locks = append(response.TeamLocks[currentTeamLock.Env].Locks, &api.Lock{ + LockId: currentTeamLock.LockID, + Message: currentTeamLock.Metadata.Message, + CreatedAt: timestamppb.New(currentTeamLock.Metadata.CreatedAt), + CreatedBy: &api.Actor{ + Name: currentTeamLock.Metadata.CreatedByName, + Email: currentTeamLock.Metadata.CreatedByEmail, + }, + }) + } + + // Deployments + deployments, err := o.DBHandler.DBSelectAllLatestDeploymentsForApplication(ctx, transaction, appName) + if err != nil { + return nil, fmt.Errorf("could not obtain deployments for app %s: %w", appName, err) + } + for envName, currentDeployment := range deployments { + deployment := &api.Deployment{ + Version: uint64(*currentDeployment.Version), + QueuedVersion: 0, + UndeployVersion: false, + DeploymentMetaData: &api.Deployment_DeploymentMetaData{ + CiLink: currentDeployment.Metadata.CiLink, + DeployAuthor: currentDeployment.Metadata.DeployedByName, + DeployTime: currentDeployment.Created.String(), + }, + } + if queuedVersion, err := o.Repository.State().GetQueuedVersion(ctx, transaction, envName, appName); err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } else { + if queuedVersion == nil { + deployment.QueuedVersion = 0 + } else { + deployment.QueuedVersion = *queuedVersion + } + } + if release, err := o.Repository.State().GetApplicationRelease(ctx, transaction, appName, uint64(*currentDeployment.Version)); err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } else if release != nil { + deployment.UndeployVersion = release.UndeployVersion + } + response.Deployments[envName] = deployment + } + return result, nil + }) + if err != nil { + return nil, err + } + response.Application = resultApp + return response, nil + } func (o *OverviewServiceServer) GetOverview( @@ -204,3 +381,32 @@ func (o *OverviewServiceServer) update(s *repository.State) { o.response.Store(r) o.notify.Notify() } + +func deriveUndeploySummary(appName string, groups []*api.EnvironmentGroup) api.UndeploySummary { + var allNormal = true + var allUndeploy = true + for _, group := range groups { + for _, environment := range group.Environments { + var app, exists = environment.Applications[appName] + if !exists { + continue + } + if app.Version == 0 { + // if the app exists but nothing is deployed, we ignore this + continue + } + if app.UndeployVersion { + allNormal = false + } else { + allUndeploy = false + } + } + } + if allUndeploy { + return api.UndeploySummary_UNDEPLOY + } + if allNormal { + return api.UndeploySummary_NORMAL + } + return api.UndeploySummary_MIXED +} diff --git a/services/cd-service/pkg/service/overview_test.go b/services/cd-service/pkg/service/overview_test.go index 4aa499474..56b602aa7 100644 --- a/services/cd-service/pkg/service/overview_test.go +++ b/services/cd-service/pkg/service/overview_test.go @@ -19,6 +19,7 @@ package service import ( "context" "database/sql" + "github.com/google/go-cmp/cmp/cmpopts" "sync" "testing" @@ -659,6 +660,187 @@ func TestOverviewService(t *testing.T) { } } +func TestGetApplicationDetails(t *testing.T) { + var dev = "dev" + var env = "development" + var appName = "test-app" + tcs := []struct { + Name string + Setup []repository.Transformer + AppName string + ExpectedResponse *api.GetAppDetailsResponse + }{ + { + Name: "Get App details", + AppName: appName, + ExpectedResponse: &api.GetAppDetailsResponse{ + Application: &api.Application{ + Name: appName, + Releases: []*api.Release{ + { + Version: 1, + SourceCommitId: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + SourceAuthor: "example ", + SourceMessage: "changed something (#678)", + PrNumber: "678", + CreatedAt: ×tamppb.Timestamp{Seconds: 1, Nanos: 1}, + }, + }, + Team: "team-123", + }, + Deployments: map[string]*api.Deployment{ + env: { + Version: 1, + QueuedVersion: 0, + UndeployVersion: false, + DeploymentMetaData: &api.Deployment_DeploymentMetaData{}, + }, + }, + TeamLocks: map[string]*api.Locks{ + "development": { + Locks: []*api.Lock{ + { + LockId: "my-team-lock", + Message: "team lock for team 123", + CreatedBy: &api.Actor{ + Name: "test tester", + Email: "testmail@example.com", + }, + }, + }, + }, + }, + AppLocks: map[string]*api.Locks{ + "development": { + Locks: []*api.Lock{ + { + LockId: "my-app-lock", + Message: "app lock for test-app", + CreatedBy: &api.Actor{ + Name: "test tester", + Email: "testmail@example.com", + }, + }, + }, + }, + }, + }, + Setup: []repository.Transformer{ + &repository.CreateEnvironment{ + Environment: env, + Config: config.EnvironmentConfig{ + Upstream: &config.EnvironmentConfigUpstream{ + Latest: true, + }, + ArgoCd: nil, + EnvironmentGroup: &dev, + }, + }, + &repository.CreateApplicationVersion{ + Authentication: repository.Authentication{}, + Version: 1, + SourceCommitId: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + SourceAuthor: "example ", + SourceMessage: "changed something (#678)", + Team: "team-123", + DisplayVersion: "", + WriteCommitData: true, + PreviousCommit: "", + TransformerEslVersion: 1, + Application: appName, + Manifests: map[string]string{ + env: "v1", + }, + }, + &repository.CreateEnvironmentTeamLock{ + Team: "team-123", + Environment: env, + LockId: "my-team-lock", + Message: "team lock for team 123", + }, + + &repository.CreateEnvironmentApplicationLock{ + Application: appName, + Environment: env, + LockId: "my-app-lock", + Message: "app lock for test-app", + }, + }, + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + shutdown := make(chan struct{}, 1) + var repo repository.Repository + migrationsPath, err := testutil.CreateMigrationsPath(4) + if err != nil { + t.Fatal(err) + } + dbConfig := &db.DBConfig{ + DriverName: "sqlite3", + MigrationsPath: migrationsPath, + WriteEslOnly: false, + } + repo, err = setupRepositoryTestWithDB(t, dbConfig) + if err != nil { + t.Fatal(err) + } + config := repository.RepositoryConfig{ + ArgoCdGenerateFiles: true, + DBHandler: repo.State().DBHandler, + } + svc := &OverviewServiceServer{ + Repository: repo, + RepositoryConfig: config, + DBHandler: repo.State().DBHandler, + Shutdown: shutdown, + } + + if err := repo.Apply(testutil.MakeTestContext(), tc.Setup...); err != nil { + t.Fatal(err) + } + + var ctx = auth.WriteUserToContext(testutil.MakeTestContext(), auth.User{ + Email: "app-email@example.com", + Name: "overview tester", + }) + + resp, err := svc.GetAppDetails(ctx, &api.GetAppDetailsRequest{AppName: appName}) + if err != nil { + t.Fatal(err) + } + + app := resp.Application + expected := tc.ExpectedResponse + if diff := cmp.Diff(app.Name, expected.Application.Name); diff != "" { + t.Fatalf("error mismatch (-want, +got):\n%s", diff) + } + + //Releases + if diff := cmp.Diff(expected.Application.Releases, resp.Application.Releases, cmpopts.IgnoreUnexported(api.Release{}), cmpopts.IgnoreFields(api.Release{}, "CreatedAt")); diff != "" { + t.Fatalf("error mismatch (-want, +got):\n%s", diff) + } + + //Deployments + expectedDeployment := expected.Deployments[env] + resultDeployment := resp.Deployments[env] + + if diff := cmp.Diff(expectedDeployment, resultDeployment, cmpopts.IgnoreUnexported(api.Deployment{}), cmpopts.IgnoreUnexported(api.Deployment_DeploymentMetaData{}), cmpopts.IgnoreFields(api.Deployment_DeploymentMetaData{}, "DeployTime")); diff != "" { + t.Fatalf("error mismatch (-want, +got):\n%s", diff) + } + //Locks + if diff := cmp.Diff(expected.AppLocks, resp.AppLocks, cmpopts.IgnoreUnexported(api.Locks{}), cmpopts.IgnoreUnexported(api.Lock{}), cmpopts.IgnoreFields(api.Lock{}, "CreatedAt"), cmpopts.IgnoreUnexported(api.Actor{})); diff != "" { + t.Fatalf("error mismatch (-want, +got):\n%s", diff) + } + if diff := cmp.Diff(expected.TeamLocks, resp.TeamLocks, cmpopts.IgnoreUnexported(api.Locks{}), cmpopts.IgnoreUnexported(api.Lock{}), cmpopts.IgnoreFields(api.Lock{}, "CreatedAt"), cmpopts.IgnoreUnexported(api.Actor{})); diff != "" { + t.Fatalf("error mismatch (-want, +got):\n%s", diff) + } + close(shutdown) + }) + } +} + func TestOverviewServiceFromCommit(t *testing.T) { type step struct { Transformer repository.Transformer diff --git a/services/frontend-service/pkg/cmd/server.go b/services/frontend-service/pkg/cmd/server.go index e12367a98..646dd5b40 100644 --- a/services/frontend-service/pkg/cmd/server.go +++ b/services/frontend-service/pkg/cmd/server.go @@ -650,6 +650,12 @@ func (p *GrpcProxy) GetFailedEsls( return p.EslServiceClient.GetFailedEsls(ctx, in) } +func (p *GrpcProxy) GetAppDetails( + ctx context.Context, + in *api.GetAppDetailsRequest) (*api.GetAppDetailsResponse, error) { + return p.OverviewClient.GetAppDetails(ctx, in) +} + func (p *GrpcProxy) GetOverview( ctx context.Context, in *api.GetOverviewRequest) (*api.GetOverviewResponse, error) { diff --git a/services/frontend-service/pkg/handler/commit_deployments_test.go b/services/frontend-service/pkg/handler/commit_deployments_test.go index 3d2345006..9ca276235 100644 --- a/services/frontend-service/pkg/handler/commit_deployments_test.go +++ b/services/frontend-service/pkg/handler/commit_deployments_test.go @@ -19,6 +19,7 @@ package handler import ( "context" "fmt" + "github.com/google/go-cmp/cmp" "net/http" "net/http/httptest" "testing" @@ -82,7 +83,7 @@ func TestHandleCommitDeployments(t *testing.T) { inputTail: "123456/", failGrpcCall: false, expectedStatusCode: http.StatusOK, - expectedResponse: "{\"deploymentStatus\":{\"app1\":{\"deploymentStatus\":{\"dev\":\"DEPLOYED\",\"prod\":\"UNKNOWN\",\"stage\":\"PENDING\"}}}}\n", + expectedResponse: "{\"deploymentStatus\":{\"app1\":{\"deploymentStatus\":{\"dev\":\"DEPLOYED\", \"prod\":\"UNKNOWN\", \"stage\":\"PENDING\"}}}}\n", }, } for _, tc := range tcs { @@ -100,8 +101,8 @@ func TestHandleCommitDeployments(t *testing.T) { if w.Code != tc.expectedStatusCode { t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, w.Code) } - if w.Body.String() != tc.expectedResponse { - t.Errorf("expected response %s, got %s", tc.expectedResponse, w.Body.String()) + if diff := cmp.Diff(tc.expectedResponse, w.Body.String()); diff != "" { + t.Errorf("response mismatch (-want, +got):\\n%s", diff) } }) } diff --git a/services/rollout-service/pkg/versions/versions_test.go b/services/rollout-service/pkg/versions/versions_test.go index 5b7c87904..26f7a7dbe 100644 --- a/services/rollout-service/pkg/versions/versions_test.go +++ b/services/rollout-service/pkg/versions/versions_test.go @@ -64,12 +64,13 @@ type mockOverviewStreamMessage struct { type mockOverviewClient struct { grpc.ClientStream - Responses map[string]*api.GetOverviewResponse - LastMetadata metadata.MD - StartStep chan struct{} - Steps chan step - savedStep *step - current int + Responses map[string]*api.GetOverviewResponse + AppDetailsResponses map[string]*api.GetAppDetailsResponse + LastMetadata metadata.MD + StartStep chan struct{} + Steps chan step + savedStep *step + current int } // GetOverview implements api.OverviewServiceClient @@ -81,6 +82,15 @@ func (m *mockOverviewClient) GetOverview(ctx context.Context, in *api.GetOvervie return nil, status.Error(codes.Unknown, "no") } +// GetOverview implements api.GetAppDetails +func (m *mockOverviewClient) GetAppDetails(ctx context.Context, in *api.GetAppDetailsRequest, opts ...grpc.CallOption) (*api.GetAppDetailsResponse, error) { + m.LastMetadata, _ = metadata.FromOutgoingContext(ctx) + if resp := m.AppDetailsResponses[in.AppName]; resp != nil { + return resp, nil + } + return nil, status.Error(codes.Unknown, "no") +} + // StreamOverview implements api.OverviewServiceClient func (m *mockOverviewClient) StreamOverview(ctx context.Context, in *api.GetOverviewRequest, opts ...grpc.CallOption) (api.OverviewService_StreamOverviewClient, error) { m.StartStep <- struct{}{}