diff --git a/BUILD.bazel b/BUILD.bazel index 27dfd1215a..483823b2c4 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -51,6 +51,7 @@ genrule( # gazelle:exclude pkg/model/deployment.pb.validate.go # gazelle:exclude pkg/model/environment.pb.validate.go # gazelle:exclude pkg/model/event.pb.validate.go +# gazelle:exclude pkg/model/insight.pb.validate.go # gazelle:exclude pkg/model/logblock.pb.validate.go # gazelle:exclude pkg/model/piped.pb.validate.go # gazelle:exclude pkg/model/piped_stats.pb.validate.go diff --git a/pkg/app/api/grpcapi/web_api.go b/pkg/app/api/grpcapi/web_api.go index 80857efdeb..23a9bb70d0 100644 --- a/pkg/app/api/grpcapi/web_api.go +++ b/pkg/app/api/grpcapi/web_api.go @@ -1311,6 +1311,91 @@ func (a *WebAPI) ListAPIKeys(ctx context.Context, req *webservice.ListAPIKeysReq } // GetInsightData returns the accumulated insight data. -func (a *WebAPI) GetInsightData(_ context.Context, _ *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) { +func (a *WebAPI) GetInsightData(ctx context.Context, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) { + claims, err := rpcauth.ExtractClaims(ctx) + if err != nil { + a.logger.Error("failed to authenticate the current user", zap.Error(err)) + return nil, err + } + + switch req.MetricsKind { + case model.InsightMetricsKind_DEPLOYMENT_FREQUENCY: + return a.getInsightDataForDeployFrequency(ctx, claims.Role.ProjectId, req) + } return nil, status.Error(codes.Unimplemented, "") } + +// getInsightDataForDeployFrequency returns the accumulated insight data for deploy frequency. +// This function is temporary implementation for front end. +func (a *WebAPI) getInsightDataForDeployFrequency(ctx context.Context, projectID string, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) { + counts := make([]*model.InsightDataPoint, req.DataPointCount) + + var movePoint func(time.Time, int) time.Time + var start time.Time + // To prevent heavy loading + // - Support only daily + // - DataPointCount needs to be less than or equal to 7 + switch req.Step { + case model.InsightStep_DAILY: + if req.DataPointCount > 7 { + return nil, status.Error(codes.InvalidArgument, "DataPointCount needs to be less than or equal to 7") + } + movePoint = func(from time.Time, i int) time.Time { + return from.AddDate(0, 0, i) + } + rangeFrom := time.Unix(req.RangeFrom, 0) + start = time.Date(rangeFrom.Year(), rangeFrom.Month(), rangeFrom.Day(), 0, 0, 0, 0, time.UTC) + default: + return nil, status.Error(codes.InvalidArgument, "Invalid step") + } + + for i := 0; i < int(req.DataPointCount); i++ { + target := movePoint(start, i) + + filters := []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: projectID, + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: target.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: movePoint(target, 1).Unix(), // target's finish time on unix time + }, + } + + if req.ApplicationId != "" { + filters = append(filters, datastore.ListFilter{ + Field: "ApplicationId", + Operator: "==", + Value: req.ApplicationId, + }) + } + + pageSize := 50 + deployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{ + PageSize: pageSize, + Filters: filters, + }) + if err != nil { + a.logger.Error("failed to get deployments", zap.Error(err)) + return nil, status.Error(codes.Internal, "Failed to get deployments") + } + + counts[i] = &model.InsightDataPoint{ + Timestamp: target.Unix(), + Value: float32(len(deployments)), + } + } + + return &webservice.GetInsightDataResponse{ + UpdatedAt: time.Now().Unix(), + DataPoints: counts, + }, nil +} diff --git a/pkg/app/api/grpcapi/web_api_test.go b/pkg/app/api/grpcapi/web_api_test.go index 58baa6ad30..60be953e35 100644 --- a/pkg/app/api/grpcapi/web_api_test.go +++ b/pkg/app/api/grpcapi/web_api_test.go @@ -17,11 +17,14 @@ package grpcapi import ( "context" "errors" + "fmt" "reflect" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "go.uber.org/zap" "github.com/pipe-cd/pipe/pkg/app/api/service/webservice" "github.com/pipe-cd/pipe/pkg/cache" @@ -387,3 +390,190 @@ func TestValidatePipedBelongsToProject(t *testing.T) { }) } } + +func TestGetInsightDataForDeployFrequency(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + PageSizeForListDeployments := 50 + tests := []struct { + name string + pipedID string + projectID string + pipedProjectCache cache.Cache + deploymentStore datastore.DeploymentStore + req *webservice.GetInsightDataRequest + res *webservice.GetInsightDataResponse + wantErr bool + }{ + { + name: "valid with InsightStep_DAILY", + pipedID: "pipedID", + projectID: "projectID", + deploymentStore: func() datastore.DeploymentStore { + target := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + targetNextDate := target.AddDate(0, 0, 1) + s := datastoretest.NewMockDeploymentStore(ctrl) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: target.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: targetNextDate.Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{ + { + Id: "id1", + }, + { + Id: "id2", + }, + }, nil) + + target = time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) + targetNextDate = target.AddDate(0, 0, 1) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: target.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: targetNextDate.Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{ + { + Id: "id1", + }, + { + Id: "id2", + }, + { + Id: "id3", + }, + }, nil) + + return s + }(), + req: &webservice.GetInsightDataRequest{ + MetricsKind: model.InsightMetricsKind_DEPLOYMENT_FREQUENCY, + Step: model.InsightStep_DAILY, + RangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + DataPointCount: 2, + ApplicationId: "ApplicationId", + }, + res: &webservice.GetInsightDataResponse{ + UpdatedAt: time.Now().Unix(), + DataPoints: []*model.InsightDataPoint{ + { + Value: 2, + Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Value: 3, + Timestamp: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), + }, + }, + }, + wantErr: false, + }, + { + name: "return error when something wrong happen on ListDeployments", + pipedID: "pipedID", + projectID: "projectID", + deploymentStore: func() datastore.DeploymentStore { + target := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + targetNextYear := target.AddDate(0, 0, 1) + s := datastoretest.NewMockDeploymentStore(ctrl) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: target.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: targetNextYear.Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{}, fmt.Errorf("something wrong happens in ListDeployments")) + return s + }(), + req: &webservice.GetInsightDataRequest{ + MetricsKind: model.InsightMetricsKind_DEPLOYMENT_FREQUENCY, + Step: model.InsightStep_DAILY, + RangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + DataPointCount: 2, + ApplicationId: "ApplicationId", + }, + res: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &WebAPI{ + pipedProjectCache: tt.pipedProjectCache, + deploymentStore: tt.deploymentStore, + logger: zap.NewNop(), + } + res, err := api.getInsightDataForDeployFrequency(ctx, tt.projectID, tt.req) + assert.Equal(t, tt.wantErr, err != nil) + if err == nil { + assert.Equal(t, tt.res.DataPoints, res.DataPoints) + } + }) + } +}