mirror of
https://github.com/goharbor/harbor.git
synced 2024-06-19 19:42:42 +02:00
Vulnerability scan data export functionality (#15998)
Vulnerability Scan Data (CVE) Export Functionality Proposal - goharbor/community#174 Closes - https://github.com/goharbor/harbor/issues/17150 Changes: * CVE Data export to CSV with filtering support. * Implement CSV data export job for creating CSVs * APIs to trigger CSV export job executions Signed-off-by: prahaladdarkin <prahaladd@vmware.com>
This commit is contained in:
parent
f852996fe5
commit
130452111b
|
@ -5595,6 +5595,122 @@ paths:
|
|||
'500':
|
||||
$ref: '#/responses/500'
|
||||
|
||||
/export/cve:
|
||||
post:
|
||||
summary: Export scan data for selected projects
|
||||
description: Export scan data for selected projects
|
||||
tags:
|
||||
- scan data export
|
||||
operationId: exportScanData
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/scanDataType'
|
||||
- name: criteria
|
||||
in: body
|
||||
description: The criteria for the export
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ScanDataExportRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Success.
|
||||
schema:
|
||||
$ref: '#/definitions/ScanDataExportJob'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'405':
|
||||
$ref: '#/responses/405'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/export/cve/execution/{execution_id}:
|
||||
get:
|
||||
summary: Get the specific scan data export execution
|
||||
description: Get the scan data export execution specified by ID
|
||||
tags:
|
||||
- scan data export
|
||||
operationId: getScanDataExportExecution
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/executionId'
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
schema:
|
||||
$ref: '#/definitions/ScanDataExportExecution'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/export/cve/executions:
|
||||
get:
|
||||
summary: Get a list of specific scan data export execution jobs for a specified user
|
||||
description: Get the scan data export execution specified by ID
|
||||
tags:
|
||||
- scan data export
|
||||
operationId: getScanDataExportExecutionList
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/userName'
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
schema:
|
||||
$ref: '#/definitions/ScanDataExportExecutionList'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/export/cve/download/{execution_id}:
|
||||
get:
|
||||
summary: Download the scan data export file
|
||||
description: Download the scan data report. Default format is CSV
|
||||
tags:
|
||||
- scan data export
|
||||
operationId: downloadScanData
|
||||
produces:
|
||||
- text/csv
|
||||
parameters:
|
||||
- $ref: '#/parameters/requestId'
|
||||
- $ref: '#/parameters/executionId'
|
||||
- name: format
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: The format of the data to be exported. e.g. CSV or PDF
|
||||
responses:
|
||||
'200':
|
||||
description: Data file containing the export data
|
||||
schema:
|
||||
type: file
|
||||
headers:
|
||||
Content-Disposition:
|
||||
type: string
|
||||
description: Value is a CSV formatted file; filename=export.csv
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
|
||||
parameters:
|
||||
query:
|
||||
name: q
|
||||
|
@ -5762,6 +5878,18 @@ parameters:
|
|||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
scanDataType:
|
||||
name: X-Scan-Data-Type
|
||||
description: The type of scan data to export
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
userName:
|
||||
name: user_name
|
||||
description: The name of the user
|
||||
in: query
|
||||
type: string
|
||||
required: true
|
||||
|
||||
responses:
|
||||
'200':
|
||||
|
@ -9036,3 +9164,89 @@ definitions:
|
|||
type: string
|
||||
format: date-time
|
||||
description: The creation time of the accessory
|
||||
|
||||
ScanDataExportRequest:
|
||||
type: object
|
||||
description: The criteria to select the scan data to export.
|
||||
properties:
|
||||
job_name:
|
||||
type: string
|
||||
description: Name of the scan data export job
|
||||
projects:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
description: A list of one or more projects for which to export the scan data, defaults to all if empty
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
description: A list of one or more labels for which to export the scan data, defaults to all if empty
|
||||
repositories:
|
||||
type: string
|
||||
description: A list of repositories for which to export the scan data, defaults to all if empty
|
||||
cveIds:
|
||||
type: string
|
||||
description: CVE-IDs for which to export data. Multiple CVE-IDs can be specified by separating using ',' and enclosed between '{}'. Defaults to all if empty
|
||||
tags:
|
||||
type: string
|
||||
description: A list of tags enclosed within '{}'. Defaults to all if empty
|
||||
ScanDataExportJob:
|
||||
type: object
|
||||
description: The metadata associated with the scan data export job
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The id of the scan data export job
|
||||
ScanDataExportExecution:
|
||||
type: object
|
||||
description: The replication execution
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: The ID of the execution
|
||||
user_id:
|
||||
type: integer
|
||||
description: The ID if the user triggering the export job
|
||||
status:
|
||||
type: string
|
||||
description: The status of the execution
|
||||
trigger:
|
||||
type: string
|
||||
description: The trigger mode
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The start time
|
||||
end_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The end time
|
||||
status_text:
|
||||
type: string
|
||||
x-omitempty: false
|
||||
description: The status text
|
||||
job_name:
|
||||
type: string
|
||||
x-omitempty: false
|
||||
description: The name of the job as specified by the user
|
||||
user_name:
|
||||
type: string
|
||||
x-omitempty: false
|
||||
description: The name of the user triggering the job
|
||||
file_present:
|
||||
type: boolean
|
||||
x-omitempty: false
|
||||
description: Indicates whether the export artifact is present in registry
|
||||
ScanDataExportExecutionList:
|
||||
type: object
|
||||
description: The list of scan data export executions
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ScanDataExportExecution'
|
||||
description: The list of scan data export executions
|
|
@ -17,7 +17,7 @@ WORKDIR /harbor/
|
|||
|
||||
USER harbor
|
||||
|
||||
VOLUME ["/var/log/jobs/"]
|
||||
VOLUME ["/var/log/jobs/", "/var/scandata_exports"]
|
||||
|
||||
HEALTHCHECK CMD curl --fail -s http://localhost:8080/api/v1/stats || curl -sk --fail --key /etc/harbor/ssl/job_service.key --cert /etc/harbor/ssl/job_service.crt https://localhost:8443/api/v1/stats || exit 1
|
||||
|
||||
|
|
|
@ -253,6 +253,7 @@ services:
|
|||
- SETUID
|
||||
volumes:
|
||||
- {{data_volume}}/job_logs:/var/log/jobs:z
|
||||
- {{data_volume}}/scandata_exports:/var/scandata_exports:z
|
||||
- type: bind
|
||||
source: ./common/config/jobservice/config.yml
|
||||
target: /etc/jobservice/config.yml
|
||||
|
|
194
src/controller/scandataexport/execution.go
Normal file
194
src/controller/scandataexport/execution.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package scandataexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
q2 "github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
"github.com/goharbor/harbor/src/pkg/systemartifact"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
)
|
||||
|
||||
func init() {
|
||||
task.SetExecutionSweeperCount(job.ScanDataExport, 50)
|
||||
}
|
||||
|
||||
var Ctl = NewController()
|
||||
|
||||
type Controller interface {
|
||||
Start(ctx context.Context, criteria export.Request) (executionID int64, err error)
|
||||
GetExecution(ctx context.Context, executionID int64) (*export.Execution, error)
|
||||
ListExecutions(ctx context.Context, userName string) ([]*export.Execution, error)
|
||||
GetTask(ctx context.Context, executionID int64) (*task.Task, error)
|
||||
DeleteExecution(ctx context.Context, executionID int64) error
|
||||
}
|
||||
|
||||
func NewController() Controller {
|
||||
return &controller{
|
||||
execMgr: task.ExecMgr,
|
||||
taskMgr: task.Mgr,
|
||||
makeCtx: orm.Context,
|
||||
sysArtifactMgr: systemartifact.Mgr,
|
||||
}
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
execMgr task.ExecutionManager
|
||||
taskMgr task.Manager
|
||||
makeCtx func() context.Context
|
||||
sysArtifactMgr systemartifact.Manager
|
||||
}
|
||||
|
||||
func (c *controller) ListExecutions(ctx context.Context, userName string) ([]*export.Execution, error) {
|
||||
keywords := make(map[string]interface{})
|
||||
keywords["VendorType"] = job.ScanDataExport
|
||||
keywords[fmt.Sprintf("ExtraAttrs.%s", export.UserNameAttribute)] = userName
|
||||
|
||||
q := q2.New(q2.KeyWords{})
|
||||
q.Keywords = keywords
|
||||
execsForUser, err := c.execMgr.List(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
execs := make([]*export.Execution, 0)
|
||||
for _, execForUser := range execsForUser {
|
||||
execs = append(execs, c.convertToExportExecStatus(ctx, execForUser))
|
||||
}
|
||||
return execs, nil
|
||||
}
|
||||
|
||||
func (c *controller) GetTask(ctx context.Context, executionID int64) (*task.Task, error) {
|
||||
query := q2.New(q2.KeyWords{})
|
||||
|
||||
keywords := make(map[string]interface{})
|
||||
keywords["VendorType"] = job.ScanDataExport
|
||||
keywords["ExecutionID"] = executionID
|
||||
query.Keywords = keywords
|
||||
query.Sorts = append(query.Sorts, &q2.Sort{
|
||||
Key: "ID",
|
||||
DESC: true,
|
||||
})
|
||||
tasks, err := c.taskMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
return nil, errors.Errorf("No task found for execution Id : %d", executionID)
|
||||
}
|
||||
// for the export JOB there would be a single instance of the task corresponding to the execution
|
||||
// we will hence return the latest instance of the task associated with this execution
|
||||
logger.Infof("Returning task instance with ID : %d", tasks[0].ID)
|
||||
return tasks[0], nil
|
||||
}
|
||||
|
||||
func (c *controller) GetExecution(ctx context.Context, executionID int64) (*export.Execution, error) {
|
||||
exec, err := c.execMgr.Get(ctx, executionID)
|
||||
if err != nil {
|
||||
logger.Errorf("Error when fetching execution status for ExecutionId: %d error : %v", executionID, err)
|
||||
return nil, err
|
||||
}
|
||||
if exec == nil {
|
||||
logger.Infof("No execution found for ExecutionId: %d", executionID)
|
||||
return nil, nil
|
||||
}
|
||||
return c.convertToExportExecStatus(ctx, exec), nil
|
||||
}
|
||||
|
||||
func (c *controller) DeleteExecution(ctx context.Context, executionID int64) error {
|
||||
err := c.execMgr.Delete(ctx, executionID)
|
||||
if err != nil {
|
||||
logger.Errorf("Error when deleting execution for ExecutionId: %d, error : %v", executionID, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *controller) Start(ctx context.Context, request export.Request) (executionID int64, err error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
vendorID := int64(ctx.Value(export.CsvJobVendorIDKey).(int))
|
||||
extraAttrs := make(map[string]interface{})
|
||||
extraAttrs[export.JobNameAttribute] = request.JobName
|
||||
extraAttrs[export.UserNameAttribute] = request.UserName
|
||||
id, err := c.execMgr.Create(ctx, job.ScanDataExport, vendorID, task.ExecutionTriggerManual, extraAttrs)
|
||||
logger.Infof("Created an execution record with id : %d for vendorID: %d", id, vendorID)
|
||||
if err != nil {
|
||||
logger.Errorf("Encountered error when creating job : %v", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// create a job object and fill with metadata and parameters
|
||||
params := make(map[string]interface{})
|
||||
params["JobId"] = id
|
||||
params["Request"] = request
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
|
||||
j := &task.Job{
|
||||
Name: job.ScanDataExport,
|
||||
Metadata: &job.Metadata{
|
||||
JobKind: job.KindGeneric,
|
||||
},
|
||||
Parameters: params,
|
||||
}
|
||||
|
||||
_, err = c.taskMgr.Create(ctx, id, j)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Unable to create a scan data export job: %v", err)
|
||||
c.markError(ctx, id, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
logger.Info("Created job for scan data export successfully")
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *controller) markError(ctx context.Context, executionID int64, err error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
// try to stop the execution first in case that some tasks are already created
|
||||
if err := c.execMgr.StopAndWait(ctx, executionID, 10*time.Second); err != nil {
|
||||
logger.Errorf("failed to stop the execution %d: %v", executionID, err)
|
||||
}
|
||||
if err := c.execMgr.MarkError(ctx, executionID, err.Error()); err != nil {
|
||||
logger.Errorf("failed to mark error for the execution %d: %v", executionID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) convertToExportExecStatus(ctx context.Context, exec *task.Execution) *export.Execution {
|
||||
execStatus := &export.Execution{
|
||||
ID: exec.ID,
|
||||
UserID: exec.VendorID,
|
||||
Status: exec.Status,
|
||||
StatusMessage: exec.StatusMessage,
|
||||
Trigger: exec.Trigger,
|
||||
StartTime: exec.StartTime,
|
||||
EndTime: exec.EndTime,
|
||||
}
|
||||
if digest, ok := exec.ExtraAttrs[export.DigestKey]; ok {
|
||||
execStatus.ExportDataDigest = digest.(string)
|
||||
}
|
||||
if jobName, ok := exec.ExtraAttrs[export.JobNameAttribute]; ok {
|
||||
execStatus.JobName = jobName.(string)
|
||||
}
|
||||
if userName, ok := exec.ExtraAttrs[export.UserNameAttribute]; ok {
|
||||
execStatus.UserName = userName.(string)
|
||||
}
|
||||
artifactExists := c.isCsvArtifactPresent(ctx, exec.ID, execStatus.ExportDataDigest)
|
||||
execStatus.FilePresent = artifactExists
|
||||
return execStatus
|
||||
}
|
||||
|
||||
func (c *controller) isCsvArtifactPresent(ctx context.Context, execID int64, digest string) bool {
|
||||
repositoryName := fmt.Sprintf("scandata_export_%v", execID)
|
||||
exists, err := c.sysArtifactMgr.Exists(ctx, strings.ToLower(export.Vendor), repositoryName, digest)
|
||||
if err != nil {
|
||||
exists = false
|
||||
}
|
||||
return exists
|
||||
}
|
332
src/controller/scandataexport/execution_test.go
Normal file
332
src/controller/scandataexport/execution_test.go
Normal file
|
@ -0,0 +1,332 @@
|
|||
package scandataexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
systemartifacttesting "github.com/goharbor/harbor/src/testing/pkg/systemartifact"
|
||||
testingTask "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
"github.com/pkg/errors"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ScanDataExportExecutionTestSuite struct {
|
||||
suite.Suite
|
||||
execMgr *testingTask.ExecutionManager
|
||||
taskMgr *testingTask.Manager
|
||||
sysArtifactMgr *systemartifacttesting.Manager
|
||||
ctl *controller
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestGetTask() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
}
|
||||
// valid task execution record exists for an execution id
|
||||
{
|
||||
t := task.Task{
|
||||
ID: 1,
|
||||
VendorType: "SCAN_DATA_EXPORT",
|
||||
ExecutionID: 100,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
RunCount: 1,
|
||||
JobID: "TestJobId",
|
||||
ExtraAttrs: nil,
|
||||
CreationTime: time.Time{},
|
||||
StartTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
EndTime: time.Time{},
|
||||
StatusRevision: 0,
|
||||
}
|
||||
|
||||
tasks := make([]*task.Task, 0)
|
||||
tasks = append(tasks, &t)
|
||||
mock.OnAnything(suite.taskMgr, "List").Return(tasks, nil).Once()
|
||||
returnedTask, err := suite.ctl.GetTask(context.Background(), 100)
|
||||
suite.NoError(err)
|
||||
suite.Equal(t, *returnedTask)
|
||||
}
|
||||
|
||||
// no task records exist for an execution id
|
||||
{
|
||||
tasks := make([]*task.Task, 0)
|
||||
mock.OnAnything(suite.taskMgr, "List").Return(tasks, nil).Once()
|
||||
_, err := suite.ctl.GetTask(context.Background(), 100)
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
// listing of tasks returns an error
|
||||
{
|
||||
mock.OnAnything(suite.taskMgr, "List").Return(nil, errors.New("test error")).Once()
|
||||
_, err := suite.ctl.GetTask(context.Background(), 100)
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestGetExecution() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
}
|
||||
// get execution succeeds
|
||||
attrs := make(map[string]interface{})
|
||||
attrs[export.JobNameAttribute] = "test-job"
|
||||
attrs[export.UserNameAttribute] = "test-user"
|
||||
{
|
||||
exec := task.Execution{
|
||||
ID: 100,
|
||||
VendorType: "SCAN_DATA_EXPORT",
|
||||
VendorID: -1,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Metrics: nil,
|
||||
Trigger: "Manual",
|
||||
ExtraAttrs: attrs,
|
||||
StartTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
EndTime: time.Time{},
|
||||
}
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&exec, nil).Once()
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Exists").Return(true, nil).Once()
|
||||
|
||||
exportExec, err := suite.ctl.GetExecution(context.TODO(), 100)
|
||||
suite.NoError(err)
|
||||
suite.Equal(exec.ID, exportExec.ID)
|
||||
suite.Equal("test-user", exportExec.UserName)
|
||||
suite.Equal("test-job", exportExec.JobName)
|
||||
suite.Equal(true, exportExec.FilePresent)
|
||||
}
|
||||
|
||||
// get execution fails
|
||||
{
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(nil, errors.New("test error")).Once()
|
||||
exportExec, err := suite.ctl.GetExecution(context.TODO(), 100)
|
||||
suite.Error(err)
|
||||
suite.Nil(exportExec)
|
||||
}
|
||||
|
||||
// get execution returns null
|
||||
{
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(nil, nil).Once()
|
||||
exportExec, err := suite.ctl.GetExecution(context.TODO(), 100)
|
||||
suite.NoError(err)
|
||||
suite.Nil(exportExec)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestGetExecutionSysArtifactExistFail() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
}
|
||||
// get execution succeeds
|
||||
attrs := make(map[string]interface{})
|
||||
attrs[export.JobNameAttribute] = "test-job"
|
||||
attrs[export.UserNameAttribute] = "test-user"
|
||||
{
|
||||
exec := task.Execution{
|
||||
ID: 100,
|
||||
VendorType: "SCAN_DATA_EXPORT",
|
||||
VendorID: -1,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Metrics: nil,
|
||||
Trigger: "Manual",
|
||||
ExtraAttrs: attrs,
|
||||
StartTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
EndTime: time.Time{},
|
||||
}
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&exec, nil).Once()
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Exists").Return(false, errors.New("test error")).Once()
|
||||
|
||||
exportExec, err := suite.ctl.GetExecution(context.TODO(), 100)
|
||||
suite.NoError(err)
|
||||
suite.Equal(exec.ID, exportExec.ID)
|
||||
suite.Equal("test-user", exportExec.UserName)
|
||||
suite.Equal("test-job", exportExec.JobName)
|
||||
suite.Equal(false, exportExec.FilePresent)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestGetExecutionList() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
}
|
||||
// get execution succeeds
|
||||
attrs := make(map[string]interface{})
|
||||
attrs[export.JobNameAttribute] = "test-job"
|
||||
attrs[export.UserNameAttribute] = "test-user"
|
||||
{
|
||||
exec := task.Execution{
|
||||
ID: 100,
|
||||
VendorType: "SCAN_DATA_EXPORT",
|
||||
VendorID: -1,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Metrics: nil,
|
||||
Trigger: "Manual",
|
||||
ExtraAttrs: attrs,
|
||||
StartTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
EndTime: time.Time{},
|
||||
}
|
||||
execs := make([]*task.Execution, 0)
|
||||
execs = append(execs, &exec)
|
||||
mock.OnAnything(suite.execMgr, "List").Return(execs, nil).Once()
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Exists").Return(true, nil).Once()
|
||||
exportExec, err := suite.ctl.ListExecutions(context.TODO(), "test-user")
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(1, len(exportExec))
|
||||
suite.Equal("test-user", exportExec[0].UserName)
|
||||
suite.Equal("test-job", exportExec[0].JobName)
|
||||
}
|
||||
|
||||
// get execution fails
|
||||
{
|
||||
mock.OnAnything(suite.execMgr, "List").Return(nil, errors.New("test error")).Once()
|
||||
exportExec, err := suite.ctl.ListExecutions(context.TODO(), "test-user")
|
||||
suite.Error(err)
|
||||
suite.Nil(exportExec)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestStart() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
}
|
||||
// execution manager and task manager return successfully
|
||||
{
|
||||
// get execution succeeds
|
||||
attrs := make(map[string]interface{})
|
||||
attrs[export.JobNameAttribute] = "test-job"
|
||||
attrs[export.UserNameAttribute] = "test-user"
|
||||
suite.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, attrs).Return(int64(10), nil)
|
||||
suite.taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(int64(20), nil)
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, export.CsvJobVendorIDKey, int(-1))
|
||||
criteria := export.Request{}
|
||||
criteria.UserName = "test-user"
|
||||
criteria.JobName = "test-job"
|
||||
executionId, err := suite.ctl.Start(ctx, criteria)
|
||||
suite.NoError(err)
|
||||
suite.Equal(int64(10), executionId)
|
||||
suite.validateExecutionManagerInvocation(ctx)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestDeleteExecution() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
}
|
||||
mock.OnAnything(suite.execMgr, "Delete").Return(nil).Once()
|
||||
err := suite.ctl.DeleteExecution(context.TODO(), int64(1))
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestStartWithExecManagerError() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
}
|
||||
// execution manager returns an error
|
||||
{
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, export.CsvJobVendorIDKey, int(-1))
|
||||
mock.OnAnything(suite.execMgr, "Create").Return(int64(-1), errors.New("Test Error"))
|
||||
_, err := suite.ctl.Start(ctx, export.Request{JobName: "test-job", UserName: "test-user"})
|
||||
suite.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TestStartWithTaskManagerError() {
|
||||
suite.taskMgr = &testingTask.Manager{}
|
||||
suite.execMgr = &testingTask.ExecutionManager{}
|
||||
suite.ctl = &controller{
|
||||
execMgr: suite.execMgr,
|
||||
taskMgr: suite.taskMgr,
|
||||
makeCtx: func() context.Context { return orm.NewContext(nil, &ormtesting.FakeOrmer{}) },
|
||||
}
|
||||
// execution manager is successful but task manager returns an error
|
||||
// execution manager and task manager return successfully
|
||||
{
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, export.CsvJobVendorIDKey, int(-1))
|
||||
attrs := make(map[string]interface{})
|
||||
attrs[export.JobNameAttribute] = "test-job"
|
||||
attrs[export.UserNameAttribute] = "test-user"
|
||||
suite.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, attrs).Return(int64(10), nil)
|
||||
suite.taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(int64(-1), errors.New("Test Error"))
|
||||
mock.OnAnything(suite.execMgr, "StopAndWait").Return(nil)
|
||||
mock.OnAnything(suite.execMgr, "MarkError").Return(nil)
|
||||
_, err := suite.ctl.Start(ctx, export.Request{JobName: "test-job", UserName: "test-user"})
|
||||
suite.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) TearDownSuite() {
|
||||
suite.execMgr = nil
|
||||
suite.taskMgr = nil
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportExecutionTestSuite) validateExecutionManagerInvocation(ctx context.Context) {
|
||||
// validate that execution manager has been called with the specified
|
||||
extraAttsMatcher := testifymock.MatchedBy(func(m map[string]interface{}) bool {
|
||||
jobName, jobNamePresent := m[export.JobNameAttribute]
|
||||
userName, userNamePresent := m[export.UserNameAttribute]
|
||||
return jobNamePresent && userNamePresent && jobName == "test-job" && userName == "test-user"
|
||||
})
|
||||
suite.execMgr.AssertCalled(suite.T(), "Create", ctx, job.ScanDataExport, int64(-1), task.ExecutionTriggerManual, extraAttsMatcher)
|
||||
}
|
||||
|
||||
func TestScanDataExportExecutionTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ScanDataExportExecutionTestSuite{})
|
||||
}
|
|
@ -28,6 +28,7 @@ require (
|
|||
github.com/go-openapi/validate v0.19.10
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
|
||||
github.com/gocraft/work v0.5.1
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0
|
||||
github.com/golang-migrate/migrate/v4 v4.15.1
|
||||
|
|
|
@ -638,6 +638,9 @@ github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5
|
|||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
|
||||
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8 h1:hp1oqdzmv37vPLYFGjuM/RmUgUMfD9vQfMszc54l55Y=
|
||||
github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
|
||||
github.com/gocraft/work v0.5.1 h1:3bRjMiOo6N4zcRgZWV3Y7uX7R22SF+A9bPTk4xRXr34=
|
||||
github.com/gocraft/work v0.5.1/go.mod h1:pc3n9Pb5FAESPPGfM0nL+7Q1xtgtRnF8rr/azzhQVlM=
|
||||
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
|
|
341
src/jobservice/job/impl/scandataexport/scan_data_export.go
Normal file
341
src/jobservice/job/impl/scandataexport/scan_data_export.go
Normal file
|
@ -0,0 +1,341 @@
|
|||
package scandataexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
"github.com/goharbor/harbor/src/pkg/systemartifact"
|
||||
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// ScanDataExport is the struct to implement the scan data export.
|
||||
// implements the Job interface
|
||||
type ScanDataExport struct {
|
||||
execMgr task.ExecutionManager
|
||||
scanDataExportDirPath string
|
||||
exportMgr export.Manager
|
||||
digestCalculator export.ArtifactDigestCalculator
|
||||
filterProcessor export.FilterProcessor
|
||||
vulnDataSelector export.VulnerabilityDataSelector
|
||||
projectMgr project.Manager
|
||||
sysArtifactMgr systemartifact.Manager
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) MaxFails() uint {
|
||||
return 1
|
||||
}
|
||||
|
||||
// MaxCurrency of the job. Unlike the WorkerPool concurrency, it controls the limit on the number jobs of that type
|
||||
// that can be active at one time by within a single redis instance.
|
||||
// The default value is 0, which means "no limit on job concurrency".
|
||||
func (sde *ScanDataExport) MaxCurrency() uint {
|
||||
return 1
|
||||
}
|
||||
|
||||
// ShouldRetry tells worker if retry the failed job when the fails is
|
||||
// still less that the number declared by the method 'MaxFails'.
|
||||
//
|
||||
// Returns:
|
||||
// true for retry and false for none-retry
|
||||
func (sde *ScanDataExport) ShouldRetry() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Validate Indicate whether the parameters of job are valid.
|
||||
// Return:
|
||||
// error if parameters are not valid. NOTES: If no parameters needed, directly return nil.
|
||||
func (sde *ScanDataExport) Validate(params job.Parameters) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the business logic here.
|
||||
// The related arguments will be injected by the workerpool.
|
||||
//
|
||||
// ctx Context : Job execution context.
|
||||
// params map[string]interface{} : parameters with key-pair style for the job execution.
|
||||
//
|
||||
// Returns:
|
||||
// error if failed to run. NOTES: If job is stopped or cancelled, a specified error should be returned
|
||||
//
|
||||
func (sde *ScanDataExport) Run(ctx job.Context, params job.Parameters) error {
|
||||
if _, ok := params[export.JobModeKey]; !ok {
|
||||
return errors.Errorf("no mode specified for scan data export execution")
|
||||
}
|
||||
|
||||
mode := params[export.JobModeKey].(string)
|
||||
logger.Infof("Scan data export job started in mode : %v", mode)
|
||||
sde.init()
|
||||
fileName := fmt.Sprintf("%s/scandata_export_%v.csv", sde.scanDataExportDirPath, params["JobId"])
|
||||
|
||||
// ensure that CSV files are cleared post the completion of the Run.
|
||||
defer sde.cleanupCsvFile(fileName, params)
|
||||
err := sde.writeCsvFile(ctx, params, fileName)
|
||||
if err != nil {
|
||||
logger.Errorf("error when writing data to CSV: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := sde.calculateFileHash(fileName)
|
||||
if err != nil {
|
||||
logger.Errorf("Error when calculating checksum for generated file: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("Export Job Id = %v, FileName = %s, Hash = %v", params["JobId"], fileName, hash)
|
||||
|
||||
csvFile, err := os.OpenFile(fileName, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
logger.Errorf(
|
||||
"Export Job Id = %v. Error when moving report file %s to persistent storage: %v", params["JobId"], fileName, err)
|
||||
return err
|
||||
}
|
||||
baseFileName := filepath.Base(fileName)
|
||||
repositoryName := strings.TrimSuffix(baseFileName, filepath.Ext(baseFileName))
|
||||
logger.Infof("Creating repository for CSV file with blob : %s", repositoryName)
|
||||
stat, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
logger.Errorf("Error when fetching file size: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("Export Job Id = %v. CSV file size: %d", params["JobId"], stat.Size())
|
||||
csvExportArtifactRecord := model.SystemArtifact{Repository: repositoryName, Digest: hash.String(), Size: stat.Size(), Type: "ScanData_CSV", Vendor: strings.ToLower(export.Vendor)}
|
||||
artID, err := sde.sysArtifactMgr.Create(ctx.SystemContext(), &csvExportArtifactRecord, csvFile)
|
||||
if err != nil {
|
||||
logger.Errorf(
|
||||
"Export Job Id = %v. Error when persisting report file %s to persistent storage: %v", params["JobId"], fileName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Export Job Id = %v. Created system artifact: %v for report file %s to persistent storage: %v", params["JobId"], artID, fileName, err)
|
||||
err = sde.updateExecAttributes(ctx, params, err, hash)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Export Job Id = %v. Error when updating execution record : %v", params["JobId"], err)
|
||||
return err
|
||||
}
|
||||
logger.Info("Scan data export job completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) updateExecAttributes(ctx job.Context, params job.Parameters, err error, hash digest.Digest) error {
|
||||
execID := int64(params["JobId"].(float64))
|
||||
exec, err := sde.execMgr.Get(ctx.SystemContext(), execID)
|
||||
if err != nil {
|
||||
logger.Errorf("Export Job Id = %v. Error when fetching execution record for update : %v", params["JobId"], err)
|
||||
return err
|
||||
}
|
||||
attrsToUpdate := make(map[string]interface{})
|
||||
for k, v := range exec.ExtraAttrs {
|
||||
attrsToUpdate[k] = v
|
||||
}
|
||||
attrsToUpdate[export.DigestKey] = hash.String()
|
||||
return sde.execMgr.UpdateExtraAttrs(ctx.SystemContext(), execID, attrsToUpdate)
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) writeCsvFile(ctx job.Context, params job.Parameters, fileName string) error {
|
||||
csvFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
systemContext := ctx.SystemContext()
|
||||
defer csvFile.Close()
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to create CSV export file %s. Error : %v", fileName, err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("Created CSV export file %s", csvFile.Name())
|
||||
|
||||
var exportParams export.Params
|
||||
|
||||
if criteira, ok := params["Request"]; ok {
|
||||
logger.Infof("Request for export : %v", criteira)
|
||||
filterCriteria, err := sde.extractCriteria(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if any projects are specified. If not then fetch all the projects
|
||||
// of which the current user is a project admin.
|
||||
projectIds, err := sde.filterProcessor.ProcessProjectFilter(systemContext, filterCriteria.UserName, filterCriteria.Projects)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(projectIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract the repository ids if any repositories have been specified
|
||||
repoIds, err := sde.getRepositoryIds(systemContext, filterCriteria.Repositories, projectIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(repoIds) == 0 {
|
||||
logger.Infof("No repositories found with specified names: %v", filterCriteria.Repositories)
|
||||
return nil
|
||||
}
|
||||
|
||||
// filter the specified repositories further using the specified tags
|
||||
repoIds, err = sde.getRepositoriesWithTags(systemContext, filterCriteria, repoIds)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(repoIds) == 0 {
|
||||
logger.Infof("No repositories found with specified names: %v and tags: %v", filterCriteria.Repositories, filterCriteria.Tags)
|
||||
return nil
|
||||
}
|
||||
|
||||
exportParams = export.Params{
|
||||
Projects: filterCriteria.Projects,
|
||||
Repositories: repoIds,
|
||||
CVEIds: filterCriteria.CVEIds,
|
||||
Labels: filterCriteria.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
exportParams.PageNumber = 1
|
||||
exportParams.PageSize = export.QueryPageSize
|
||||
|
||||
for {
|
||||
data, err := sde.exportMgr.Fetch(systemContext, exportParams)
|
||||
if err != nil {
|
||||
logger.Error("Encountered error reading from the report table", err)
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
logger.Infof("No more data to fetch. Exiting...")
|
||||
break
|
||||
}
|
||||
logger.Infof("Export Job Id = %v, Page Number = %d, Page Size = %d Num Records = %d", params["JobId"], exportParams.PageNumber, exportParams.PageSize, len(data))
|
||||
|
||||
// for the first page write the CSV with the headers
|
||||
if exportParams.PageNumber == 1 {
|
||||
err = gocsv.Marshal(data, csvFile)
|
||||
} else {
|
||||
err = gocsv.MarshalWithoutHeaders(data, csvFile)
|
||||
}
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
exportParams.PageNumber = exportParams.PageNumber + 1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) getRepositoryIds(ctx context.Context, filter string, projectIds []int64) ([]int64, error) {
|
||||
repositoryIds := make([]int64, 0)
|
||||
candidates, err := sde.filterProcessor.ProcessRepositoryFilter(ctx, filter, projectIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if candidates == nil {
|
||||
return repositoryIds, nil
|
||||
}
|
||||
for _, cand := range candidates {
|
||||
repositoryIds = append(repositoryIds, cand.NamespaceID)
|
||||
}
|
||||
return repositoryIds, nil
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) getRepositoriesWithTags(ctx context.Context, filterCriteria *export.Request, repositoryIds []int64) ([]int64, error) {
|
||||
if filterCriteria.Tags == "" {
|
||||
return repositoryIds, nil
|
||||
}
|
||||
candidates, err := sde.filterProcessor.ProcessTagFilter(ctx, filterCriteria.Tags, repositoryIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if candidates == nil {
|
||||
return make([]int64, 0), nil
|
||||
}
|
||||
filteredCandidates := make([]int64, 0)
|
||||
for _, cand := range candidates {
|
||||
filteredCandidates = append(filteredCandidates, cand.NamespaceID)
|
||||
}
|
||||
return filteredCandidates, nil
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) extractCriteria(params job.Parameters) (*export.Request, error) {
|
||||
filterMap, ok := params["Request"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.Errorf("malformed criteria '%v'", params["Request"])
|
||||
}
|
||||
jsonData, err := json.Marshal(filterMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
criteria := &export.Request{}
|
||||
err = criteria.FromJSON(string(jsonData))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return criteria, nil
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) calculateFileHash(fileName string) (digest.Digest, error) {
|
||||
return sde.digestCalculator.Calculate(fileName)
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) init() {
|
||||
if sde.execMgr == nil {
|
||||
sde.execMgr = task.NewExecutionManager()
|
||||
}
|
||||
|
||||
if sde.scanDataExportDirPath == "" {
|
||||
sde.scanDataExportDirPath = export.ScanDataExportDir
|
||||
}
|
||||
|
||||
if sde.exportMgr == nil {
|
||||
sde.exportMgr = export.NewManager()
|
||||
}
|
||||
|
||||
if sde.digestCalculator == nil {
|
||||
sde.digestCalculator = &export.SHA256ArtifactDigestCalculator{}
|
||||
}
|
||||
|
||||
if sde.filterProcessor == nil {
|
||||
sde.filterProcessor = export.NewFilterProcessor()
|
||||
}
|
||||
|
||||
if sde.vulnDataSelector == nil {
|
||||
sde.vulnDataSelector = export.NewVulnerabilityDataSelector()
|
||||
}
|
||||
|
||||
if sde.projectMgr == nil {
|
||||
sde.projectMgr = project.New()
|
||||
}
|
||||
|
||||
if sde.sysArtifactMgr == nil {
|
||||
sde.sysArtifactMgr = systemartifact.Mgr
|
||||
}
|
||||
}
|
||||
|
||||
func (sde *ScanDataExport) cleanupCsvFile(fileName string, params job.Parameters) {
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
logger.Infof("Export Job Id = %v, CSV Export File = %s does not exist. Nothing to do", params["JobId"], fileName)
|
||||
return
|
||||
}
|
||||
err := os.Remove(fileName)
|
||||
if err != nil {
|
||||
logger.Errorf("Export Job Id = %d, CSV Export File = %s could not deleted. Error = %v", params["JobId"], fileName, err)
|
||||
return
|
||||
}
|
||||
}
|
649
src/jobservice/job/impl/scandataexport/scan_data_export_test.go
Normal file
649
src/jobservice/job/impl/scandataexport/scan_data_export_test.go
Normal file
|
@ -0,0 +1,649 @@
|
|||
package scandataexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/selector"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
mockjobservice "github.com/goharbor/harbor/src/testing/jobservice"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/project"
|
||||
export2 "github.com/goharbor/harbor/src/testing/pkg/scan/export"
|
||||
systemartifacttesting "github.com/goharbor/harbor/src/testing/pkg/systemartifact"
|
||||
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
"github.com/opencontainers/go-digest"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
const JobId = float64(100)
|
||||
const MockDigest = "mockDigest"
|
||||
|
||||
type ScanDataExportJobTestSuite struct {
|
||||
htesting.Suite
|
||||
execMgr *tasktesting.ExecutionManager
|
||||
job *ScanDataExport
|
||||
exportMgr *export2.Manager
|
||||
digestCalculator *export2.ArtifactDigestCalculator
|
||||
filterProcessor *export2.FilterProcessor
|
||||
projectMgr *project.Manager
|
||||
sysArtifactMgr *systemartifacttesting.Manager
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) SetupTest() {
|
||||
suite.execMgr = &tasktesting.ExecutionManager{}
|
||||
suite.exportMgr = &export2.Manager{}
|
||||
suite.digestCalculator = &export2.ArtifactDigestCalculator{}
|
||||
suite.filterProcessor = &export2.FilterProcessor{}
|
||||
suite.projectMgr = &project.Manager{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.job = &ScanDataExport{
|
||||
execMgr: suite.execMgr,
|
||||
exportMgr: suite.exportMgr,
|
||||
scanDataExportDirPath: "/tmp",
|
||||
digestCalculator: suite.digestCalculator,
|
||||
filterProcessor: suite.filterProcessor,
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
}
|
||||
|
||||
suite.execMgr.On("UpdateExtraAttrs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
// all BLOB related operations succeed
|
||||
suite.sysArtifactMgr.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRun() {
|
||||
|
||||
data := suite.createDataRecords(3, 1)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil)
|
||||
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
|
||||
m := make(map[string]interface{})
|
||||
m[export.DigestKey] = MockDigest
|
||||
m[export.CreateTimestampKey] = mock.Anything
|
||||
|
||||
extraAttrsMatcher := testifymock.MatchedBy(func(attrsMap map[string]interface{}) bool {
|
||||
_, ok := m[export.CreateTimestampKey]
|
||||
return attrsMap[export.DigestKey] == MockDigest && ok && attrsMap[export.JobNameAttribute] == "test-job" && attrsMap[export.UserNameAttribute] == "test-user"
|
||||
})
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), extraAttrsMatcher)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRunAttributeUpdateError() {
|
||||
|
||||
data := suite.createDataRecords(3, 1)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(nil, errors.New("test-error"))
|
||||
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.Error(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
|
||||
m := make(map[string]interface{})
|
||||
m[export.DigestKey] = MockDigest
|
||||
m[export.CreateTimestampKey] = mock.Anything
|
||||
|
||||
extraAttrsMatcher := testifymock.MatchedBy(func(attrsMap map[string]interface{}) bool {
|
||||
_, ok := m[export.CreateTimestampKey]
|
||||
return attrsMap[export.DigestKey] == MockDigest && ok && attrsMap[export.JobNameAttribute] == "test-job" && attrsMap[export.UserNameAttribute] == "test-user"
|
||||
})
|
||||
suite.execMgr.AssertNotCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), extraAttrsMatcher)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRunWithCriteria() {
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{1}, nil).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
|
||||
m := make(map[string]interface{})
|
||||
m[export.DigestKey] = MockDigest
|
||||
m[export.CreateTimestampKey] = mock.Anything
|
||||
|
||||
extraAttrsMatcher := testifymock.MatchedBy(func(attrsMap map[string]interface{}) bool {
|
||||
_, ok := m[export.CreateTimestampKey]
|
||||
return attrsMap[export.DigestKey] == MockDigest && ok
|
||||
})
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), extraAttrsMatcher)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
exportParamsMatcher := testifymock.MatchedBy(func(params export.Params) bool {
|
||||
return reflect.DeepEqual(params.Labels, criteria.Labels) && reflect.DeepEqual(params.CVEIds, criteria.CVEIds) && reflect.DeepEqual(params.Repositories, []int64{1}) && reflect.DeepEqual(params.Projects, criteria.Projects)
|
||||
})
|
||||
suite.exportMgr.AssertCalled(suite.T(), "Fetch", mock.Anything, exportParamsMatcher)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
{
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil).Once()
|
||||
data := suite.createDataRecords(3, 1)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{1}, nil).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Times(3)
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
m := make(map[string]interface{})
|
||||
m[export.DigestKey] = MockDigest
|
||||
m[export.CreateTimestampKey] = mock.Anything
|
||||
|
||||
extraAttrsMatcher := testifymock.MatchedBy(func(attrsMap map[string]interface{}) bool {
|
||||
_, ok := m[export.CreateTimestampKey]
|
||||
return attrsMap[export.DigestKey] == MockDigest && ok
|
||||
})
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), extraAttrsMatcher)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
exportParamsMatcher := testifymock.MatchedBy(func(params export.Params) bool {
|
||||
return reflect.DeepEqual(params.Labels, criteria.Labels) && reflect.DeepEqual(params.CVEIds, criteria.CVEIds) && reflect.DeepEqual(params.Repositories, []int64{1}) && reflect.DeepEqual(params.Projects, criteria.Projects)
|
||||
})
|
||||
suite.exportMgr.AssertCalled(suite.T(), "Fetch", mock.Anything, exportParamsMatcher)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRunWithCriteriaForProjectIdFilter() {
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return(nil, errors.New("test error")).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.Error(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertNotCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertNotCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
// empty list of projects
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{}, nil).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRunWithCriteriaForRepositoryIdFilter() {
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{1}, errors.New("test error")).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(nil, errors.New("test error"))
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.Error(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertNotCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertNotCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
// empty list of repo ids
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidates := make([]*selector.Candidate, 0)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{}, nil).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(repoCandidates, nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestRunWithCriteriaForRepositoryIdWithTagFilter() {
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidate1 := &selector.Candidate{NamespaceID: 1}
|
||||
repoCandidates := []*selector.Candidate{repoCandidate1}
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{1}, errors.New("test error")).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(nil, errors.New("test error"))
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.Error(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertNotCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertNotCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
// empty list of repo ids after applying tag filters
|
||||
{
|
||||
data := suite.createDataRecords(3, 1)
|
||||
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(MockDigest), nil)
|
||||
execAttrs := make(map[string]interface{})
|
||||
execAttrs[export.JobNameAttribute] = "test-job"
|
||||
execAttrs[export.UserNameAttribute] = "test-user"
|
||||
mock.OnAnything(suite.execMgr, "Get").Return(&task.Execution{ID: int64(JobId), ExtraAttrs: execAttrs}, nil).Once()
|
||||
|
||||
repoCandidates := make([]*selector.Candidate, 0)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessProjectFilter").Return([]int64{}, nil).Once()
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessRepositoryFilter").Return(repoCandidates, nil)
|
||||
mock.OnAnything(suite.filterProcessor, "ProcessTagFilter").Return(make([]*selector.Candidate, 0), nil)
|
||||
|
||||
criteria := export.Request{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{1},
|
||||
Projects: []int64{1},
|
||||
Repositories: "test-repo",
|
||||
Tags: "test-tag",
|
||||
}
|
||||
criteriaMap := make(map[string]interface{})
|
||||
bytes, _ := json.Marshal(criteria)
|
||||
json.Unmarshal(bytes, &criteriaMap)
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
params["Request"] = criteriaMap
|
||||
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
ctx.On("SystemContext").Return(context.TODO()).Once()
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.NoError(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertCalled(suite.T(), "UpdateExtraAttrs", mock.Anything, int64(JobId), mock.Anything)
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
|
||||
suite.exportMgr.AssertNotCalled(suite.T(), "Fetch", mock.Anything, mock.Anything)
|
||||
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TestExportDigestCalculationErrorsOut() {
|
||||
data := suite.createDataRecords(3, 1)
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(data, nil).Once()
|
||||
mock.OnAnything(suite.exportMgr, "Fetch").Return(make([]export.Data, 0), nil).Once()
|
||||
mock.OnAnything(suite.digestCalculator, "Calculate").Return(digest.Digest(""), errors.New("test error"))
|
||||
params := job.Parameters{}
|
||||
params[export.JobModeKey] = export.JobModeExport
|
||||
params["JobId"] = JobId
|
||||
ctx := &mockjobservice.MockJobContext{}
|
||||
|
||||
err := suite.job.Run(ctx, params)
|
||||
suite.Error(err)
|
||||
sysArtifactRecordMatcher := testifymock.MatchedBy(func(sa *model.SystemArtifact) bool {
|
||||
return sa.Repository == "scandata_export_100" && sa.Vendor == strings.ToLower(export.Vendor) && sa.Digest == MockDigest
|
||||
})
|
||||
suite.sysArtifactMgr.AssertNotCalled(suite.T(), "Create", mock.Anything, sysArtifactRecordMatcher, mock.Anything)
|
||||
suite.execMgr.AssertNotCalled(suite.T(), "UpdateExtraAttrs")
|
||||
_, err = os.Stat("/tmp/scandata_export_100.csv")
|
||||
suite.Truef(os.IsNotExist(err), "Expected CSV file to be deleted")
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) TearDownTest() {
|
||||
path := fmt.Sprintf("/tmp/scandata_export_%v.csv", JobId)
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
err = os.Remove(path)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *ScanDataExportJobTestSuite) createDataRecords(numRecs int, ownerId int64) []export.Data {
|
||||
data := make([]export.Data, 0)
|
||||
for i := 1; i <= numRecs; i++ {
|
||||
dataRec := export.Data{
|
||||
ID: int64(i),
|
||||
ProjectName: fmt.Sprintf("TestProject%d", i),
|
||||
ProjectOwner: strconv.FormatInt(ownerId, 10),
|
||||
ScannerName: fmt.Sprintf("TestScanner%d", i),
|
||||
CVEId: fmt.Sprintf("CVEId-%d", i),
|
||||
Package: fmt.Sprintf("Package%d", i),
|
||||
Severity: fmt.Sprintf("Severity%d", i),
|
||||
CVSSScoreV3: fmt.Sprintf("3.0"),
|
||||
CVSSScoreV2: fmt.Sprintf("2.0"),
|
||||
CVSSVectorV3: fmt.Sprintf("TestCVSSVectorV3%d", i),
|
||||
CVSSVectorV2: fmt.Sprintf("TestCVSSVectorV2%d", i),
|
||||
CWEIds: "",
|
||||
}
|
||||
data = append(data, dataRec)
|
||||
}
|
||||
return data
|
||||
}
|
||||
func TestScanDataExportJobSuite(t *testing.T) {
|
||||
suite.Run(t, &ScanDataExportJobTestSuite{})
|
||||
}
|
|
@ -38,4 +38,6 @@ const (
|
|||
PurgeAudit = "PURGE_AUDIT"
|
||||
// SystemArtifactCleanup : the name of the SystemArtifact cleanup job
|
||||
SystemArtifactCleanup = "SYSTEM_ARTIFACT_CLEANUP"
|
||||
// ScanDataExport : the name of the scan data export job
|
||||
ScanDataExport = "SCAN_DATA_EXPORT"
|
||||
)
|
||||
|
|
|
@ -17,6 +17,7 @@ package runtime
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/jobservice/job/impl/scandataexport"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
@ -319,6 +320,7 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(
|
|||
job.WebhookJob: (*notification.WebhookJob)(nil),
|
||||
job.SlackJob: (*notification.SlackJob)(nil),
|
||||
job.P2PPreheat: (*preheat.Job)(nil),
|
||||
job.ScanDataExport: (*scandataexport.ScanDataExport)(nil),
|
||||
// In v2.2 we migrate the scheduled replication, garbage collection and scan all to
|
||||
// the scheduler mechanism, the following three jobs are kept for the legacy jobs
|
||||
// and they can be removed after several releases
|
||||
|
|
15
src/pkg/scan/export/constants.go
Normal file
15
src/pkg/scan/export/constants.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package export
|
||||
|
||||
// CsvJobVendorID specific type to be used in contexts
|
||||
type CsvJobVendorID string
|
||||
|
||||
const (
|
||||
JobNameAttribute = "job_name"
|
||||
UserNameAttribute = "user_name"
|
||||
ScanDataExportDir = "/var/scandata_exports"
|
||||
QueryPageSize = 100
|
||||
DigestKey = "artifact_digest"
|
||||
CreateTimestampKey = "create_ts"
|
||||
Vendor = "SCAN_DATA_EXPORT"
|
||||
CsvJobVendorIDKey = CsvJobVendorID("vendorId")
|
||||
)
|
28
src/pkg/scan/export/digest_calculator.go
Normal file
28
src/pkg/scan/export/digest_calculator.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ArtifactDigestCalculator is an interface to be implemented by all file hash calculators
|
||||
type ArtifactDigestCalculator interface {
|
||||
// Calculate returns the hash for a file
|
||||
Calculate(fileName string) (digest.Digest, error)
|
||||
}
|
||||
|
||||
type SHA256ArtifactDigestCalculator struct{}
|
||||
|
||||
func (calc *SHA256ArtifactDigestCalculator) Calculate(fileName string) (digest.Digest, error) {
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return digest.NewDigest(digest.SHA256, hash), nil
|
||||
}
|
32
src/pkg/scan/export/digest_calculator_test.go
Normal file
32
src/pkg/scan/export/digest_calculator_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type DigestCalculatorTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *DigestCalculatorTestSuite) TestDigestCalculation() {
|
||||
fileName := "/tmp/testfile.txt"
|
||||
data := []byte("test")
|
||||
ioutil.WriteFile(fileName, data, os.ModePerm)
|
||||
digestCalc := SHA256ArtifactDigestCalculator{}
|
||||
digest, err := digestCalc.Calculate(fileName)
|
||||
suite.NoError(err)
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
expectedDigest := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
|
||||
suite.Equal(expectedDigest, digest.String())
|
||||
}
|
||||
|
||||
func TestDigestCalculatorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &DigestCalculatorTestSuite{})
|
||||
}
|
60
src/pkg/scan/export/export_data_selector.go
Normal file
60
src/pkg/scan/export/export_data_selector.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package export
|
||||
|
||||
import "github.com/bmatcuk/doublestar"
|
||||
|
||||
const (
|
||||
CVEIDMatches = "cveIdMatches"
|
||||
PackageMatches = "packageMatches"
|
||||
ScannerMatches = "scannerMatches"
|
||||
CVE2VectorMatches = "cve2VectorMatches"
|
||||
CVE3VectorMatches = "cve3VectorMatches"
|
||||
)
|
||||
|
||||
// VulnerabilityDataSelector is a specialized implementation of a selector
|
||||
// leveraging the doublestar pattern to select vulnerabilities
|
||||
type VulnerabilityDataSelector interface {
|
||||
Select(vulnDataRecords []Data, decoration string, pattern string) ([]Data, error)
|
||||
}
|
||||
|
||||
type defaultVulnerabilitySelector struct{}
|
||||
|
||||
// NewVulnerabilityDataSelector selects the vulnerability data record
|
||||
// that matches the provided conditions
|
||||
func NewVulnerabilityDataSelector() VulnerabilityDataSelector {
|
||||
return &defaultVulnerabilitySelector{}
|
||||
}
|
||||
|
||||
func (vds *defaultVulnerabilitySelector) Select(vulnDataRecords []Data, decoration string, pattern string) ([]Data, error) {
|
||||
selected := make([]Data, 0)
|
||||
value := ""
|
||||
|
||||
for _, vulnDataRecord := range vulnDataRecords {
|
||||
switch decoration {
|
||||
case CVEIDMatches:
|
||||
value = vulnDataRecord.CVEId
|
||||
case PackageMatches:
|
||||
value = vulnDataRecord.Package
|
||||
case ScannerMatches:
|
||||
value = vulnDataRecord.ScannerName
|
||||
case CVE2VectorMatches:
|
||||
value = vulnDataRecord.CVSSVectorV2
|
||||
case CVE3VectorMatches:
|
||||
value = vulnDataRecord.CVSSVectorV3
|
||||
}
|
||||
matched, err := vds.match(pattern, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matched {
|
||||
selected = append(selected, vulnDataRecord)
|
||||
}
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func (vds *defaultVulnerabilitySelector) match(pattern, str string) (bool, error) {
|
||||
if len(pattern) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
return doublestar.Match(pattern, str)
|
||||
}
|
123
src/pkg/scan/export/export_data_selector_test.go
Normal file
123
src/pkg/scan/export/export_data_selector_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ExportDataSelectorTestSuite struct {
|
||||
suite.Suite
|
||||
exportDataSelector VulnerabilityDataSelector
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) SetupSuite() {
|
||||
suite.exportDataSelector = NewVulnerabilityDataSelector()
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) TestCVEFilter() {
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVEIDMatches, "CVEId-1")
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(filtered))
|
||||
suite.Equal("CVEId-1", filtered[0].CVEId)
|
||||
}
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVEIDMatches, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) TestPackageFilter() {
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, PackageMatches, "Package1")
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(filtered))
|
||||
suite.Equal("Package1", filtered[0].Package)
|
||||
}
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, PackageMatches, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) TestScannerNameFilter() {
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, ScannerMatches, "TestScanner1")
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(filtered))
|
||||
suite.Equal("TestScanner1", filtered[0].ScannerName)
|
||||
}
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, ScannerMatches, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) TestCVE2VectorMatches() {
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVE2VectorMatches, "TestCVSSVectorV21")
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(filtered))
|
||||
suite.Equal("TestCVSSVectorV21", filtered[0].CVSSVectorV2)
|
||||
}
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVE2VectorMatches, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) TestCVE3VectorMatches() {
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVE3VectorMatches, "TestCVSSVectorV31")
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(filtered))
|
||||
suite.Equal("TestCVSSVectorV31", filtered[0].CVSSVectorV3)
|
||||
}
|
||||
{
|
||||
dataRecords := suite.createDataRecords(10, 1)
|
||||
filtered, err := suite.exportDataSelector.Select(dataRecords, CVE3VectorMatches, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(filtered))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportDataSelectorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ExportDataSelectorTestSuite{})
|
||||
}
|
||||
|
||||
func (suite *ExportDataSelectorTestSuite) createDataRecords(numRecs int, ownerId int64) []Data {
|
||||
data := make([]Data, 0)
|
||||
for i := 1; i <= numRecs; i++ {
|
||||
dataRec := Data{
|
||||
ID: int64(i),
|
||||
ProjectName: fmt.Sprintf("TestProject%d", i),
|
||||
ProjectOwner: strconv.FormatInt(ownerId, 10),
|
||||
ScannerName: fmt.Sprintf("TestScanner%d", i),
|
||||
CVEId: fmt.Sprintf("CVEId-%d", i),
|
||||
Package: fmt.Sprintf("Package%d", i),
|
||||
Severity: fmt.Sprintf("Severity%d", i),
|
||||
CVSSScoreV3: fmt.Sprintf("3.0"),
|
||||
CVSSScoreV2: fmt.Sprintf("2.0"),
|
||||
CVSSVectorV3: fmt.Sprintf("TestCVSSVectorV3%d", i),
|
||||
CVSSVectorV2: fmt.Sprintf("TestCVSSVectorV2%d", i),
|
||||
CWEIds: "",
|
||||
}
|
||||
data = append(data, dataRec)
|
||||
}
|
||||
return data
|
||||
}
|
142
src/pkg/scan/export/filter_processor.go
Normal file
142
src/pkg/scan/export/filter_processor.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/pkg"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/lib/selector"
|
||||
"github.com/goharbor/harbor/src/lib/selector/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/pkg/tag"
|
||||
"github.com/goharbor/harbor/src/pkg/user"
|
||||
)
|
||||
|
||||
type FilterProcessor interface {
|
||||
ProcessProjectFilter(ctx context.Context, userName string, projectsToFilter []int64) ([]int64, error)
|
||||
ProcessRepositoryFilter(ctx context.Context, filter string, projectIds []int64) ([]*selector.Candidate, error)
|
||||
ProcessTagFilter(ctx context.Context, filter string, repositoryIds []int64) ([]*selector.Candidate, error)
|
||||
}
|
||||
|
||||
type DefaultFilterProcessor struct {
|
||||
repoMgr repository.Manager
|
||||
tagMgr tag.Manager
|
||||
usrMgr user.Manager
|
||||
projectMgr project.Manager
|
||||
}
|
||||
|
||||
// NewFilterProcessor constructs an instance of a FilterProcessor
|
||||
func NewFilterProcessor() FilterProcessor {
|
||||
return &DefaultFilterProcessor{
|
||||
repoMgr: pkg.RepositoryMgr,
|
||||
tagMgr: tag.Mgr,
|
||||
usrMgr: user.Mgr,
|
||||
projectMgr: pkg.ProjectMgr,
|
||||
}
|
||||
}
|
||||
|
||||
func (dfp *DefaultFilterProcessor) ProcessProjectFilter(ctx context.Context, userName string, projectIdsToFilter []int64) ([]int64, error) {
|
||||
// get the user id of the current user
|
||||
|
||||
usr, err := dfp.usrMgr.GetByName(ctx, userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("Retrieved user id :%d for user name : %s", usr.UserID, userName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := dfp.getProjectQueryFilter(usr)
|
||||
projects, err := dfp.projectMgr.List(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("Selected %d projects administered by user %s ", len(projects), userName)
|
||||
projectIds := make([]int64, 0)
|
||||
for _, proj := range projects {
|
||||
projectIds = append(projectIds, proj.ProjectID)
|
||||
}
|
||||
|
||||
// check if the project ids specified in the filter are present in the list
|
||||
// of projects of which the current user is a project admin
|
||||
if len(projectIdsToFilter) == 0 {
|
||||
return projectIds, nil
|
||||
}
|
||||
m := make(map[int64]bool)
|
||||
for _, projectID := range projectIds {
|
||||
m[projectID] = true
|
||||
}
|
||||
filtered := make([]int64, 0)
|
||||
|
||||
for _, filteredProjID := range projectIdsToFilter {
|
||||
if m[filteredProjID] {
|
||||
filtered = append(filtered, filteredProjID)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (dfp *DefaultFilterProcessor) ProcessRepositoryFilter(ctx context.Context, filter string, projectIds []int64) ([]*selector.Candidate, error) {
|
||||
sel := doublestar.New(doublestar.RepoMatches, filter, "")
|
||||
candidates := make([]*selector.Candidate, 0)
|
||||
|
||||
for _, projectID := range projectIds {
|
||||
query := q.New(q.KeyWords{"ProjectID": projectID})
|
||||
allRepos, err := dfp.repoMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, repository := range allRepos {
|
||||
namespace, repo := utils.ParseRepository(repository.Name)
|
||||
candidates = append(candidates, &selector.Candidate{NamespaceID: repository.RepositoryID, Namespace: namespace, Repository: repo, Kind: "image"})
|
||||
}
|
||||
}
|
||||
// no repo filter specified then return all repos across all projects
|
||||
if filter == "" {
|
||||
return candidates, nil
|
||||
}
|
||||
return sel.Select(candidates)
|
||||
}
|
||||
|
||||
func (dfp *DefaultFilterProcessor) ProcessTagFilter(ctx context.Context, filter string, repositoryIds []int64) ([]*selector.Candidate, error) {
|
||||
sel := doublestar.New(doublestar.Matches, filter, "")
|
||||
candidates := make([]*selector.Candidate, 0)
|
||||
|
||||
for _, repoID := range repositoryIds {
|
||||
query := q.New(q.KeyWords{"RepositoryID": repoID})
|
||||
allTags, err := dfp.tagMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cand := &selector.Candidate{NamespaceID: repoID, Kind: "image"}
|
||||
for _, tag := range allTags {
|
||||
cand.Tags = append(cand.Tags, tag.Name)
|
||||
}
|
||||
candidates = append(candidates, cand)
|
||||
}
|
||||
// no tags specified then simply return all the candidates
|
||||
if filter == "" {
|
||||
return candidates, nil
|
||||
}
|
||||
return sel.Select(candidates)
|
||||
}
|
||||
|
||||
func (dfp *DefaultFilterProcessor) getProjectQueryFilter(user *commonmodels.User) *q.Query {
|
||||
secContext := local.NewSecurityContext(user)
|
||||
if secContext.IsSysAdmin() {
|
||||
logger.Infof("User %v is sys admin. Selecting all projects for export.", user.Username)
|
||||
return q.New(q.KeyWords{})
|
||||
}
|
||||
logger.Infof("User %v is not sys admin. Selecting projects with admin roles for export.", user.Username)
|
||||
return q.New(q.KeyWords{"member": &models.MemberQuery{UserID: user.UserID, Role: common.RoleProjectAdmin}})
|
||||
}
|
233
src/pkg/scan/export/filter_processor_test.go
Normal file
233
src/pkg/scan/export/filter_processor_test.go
Normal file
|
@ -0,0 +1,233 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"github.com/goharbor/harbor/src/pkg/repository/model"
|
||||
tag2 "github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/project"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/tag"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/user"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FilterProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
repoMgr *repository.Manager
|
||||
tagMgr *tag.FakeManager
|
||||
usrMgr *user.Manager
|
||||
projectMgr *project.Manager
|
||||
filterProcessor FilterProcessor
|
||||
}
|
||||
|
||||
func (suite *FilterProcessorTestSuite) SetupSuite() {
|
||||
|
||||
}
|
||||
|
||||
func (suite *FilterProcessorTestSuite) SetupTest() {
|
||||
suite.repoMgr = &repository.Manager{}
|
||||
suite.tagMgr = &tag.FakeManager{}
|
||||
suite.usrMgr = &user.Manager{}
|
||||
suite.projectMgr = &project.Manager{}
|
||||
suite.filterProcessor = &DefaultFilterProcessor{
|
||||
repoMgr: suite.repoMgr,
|
||||
tagMgr: suite.tagMgr,
|
||||
usrMgr: suite.usrMgr,
|
||||
projectMgr: suite.projectMgr,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FilterProcessorTestSuite) TestProcessProjectFilter() {
|
||||
project1 := &models.Project{ProjectID: 1}
|
||||
|
||||
project2 := &models.Project{ProjectID: 2}
|
||||
|
||||
// no filtered projects returns all projects
|
||||
{
|
||||
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1}, nil).Once()
|
||||
suite.projectMgr.On("List", mock.Anything, mock.Anything).Return([]*models.Project{project1, project2}, nil).Once()
|
||||
projectIds, err := suite.filterProcessor.ProcessProjectFilter(context.TODO(), "test-user", []int64{})
|
||||
suite.Equal(2, len(projectIds))
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// filtered project
|
||||
{
|
||||
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1}, nil).Once()
|
||||
suite.projectMgr.On("List", mock.Anything, mock.Anything).Return([]*models.Project{project1, project2}, nil).Once()
|
||||
projectIds, err := suite.filterProcessor.ProcessProjectFilter(context.TODO(), "test-user", []int64{1})
|
||||
suite.Equal(1, len(projectIds))
|
||||
suite.Equal(int64(1), projectIds[0])
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// project listing for admin user
|
||||
{
|
||||
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1, SysAdminFlag: true}, nil).Once()
|
||||
suite.projectMgr.On("List", mock.Anything, mock.Anything).Return([]*models.Project{project1, project2}, nil).Once()
|
||||
_, err := suite.filterProcessor.ProcessProjectFilter(context.TODO(), "test-user", []int64{1})
|
||||
suite.NoError(err)
|
||||
queryArgumentMatcher := testifymock.MatchedBy(func(query *q.Query) bool {
|
||||
return len(query.Keywords) == 0
|
||||
})
|
||||
suite.projectMgr.AssertCalled(suite.T(), "List", mock.Anything, queryArgumentMatcher)
|
||||
}
|
||||
|
||||
// project listing returns an error
|
||||
// filtered project
|
||||
{
|
||||
suite.usrMgr.On("GetByName", mock.Anything, "test-user").Return(&commonmodels.User{UserID: 1}, nil).Once()
|
||||
suite.projectMgr.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("test-error")).Once()
|
||||
projectIds, err := suite.filterProcessor.ProcessProjectFilter(context.TODO(), "test-user", []int64{1})
|
||||
suite.Error(err)
|
||||
suite.Nil(projectIds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *FilterProcessorTestSuite) TestProcessRepositoryFilter() {
|
||||
|
||||
repoRecord1 := model.RepoRecord{
|
||||
RepositoryID: int64(1),
|
||||
Name: "test/repo1",
|
||||
ProjectID: int64(100),
|
||||
Description: "test repo 1",
|
||||
PullCount: 1,
|
||||
StarCount: 4,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
repoRecord2 := model.RepoRecord{
|
||||
RepositoryID: int64(1),
|
||||
Name: "test/repo2",
|
||||
ProjectID: int64(100),
|
||||
Description: "test repo 2",
|
||||
PullCount: 1,
|
||||
StarCount: 4,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
|
||||
allRepos := make([]*model.RepoRecord, 0)
|
||||
allRepos = append(allRepos, &repoRecord1, &repoRecord2)
|
||||
|
||||
// filter required repositories
|
||||
{
|
||||
suite.repoMgr.On("List", mock.Anything, mock.Anything).Return(allRepos, nil).Once()
|
||||
candidates, err := suite.filterProcessor.ProcessRepositoryFilter(context.TODO(), "repo1", []int64{100})
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(candidates), "Expected 1 candidate but found ", len(candidates))
|
||||
suite.Equal("repo1", candidates[0].Repository)
|
||||
}
|
||||
|
||||
// simulate repo manager returning an error
|
||||
{
|
||||
suite.repoMgr.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("test error")).Once()
|
||||
candidates, err := suite.filterProcessor.ProcessRepositoryFilter(context.TODO(), "repo1", []int64{100})
|
||||
suite.Error(err)
|
||||
suite.Nil(candidates)
|
||||
}
|
||||
|
||||
// simulate doublestar filtering
|
||||
{
|
||||
repoRecord3 := model.RepoRecord{
|
||||
RepositoryID: int64(1),
|
||||
Name: "test/repo1/ubuntu",
|
||||
ProjectID: int64(100),
|
||||
Description: "test repo 1",
|
||||
PullCount: 1,
|
||||
StarCount: 4,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
repoRecord4 := model.RepoRecord{
|
||||
RepositoryID: int64(1),
|
||||
Name: "test/repo1/centos",
|
||||
ProjectID: int64(100),
|
||||
Description: "test repo 2",
|
||||
PullCount: 1,
|
||||
StarCount: 4,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
allRepos = append(allRepos, &repoRecord3, &repoRecord4)
|
||||
suite.repoMgr.On("List", mock.Anything, mock.Anything).Return(allRepos, nil).Once()
|
||||
candidates, err := suite.filterProcessor.ProcessRepositoryFilter(context.TODO(), "repo1/**", []int64{100})
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, len(candidates), "Expected 2 candidate but found ", len(candidates))
|
||||
m := map[string]bool{}
|
||||
for _, cand := range candidates {
|
||||
m[cand.Repository] = true
|
||||
}
|
||||
_, ok := m["repo1/ubuntu"]
|
||||
suite.True(ok)
|
||||
_, ok = m["repo1/centos"]
|
||||
suite.True(ok)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FilterProcessorTestSuite) TestProcessTagFilter() {
|
||||
|
||||
testTag1 := tag2.Tag{
|
||||
ID: int64(1),
|
||||
RepositoryID: int64(1),
|
||||
ArtifactID: int64(1),
|
||||
Name: "test-tag1",
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{},
|
||||
}
|
||||
|
||||
testTag2 := tag2.Tag{
|
||||
ID: int64(2),
|
||||
RepositoryID: int64(1),
|
||||
ArtifactID: int64(1),
|
||||
Name: "test-tag2",
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{},
|
||||
}
|
||||
|
||||
testTag3 := tag2.Tag{
|
||||
ID: int64(3),
|
||||
RepositoryID: int64(2),
|
||||
ArtifactID: int64(2),
|
||||
Name: "test-tag3",
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{},
|
||||
}
|
||||
|
||||
allTags := make([]*tag2.Tag, 0)
|
||||
|
||||
allTags = append(allTags, &testTag1, &testTag2)
|
||||
|
||||
// filter required repositories haveing the specified tags
|
||||
{
|
||||
suite.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag2.Tag{&testTag1, &testTag2}, nil).Once()
|
||||
suite.tagMgr.On("List", mock.Anything, mock.Anything).Return([]*tag2.Tag{&testTag3}, nil).Once()
|
||||
|
||||
candidates, err := suite.filterProcessor.ProcessTagFilter(context.TODO(), "*tag2", []int64{1, 2})
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(candidates), "Expected 1 candidate but found ", len(candidates))
|
||||
suite.Equal(int64(1), candidates[0].NamespaceID)
|
||||
}
|
||||
|
||||
// simulate repo manager returning an error
|
||||
{
|
||||
suite.tagMgr.On("List", mock.Anything, mock.Anything).Return(nil, errors.New("test error")).Once()
|
||||
candidates, err := suite.filterProcessor.ProcessTagFilter(context.TODO(), "repo1", []int64{1, 2})
|
||||
suite.Error(err)
|
||||
suite.Nil(candidates)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFilterProcessorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &FilterProcessorTestSuite{})
|
||||
}
|
233
src/pkg/scan/export/manager.go
Normal file
233
src/pkg/scan/export/manager.go
Normal file
|
@ -0,0 +1,233 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
beego_orm "github.com/beego/beego/orm"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
q2 "github.com/goharbor/harbor/src/lib/q"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
VulnScanReportView = "vuln_scan_report"
|
||||
VulnScanReportQuery = `select row_number() over() as result_row_id, project.project_id as project_id, project."name" as project_name, harbor_user.user_id as user_id, harbor_user.username as project_owner, repository.repository_id, repository.name as repository_name,
|
||||
scanner_registration.id as scanner_id, scanner_registration."name" as scanner_name,
|
||||
vulnerability_record.cve_id, vulnerability_record.package, vulnerability_record.severity,
|
||||
vulnerability_record.cvss_score_v3, vulnerability_record.cvss_score_v2, vulnerability_record.cvss_vector_v3, vulnerability_record.cvss_vector_v2, vulnerability_record.cwe_ids from report_vulnerability_record inner join scan_report on report_vulnerability_record.report_uuid = scan_report.uuid
|
||||
inner join artifact on scan_report.digest = artifact.digest
|
||||
inner join artifact_reference on artifact.id = artifact_reference.child_id
|
||||
inner join vulnerability_record on report_vulnerability_record.vuln_record_id = vulnerability_record.id
|
||||
inner join project on artifact.project_id = project.project_id
|
||||
inner join repository on artifact.repository_id = repository.repository_id
|
||||
inner join tag on tag.repository_id = repository.repository_id
|
||||
inner join harbor_user on project.owner_id = harbor_user.user_id
|
||||
inner join scanner_registration on scan_report.registration_uuid = scanner_registration.uuid `
|
||||
ArtifactBylabelQueryTemplate = "select distinct artifact.id from artifact inner join label_reference on artifact.id = label_reference.artifact_id inner join harbor_label on label_reference.label_id = harbor_label.id and harbor_label.id in (%s)"
|
||||
SQLAnd = " and "
|
||||
RepositoryIDColumn = "repository.repository_id"
|
||||
ProjectIDColumn = "project.project_id"
|
||||
TagIDColumn = "tag.id"
|
||||
ArtifactParentIDColumn = "artifact_reference.parent_id"
|
||||
GroupBy = " group by "
|
||||
GroupByCols = `package, vulnerability_record.severity, vulnerability_record.cve_id, project.project_id, harbor_user.user_id ,
|
||||
repository.repository_id, scanner_registration.id, vulnerability_record.cvss_score_v3,
|
||||
vulnerability_record.cvss_score_v2, vulnerability_record.cvss_vector_v3, vulnerability_record.cvss_vector_v2,
|
||||
vulnerability_record.cwe_ids`
|
||||
JobModeExport = "export"
|
||||
JobModeKey = "mode"
|
||||
)
|
||||
|
||||
var (
|
||||
Mgr = NewManager()
|
||||
)
|
||||
|
||||
// Params specifies the filters for controlling the scan data export process
|
||||
type Params struct {
|
||||
// cve ids
|
||||
CVEIds string
|
||||
|
||||
// A list of one or more labels for which to export the scan data, defaults to all if empty
|
||||
Labels []int64
|
||||
|
||||
// A list of one or more projects for which to export the scan data, defaults to all if empty
|
||||
Projects []int64
|
||||
|
||||
// A list of repositories for which to export the scan data, defaults to all if empty
|
||||
Repositories []int64
|
||||
|
||||
// A list of tags for which to export the scan data, defaults to all if empty
|
||||
Tags []int64
|
||||
|
||||
// PageNumber
|
||||
PageNumber int64
|
||||
|
||||
// PageSize
|
||||
PageSize int64
|
||||
}
|
||||
|
||||
// FromJSON parses robot from json data
|
||||
func (p *Params) FromJSON(jsonData string) error {
|
||||
if len(jsonData) == 0 {
|
||||
return errors.New("empty json data to parse")
|
||||
}
|
||||
|
||||
return json.Unmarshal([]byte(jsonData), p)
|
||||
}
|
||||
|
||||
// ToJSON marshals Robot to JSON data
|
||||
func (p *Params) ToJSON() (string, error) {
|
||||
data, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
Fetch(ctx context.Context, params Params) ([]Data, error)
|
||||
}
|
||||
|
||||
type exportManager struct {
|
||||
exportDataFilter VulnerabilityDataSelector
|
||||
}
|
||||
|
||||
func NewManager() Manager {
|
||||
return &exportManager{exportDataFilter: NewVulnerabilityDataSelector()}
|
||||
}
|
||||
|
||||
func (em *exportManager) Fetch(ctx context.Context, params Params) ([]Data, error) {
|
||||
exportData := make([]Data, 0)
|
||||
artifactIdsWithLabel, err := em.getArtifactsWithLabel(ctx, params.Labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if labels are present but no artifact ids were retrieved then return empty
|
||||
// results
|
||||
if len(params.Labels) > 0 && len(artifactIdsWithLabel) == 0 {
|
||||
return exportData, nil
|
||||
}
|
||||
|
||||
rawSeter, _ := em.buildQuery(ctx, params, artifactIdsWithLabel)
|
||||
_, err = rawSeter.QueryRows(&exportData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exportData, err = em.exportDataFilter.Select(exportData, CVEIDMatches, params.CVEIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return exportData, nil
|
||||
}
|
||||
|
||||
func (em *exportManager) buildQuery(ctx context.Context, params Params, artifactsWithLabel []int64) (beego_orm.RawSeter, error) {
|
||||
sql := VulnScanReportQuery
|
||||
filterFragment, err := em.getFilters(ctx, params, artifactsWithLabel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(filterFragment) > 0 {
|
||||
sql = fmt.Sprintf("%s %s %s %s %s", VulnScanReportQuery, SQLAnd, filterFragment, GroupBy, GroupByCols)
|
||||
}
|
||||
logger.Infof("SQL query : %s", sql)
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("Parameters : %v", params)
|
||||
pageSize := params.PageSize
|
||||
q := &q2.Query{
|
||||
Keywords: nil,
|
||||
Sorts: nil,
|
||||
PageNumber: params.PageNumber,
|
||||
PageSize: pageSize,
|
||||
Sorting: "",
|
||||
}
|
||||
logger.Infof("Query constructed : %v", q)
|
||||
paginationParams := make([]interface{}, 0)
|
||||
query, pageLimits := orm.PaginationOnRawSQL(q, sql, paginationParams)
|
||||
logger.Infof("Final Paginated query : %s", query)
|
||||
logger.Infof("Final pagination parameters %v", pageLimits)
|
||||
return ormer.Raw(query, pageLimits), nil
|
||||
}
|
||||
|
||||
func (em *exportManager) getFilters(ctx context.Context, params Params, artifactsWithLabel []int64) (string, error) {
|
||||
// it is required that the request payload contains only IDs of the
|
||||
// projects, repositories, tags and label objects.
|
||||
// only CVE ID fields can be strings
|
||||
filters := make([]string, 0)
|
||||
if params.Repositories != nil {
|
||||
filters = em.buildIDFilterFragmentWithIn(params.Repositories, filters, RepositoryIDColumn)
|
||||
}
|
||||
if params.Projects != nil {
|
||||
filters = em.buildIDFilterFragmentWithIn(params.Projects, filters, ProjectIDColumn)
|
||||
}
|
||||
if params.Tags != nil {
|
||||
filters = em.buildIDFilterFragmentWithIn(params.Tags, filters, TagIDColumn)
|
||||
}
|
||||
|
||||
if len(artifactsWithLabel) > 0 {
|
||||
filters = em.buildIDFilterFragmentWithIn(artifactsWithLabel, filters, ArtifactParentIDColumn)
|
||||
}
|
||||
|
||||
if len(filters) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
logger.Infof("All filters : %v", filters)
|
||||
completeFilter := strings.Builder{}
|
||||
for _, filter := range filters {
|
||||
if completeFilter.Len() > 0 {
|
||||
completeFilter.WriteString(SQLAnd)
|
||||
}
|
||||
completeFilter.WriteString(filter)
|
||||
}
|
||||
return completeFilter.String(), nil
|
||||
}
|
||||
|
||||
func (em *exportManager) buildIDFilterFragmentWithIn(ids []int64, filters []string, column string) []string {
|
||||
if len(ids) == 0 {
|
||||
return filters
|
||||
}
|
||||
strIds := make([]string, 0)
|
||||
for _, id := range ids {
|
||||
strIds = append(strIds, strconv.FormatInt(id, 10))
|
||||
}
|
||||
filters = append(filters, fmt.Sprintf(" %s in (%s)", column, strings.Join(strIds, ",")))
|
||||
return filters
|
||||
}
|
||||
|
||||
// utility method to get all child artifacts belonging to a parent containing
|
||||
// the specified label ids.
|
||||
// Within Harbor, labels are attached to the root artifact whereas scan results
|
||||
// are associated with the child artifact.
|
||||
func (em *exportManager) getArtifactsWithLabel(ctx context.Context, ids []int64) ([]int64, error) {
|
||||
artifactIds := make([]int64, 0)
|
||||
|
||||
if len(ids) == 0 {
|
||||
return artifactIds, nil
|
||||
}
|
||||
strIds := make([]string, 0)
|
||||
for _, id := range ids {
|
||||
strIds = append(strIds, strconv.FormatInt(id, 10))
|
||||
}
|
||||
artifactQuery := fmt.Sprintf(ArtifactBylabelQueryTemplate, strings.Join(strIds, ","))
|
||||
logger.Infof("Triggering artifact query : %s", artifactQuery)
|
||||
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numRows, err := ormer.Raw(artifactQuery).QueryRows(&artifactIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("Found %d artifacts with specified tags", numRows)
|
||||
|
||||
return artifactIds, nil
|
||||
}
|
371
src/pkg/scan/export/manager_test.go
Normal file
371
src/pkg/scan/export/manager_test.go
Normal file
|
@ -0,0 +1,371 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogo/protobuf/sortkeys"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
artifactDao "github.com/goharbor/harbor/src/pkg/artifact/dao"
|
||||
labelDao "github.com/goharbor/harbor/src/pkg/label/dao"
|
||||
labelModel "github.com/goharbor/harbor/src/pkg/label/model"
|
||||
projectDao "github.com/goharbor/harbor/src/pkg/project/dao"
|
||||
repoDao "github.com/goharbor/harbor/src/pkg/repository/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/repository/model"
|
||||
daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
tagDao "github.com/goharbor/harbor/src/pkg/tag/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||
userDao "github.com/goharbor/harbor/src/pkg/user/dao"
|
||||
htesting "github.com/goharbor/harbor/src/testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const RegistrationUUID = "scannerIdExportData"
|
||||
const ReportUUD = "reportUUId"
|
||||
|
||||
type ExportManagerSuite struct {
|
||||
htesting.Suite
|
||||
artifactDao artifactDao.DAO
|
||||
projectDao projectDao.DAO
|
||||
userDao userDao.DAO
|
||||
repositoryDao repoDao.DAO
|
||||
tagDao tagDao.DAO
|
||||
scanDao daoscan.DAO
|
||||
vulnDao daoscan.VulnerabilityRecordDao
|
||||
labelDao labelDao.DAO
|
||||
exportManager Manager
|
||||
testDataId testDataIds
|
||||
}
|
||||
|
||||
type testDataIds struct {
|
||||
artifactId []int64
|
||||
repositoryId []int64
|
||||
artRefId []int64
|
||||
reportId []int64
|
||||
tagId []int64
|
||||
vulnRecs []int64
|
||||
labelId []int64
|
||||
labelRefId []int64
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) SetupSuite() {
|
||||
suite.Suite.SetupSuite()
|
||||
suite.artifactDao = artifactDao.New()
|
||||
suite.projectDao = projectDao.New()
|
||||
suite.userDao = userDao.New()
|
||||
suite.repositoryDao = repoDao.New()
|
||||
suite.tagDao = tagDao.New()
|
||||
suite.scanDao = daoscan.New()
|
||||
suite.vulnDao = daoscan.NewVulnerabilityRecordDao()
|
||||
suite.labelDao = labelDao.New()
|
||||
suite.exportManager = NewManager()
|
||||
suite.setupTestData()
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) TearDownSuite() {
|
||||
suite.clearTestData()
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) SetupTest() {
|
||||
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) TearDownTest() {
|
||||
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) clearTestData() {
|
||||
|
||||
for _, labelRefId := range suite.testDataId.labelRefId {
|
||||
err := suite.labelDao.DeleteReference(suite.Context(), labelRefId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// delete labels and label references
|
||||
for _, labelId := range suite.testDataId.labelId {
|
||||
err := suite.labelDao.Delete(suite.Context(), labelId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
for _, artRefId := range suite.testDataId.artRefId {
|
||||
err := suite.artifactDao.DeleteReference(suite.Context(), artRefId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
for _, repoId := range suite.testDataId.repositoryId {
|
||||
err := suite.repositoryDao.Delete(suite.Context(), repoId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
for _, tagId := range suite.testDataId.tagId {
|
||||
err := suite.tagDao.Delete(suite.Context(), tagId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
for _, artId := range suite.testDataId.artifactId {
|
||||
err := suite.artifactDao.Delete(suite.Context(), artId)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
err := scanner.DeleteRegistration(suite.Context(), RegistrationUUID)
|
||||
suite.NoError(err, "Error when cleaning up scanner registrations")
|
||||
|
||||
suite.cleanUpAdditionalData(ReportUUD, RegistrationUUID)
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) TestExport() {
|
||||
// no label based filtering
|
||||
{
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), Params{})
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(data))
|
||||
}
|
||||
|
||||
// with label based filtering. all 10 records should be got back since there is a
|
||||
// label associated with the artifact
|
||||
{
|
||||
p := Params{
|
||||
Labels: suite.testDataId.labelId,
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(10, len(data))
|
||||
}
|
||||
|
||||
// with label based filtering and specifying a non-existent label Id.
|
||||
// should return 0 records
|
||||
{
|
||||
allLabels, err := suite.labelDao.List(suite.Context(), &q.Query{})
|
||||
suite.NoError(err)
|
||||
allLabelIds := make([]int64, 0)
|
||||
for _, lbl := range allLabels {
|
||||
allLabelIds = append(allLabelIds, lbl.ID)
|
||||
}
|
||||
sortkeys.Int64s(allLabelIds)
|
||||
|
||||
p := Params{
|
||||
Labels: []int64{allLabelIds[len(allLabelIds)-1] + int64(1)},
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, len(data))
|
||||
}
|
||||
|
||||
// specify a project id that does not exist in the system.
|
||||
{
|
||||
p := Params{
|
||||
Projects: []int64{int64(-1)},
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, len(data))
|
||||
}
|
||||
|
||||
// specify a non existent repository
|
||||
{
|
||||
p := Params{
|
||||
Repositories: []int64{int64(-1)},
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, len(data))
|
||||
}
|
||||
|
||||
// specify a non-existent tag
|
||||
{
|
||||
p := Params{
|
||||
Tags: []int64{int64(-1)},
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) TestExportWithCVEFilter() {
|
||||
{
|
||||
p := Params{
|
||||
CVEIds: "CVE-ID2",
|
||||
}
|
||||
data, err := suite.exportManager.Fetch(suite.Context(), p)
|
||||
suite.NoError(err)
|
||||
suite.Equal(1, len(data))
|
||||
suite.Equal(p.CVEIds, data[0].CVEId)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) registerScanner(registrationUUID string) {
|
||||
r := &scanner.Registration{
|
||||
UUID: registrationUUID,
|
||||
Name: registrationUUID,
|
||||
Description: "sample registration",
|
||||
URL: fmt.Sprintf("https://sample.scanner.com/%s", registrationUUID),
|
||||
}
|
||||
|
||||
_, err := scanner.AddRegistration(suite.Context(), r)
|
||||
suite.NoError(err, "add new registration")
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) generateVulnerabilityRecordsForReport(registrationUUID string, numRecords int) []*daoscan.VulnerabilityRecord {
|
||||
vulns := make([]*daoscan.VulnerabilityRecord, 0)
|
||||
for i := 1; i <= numRecords; i++ {
|
||||
vulnV2 := new(daoscan.VulnerabilityRecord)
|
||||
vulnV2.CVEID = fmt.Sprintf("CVE-ID%d", i)
|
||||
vulnV2.Package = fmt.Sprintf("Package%d", i)
|
||||
vulnV2.PackageVersion = "NotAvailable"
|
||||
vulnV2.PackageType = "Unknown"
|
||||
vulnV2.Fix = "1.0.0"
|
||||
vulnV2.URLs = "url1"
|
||||
vulnV2.RegistrationUUID = registrationUUID
|
||||
if i%2 == 0 {
|
||||
vulnV2.Severity = "High"
|
||||
} else if i%3 == 0 {
|
||||
vulnV2.Severity = "Medium"
|
||||
} else if i%4 == 0 {
|
||||
vulnV2.Severity = "Critical"
|
||||
} else {
|
||||
vulnV2.Severity = "Low"
|
||||
}
|
||||
vulns = append(vulns, vulnV2)
|
||||
}
|
||||
|
||||
return vulns
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) insertVulnRecordForReport(reportUUID string, vr *daoscan.VulnerabilityRecord) {
|
||||
id, err := suite.vulnDao.Create(suite.Context(), vr)
|
||||
suite.NoError(err, "Failed to create vulnerability record")
|
||||
suite.testDataId.vulnRecs = append(suite.testDataId.vulnRecs, id)
|
||||
|
||||
err = suite.vulnDao.InsertForReport(suite.Context(), reportUUID, id)
|
||||
suite.NoError(err, "Failed to insert vulnerability record row for report %s", reportUUID)
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) cleanUpAdditionalData(reportID string, scannerID string) {
|
||||
_, err := suite.scanDao.DeleteMany(suite.Context(), q.Query{Keywords: q.KeyWords{"uuid": reportID}})
|
||||
|
||||
suite.NoError(err)
|
||||
_, err = suite.vulnDao.DeleteForReport(suite.Context(), reportID)
|
||||
suite.NoError(err, "Failed to cleanup records")
|
||||
_, err = suite.vulnDao.DeleteForScanner(suite.Context(), scannerID)
|
||||
suite.NoError(err, "Failed to delete vulnerability records")
|
||||
}
|
||||
|
||||
func (suite *ExportManagerSuite) setupTestData() {
|
||||
// create repositories
|
||||
repoRecord := &model.RepoRecord{
|
||||
Name: "library/ubuntu",
|
||||
ProjectID: 1,
|
||||
Description: "",
|
||||
PullCount: 1,
|
||||
StarCount: 0,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
repoId, err := suite.repositoryDao.Create(suite.Context(), repoRecord)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.repositoryId = append(suite.testDataId.repositoryId, repoId)
|
||||
|
||||
// create artifacts for repositories
|
||||
art := &artifactDao.Artifact{
|
||||
Type: "IMAGE",
|
||||
MediaType: "application/vnd.docker.container.image.v1+json",
|
||||
ManifestMediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
ProjectID: 1,
|
||||
RepositoryID: repoId,
|
||||
RepositoryName: "library/ubuntu",
|
||||
Digest: "sha256:e3d7ff9efd8431d9ef39a144c45992df5502c995b9ba3c53ff70c5b52a848d9c",
|
||||
Size: 28573056,
|
||||
Icon: "",
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{}.Add(-10 * time.Minute),
|
||||
ExtraAttrs: `{"architecture":"amd64","author":"","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/bash"]},"created":"2021-03-04T02:24:42.927713926Z","os":"linux"}`,
|
||||
Annotations: "",
|
||||
}
|
||||
artId, err := suite.artifactDao.Create(suite.Context(), art)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.artifactId = append(suite.testDataId.artifactId, artId)
|
||||
|
||||
// create a tag and associate with the repository
|
||||
t := &tag.Tag{
|
||||
RepositoryID: repoId,
|
||||
ArtifactID: artId,
|
||||
Name: "latest",
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{},
|
||||
}
|
||||
tagId, err := suite.tagDao.Create(suite.Context(), t)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.tagId = append(suite.testDataId.tagId, tagId)
|
||||
|
||||
// create an artifact reference
|
||||
artReference := &artifactDao.ArtifactReference{
|
||||
ParentID: artId,
|
||||
ChildID: artId,
|
||||
ChildDigest: "sha256:e3d7ff9efd8431d9ef39a144c45992df5502c995b9ba3c53ff70c5b52a848d9c",
|
||||
Platform: `{"architecture":"amd64","os":"linux"}`,
|
||||
URLs: "",
|
||||
Annotations: "",
|
||||
}
|
||||
artRefId, err := suite.artifactDao.CreateReference(suite.Context(), artReference)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.artRefId = append(suite.testDataId.artRefId, artRefId)
|
||||
|
||||
// create a label
|
||||
l := labelModel.Label{
|
||||
Name: "TestLabel",
|
||||
Description: "",
|
||||
Color: "Green",
|
||||
Level: "",
|
||||
Scope: "",
|
||||
ProjectID: 1,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
Deleted: false,
|
||||
}
|
||||
labelId, err := suite.labelDao.Create(suite.Context(), &l)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.labelId = append(suite.testDataId.labelId, labelId)
|
||||
|
||||
lRef := labelModel.Reference{
|
||||
ID: 0,
|
||||
LabelID: labelId,
|
||||
ArtifactID: artId,
|
||||
CreationTime: time.Time{},
|
||||
UpdateTime: time.Time{},
|
||||
}
|
||||
lRefId, err := suite.labelDao.CreateReference(suite.Context(), &lRef)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.labelRefId = append(suite.testDataId.labelRefId, lRefId)
|
||||
|
||||
// register a scanner
|
||||
suite.registerScanner(RegistrationUUID)
|
||||
|
||||
// create a vulnerability scan report
|
||||
r := &daoscan.Report{
|
||||
UUID: ReportUUD,
|
||||
Digest: "sha256:e3d7ff9efd8431d9ef39a144c45992df5502c995b9ba3c53ff70c5b52a848d9c",
|
||||
RegistrationUUID: RegistrationUUID,
|
||||
MimeType: v1.MimeTypeGenericVulnerabilityReport,
|
||||
Status: job.PendingStatus.String(),
|
||||
Report: "",
|
||||
}
|
||||
reportId, err := suite.scanDao.Create(suite.Context(), r)
|
||||
suite.NoError(err)
|
||||
suite.testDataId.reportId = append(suite.testDataId.reportId, reportId)
|
||||
|
||||
// generate vulnerability records for the report
|
||||
vulns := suite.generateVulnerabilityRecordsForReport(RegistrationUUID, 10)
|
||||
suite.NotEmpty(vulns)
|
||||
|
||||
for _, vuln := range vulns {
|
||||
suite.insertVulnRecordForReport(ReportUUD, vuln)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportManager(t *testing.T) {
|
||||
suite.Run(t, &ExportManagerSuite{})
|
||||
}
|
115
src/pkg/scan/export/model.go
Normal file
115
src/pkg/scan/export/model.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Data models a single row of the exported scan vulnerability data
|
||||
|
||||
type Data struct {
|
||||
ID int64 `orm:"column(result_row_id)" csv:"RowId"`
|
||||
ProjectName string `orm:"column(project_name)" csv:"Project"`
|
||||
ProjectOwner string `orm:"column(project_owner)" csv:"Owner"`
|
||||
ScannerName string `orm:"column(scanner_name)" csv:"Scanner"`
|
||||
Repository string `orm:"column(repository_name)" csv:"Repository"`
|
||||
ArtifactDigest string `orm:"column(artifact_digest)" csv:"Artifact Digest"`
|
||||
CVEId string `orm:"column(cve_id)" csv:"CVE"`
|
||||
Package string `orm:"column(package)" csv:"Package"`
|
||||
Severity string `orm:"column(severity)" csv:"Severity"`
|
||||
CVSSScoreV3 string `orm:"column(cvss_score_v3)" csv:"CVSS V3 Score"`
|
||||
CVSSScoreV2 string `orm:"column(cvss_score_v2)" csv:"CVSS V2 Score"`
|
||||
CVSSVectorV3 string `orm:"column(cvss_vector_v3)" csv:"CVSS V3 Vector"`
|
||||
CVSSVectorV2 string `orm:"column(cvss_vector_v2)" csv:"CVSS V2 Vector"`
|
||||
CWEIds string `orm:"column(cwe_ids)" csv:"CWE Ids"`
|
||||
}
|
||||
|
||||
// Request encapsulates the filters to be provided when exporting the data for a scan.
|
||||
type Request struct {
|
||||
|
||||
// UserID contains the database identity of the user initiating the export request
|
||||
UserID int
|
||||
|
||||
// UserName contains the name of the user initiating the export request
|
||||
UserName string
|
||||
|
||||
// JobName contains the name of the job as specified by the external client.
|
||||
JobName string
|
||||
|
||||
// cve ids
|
||||
CVEIds string
|
||||
|
||||
// A list of one or more labels for which to export the scan data, defaults to all if empty
|
||||
Labels []int64
|
||||
|
||||
// A list of one or more projects for which to export the scan data, defaults to all if empty
|
||||
Projects []int64
|
||||
|
||||
// A list of repositories for which to export the scan data, defaults to all if empty
|
||||
Repositories string
|
||||
|
||||
// A list of tags for which to export the scan data, defaults to all if empty
|
||||
Tags string
|
||||
}
|
||||
|
||||
// FromJSON parses robot from json data
|
||||
func (c *Request) FromJSON(jsonData string) error {
|
||||
if len(jsonData) == 0 {
|
||||
return errors.New("empty json data to parse")
|
||||
}
|
||||
|
||||
return json.Unmarshal([]byte(jsonData), c)
|
||||
}
|
||||
|
||||
// ToJSON marshals Robot to JSON data
|
||||
func (c *Request) ToJSON() (string, error) {
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Execution provides details about the running status of a scan data export job
|
||||
type Execution struct {
|
||||
// ID of the execution
|
||||
ID int64
|
||||
// UserID triggering the execution
|
||||
UserID int64
|
||||
// Status provides the status of the execution
|
||||
Status string
|
||||
// StatusMessage contains the human-readable status message for the execution
|
||||
StatusMessage string
|
||||
// Trigger indicates the mode of trigger for the job execution
|
||||
Trigger string
|
||||
// StartTime contains the start time instant of the execution
|
||||
StartTime time.Time
|
||||
// EndTime contains the end time instant of the execution
|
||||
EndTime time.Time
|
||||
// ExportDataDigest contains the SHA256 hash of the exported scan data artifact
|
||||
ExportDataDigest string
|
||||
// Name of the job as specified during the export task invocation
|
||||
JobName string
|
||||
// Name of the user triggering the job
|
||||
UserName string
|
||||
// FilePresent is true if file artifact is actually present, false otherwise
|
||||
FilePresent bool
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
// ID of the scan data export task
|
||||
ID int64
|
||||
// Job Id corresponding to the task
|
||||
JobID string
|
||||
// Status of the current task execution
|
||||
Status string
|
||||
// Status message for the current task execution
|
||||
StatusMessage string
|
||||
}
|
||||
|
||||
type TriggerParam struct {
|
||||
TimeWindowMinutes int
|
||||
PageSize int
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/goharbor/harbor/src/pkg/systemartifact/model"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -18,8 +19,8 @@ var (
|
|||
keyFormat = "%s:%s"
|
||||
)
|
||||
|
||||
const repositoryFormat = "sys_harbor/%s/%s"
|
||||
const systemArtifactProjectName = "sys_h@rb0r"
|
||||
const repositoryFormat = "sys_harb0r/%s/%s"
|
||||
const systemArtifactProjectName = "sys_harb0r"
|
||||
|
||||
// Manager provides a low-level interface for harbor services
|
||||
// to create registry artifacts containing arbitrary data but which
|
||||
|
@ -91,7 +92,10 @@ func NewManager() Manager {
|
|||
|
||||
func (mgr *systemArtifactManager) Create(ctx context.Context, artifactRecord *model.SystemArtifact, reader io.Reader) (int64, error) {
|
||||
var artifactID int64
|
||||
|
||||
// create time defaults to current time if unset
|
||||
if artifactRecord.CreateTime.IsZero() {
|
||||
artifactRecord.CreateTime = time.Now()
|
||||
}
|
||||
// the entire create operation is executed within a transaction to ensure that any failures
|
||||
// during the blob creation or tracking record creation result in a rollback of the transaction
|
||||
createError := orm.WithTransaction(func(ctx context.Context) error {
|
||||
|
@ -103,6 +107,7 @@ func (mgr *systemArtifactManager) Create(ctx context.Context, artifactRecord *mo
|
|||
repoName := mgr.getRepositoryName(artifactRecord.Vendor, artifactRecord.Repository)
|
||||
err = mgr.regCli.PushBlob(repoName, artifactRecord.Digest, artifactRecord.Size, reader)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating system artifact record for %s/%s/%s: %v", artifactRecord.Vendor, artifactRecord.Repository, artifactRecord.Digest, err)
|
||||
return err
|
||||
}
|
||||
artifactID = id
|
||||
|
|
|
@ -63,6 +63,24 @@ func (suite *ManagerTestSuite) TestCreate() {
|
|||
suite.regCli.AssertCalled(suite.T(), "PushBlob")
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestCreateTimeNotSet() {
|
||||
sa := model.SystemArtifact{
|
||||
Repository: "test_repo",
|
||||
Digest: "test_digest",
|
||||
Size: int64(100),
|
||||
Vendor: "test_vendor",
|
||||
Type: "test_type",
|
||||
}
|
||||
suite.dao.On("Create", mock.Anything, &sa, mock.Anything).Return(int64(1), nil).Once()
|
||||
suite.regCli.On("PushBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
reader := strings.NewReader("test data string")
|
||||
id, err := suite.mgr.Create(orm.NewContext(nil, &ormtesting.FakeOrmer{}), &sa, reader)
|
||||
suite.Equalf(int64(1), id, "Expected row to correctly inserted")
|
||||
suite.NoErrorf(err, "Unexpected error when creating artifact: %v", err)
|
||||
suite.regCli.AssertCalled(suite.T(), "PushBlob")
|
||||
suite.False(sa.CreateTime.IsZero(), "Create time expected to be set")
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestCreatePushBlobFails() {
|
||||
sa := model.SystemArtifact{
|
||||
Repository: "test_repo",
|
||||
|
@ -171,7 +189,7 @@ func (suite *ManagerTestSuite) TestDelete() {
|
|||
|
||||
suite.NoErrorf(err, "Unexpected error when deleting artifact: %v", err)
|
||||
suite.dao.AssertCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob", "sys_harb0r/test_vendor/test_repo", "test_digest")
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestDeleteSystemArtifactDeleteError() {
|
||||
|
@ -184,7 +202,7 @@ func (suite *ManagerTestSuite) TestDeleteSystemArtifactDeleteError() {
|
|||
|
||||
suite.Errorf(err, "Expected error when deleting artifact: %v", err)
|
||||
suite.dao.AssertCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob", "sys_harb0r/test_vendor/test_repo", "test_digest")
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestDeleteSystemArtifactBlobDeleteError() {
|
||||
|
@ -197,7 +215,7 @@ func (suite *ManagerTestSuite) TestDeleteSystemArtifactBlobDeleteError() {
|
|||
|
||||
suite.Errorf(err, "Expected error when deleting artifact: %v", err)
|
||||
suite.dao.AssertNotCalled(suite.T(), "Delete", mock.Anything, "test_vendor", "test_repo", "test_digest")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob")
|
||||
suite.regCli.AssertCalled(suite.T(), "DeleteBlob", "sys_harb0r/test_vendor/test_repo", "test_digest")
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestExist() {
|
||||
|
|
|
@ -65,6 +65,7 @@ func New() http.Handler {
|
|||
StatisticAPI: newStatisticAPI(),
|
||||
ProjectMetadataAPI: newProjectMetadaAPI(),
|
||||
PurgeAPI: newPurgeAPI(),
|
||||
ScanDataExportAPI: newScanDataExportAPI(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
237
src/server/v2.0/handler/scanexport.go
Normal file
237
src/server/v2.0/handler/scanexport.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/user"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/scandataexport"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/systemartifact"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/scan_data_export"
|
||||
)
|
||||
|
||||
func newScanDataExportAPI() *scanDataExportAPI {
|
||||
return &scanDataExportAPI{
|
||||
scanDataExportCtl: scandataexport.Ctl,
|
||||
sysArtifactMgr: systemartifact.Mgr,
|
||||
userMgr: user.Mgr,
|
||||
}
|
||||
}
|
||||
|
||||
type scanDataExportAPI struct {
|
||||
BaseAPI
|
||||
scanDataExportCtl scandataexport.Controller
|
||||
sysArtifactMgr systemartifact.Manager
|
||||
userMgr user.Manager
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) ExportScanData(ctx context.Context, params operation.ExportScanDataParams) middleware.Responder {
|
||||
if err := se.RequireAuthenticated(ctx); err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
// check if the MIME type for the export is the Generic vulnerability data
|
||||
if params.XScanDataType != v1.MimeTypeGenericVulnerabilityReport {
|
||||
error := &models.Error{Message: fmt.Sprintf("Unsupported MIME type : %s", params.XScanDataType)}
|
||||
errors := &models.Errors{Errors: []*models.Error{error}}
|
||||
return operation.NewExportScanDataBadRequest().WithPayload(errors)
|
||||
}
|
||||
|
||||
// loop through the list of projects and validate that scan privilege and create privilege
|
||||
// is available for all projects
|
||||
// TODO : Should we just ignore projects that do not have the required level of access?
|
||||
|
||||
projects := params.Criteria.Projects
|
||||
for _, project := range projects {
|
||||
if err := se.RequireProjectAccess(ctx, project, rbac.ActionCreate, rbac.ResourceScan); err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
scanDataExportJob := new(models.ScanDataExportJob)
|
||||
|
||||
secContext, err := se.GetSecurityContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
// vendor id associated with the job == the user id
|
||||
usr, err := se.userMgr.GetByName(ctx, secContext.GetUsername())
|
||||
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
if usr == nil {
|
||||
error := &models.Error{Message: fmt.Sprintf("User : %s not found", secContext.GetUsername())}
|
||||
errors := &models.Errors{Errors: []*models.Error{error}}
|
||||
return operation.NewExportScanDataForbidden().WithPayload(errors)
|
||||
}
|
||||
|
||||
userContext := context.WithValue(ctx, export.CsvJobVendorIDKey, usr.UserID)
|
||||
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
jobID, err := se.scanDataExportCtl.Start(userContext, se.convertToCriteria(params.Criteria, secContext.GetUsername(), usr.UserID))
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
scanDataExportJob.ID = jobID
|
||||
return operation.NewExportScanDataOK().WithPayload(scanDataExportJob)
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) GetScanDataExportExecution(ctx context.Context, params operation.GetScanDataExportExecutionParams) middleware.Responder {
|
||||
err := se.RequireAuthenticated(ctx)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
execution, err := se.scanDataExportCtl.GetExecution(ctx, params.ExecutionID)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
sdeExec := models.ScanDataExportExecution{
|
||||
EndTime: strfmt.DateTime(execution.EndTime),
|
||||
ID: execution.ID,
|
||||
StartTime: strfmt.DateTime(execution.StartTime),
|
||||
Status: execution.Status,
|
||||
StatusText: execution.StatusMessage,
|
||||
Trigger: execution.Trigger,
|
||||
UserID: execution.UserID,
|
||||
JobName: execution.JobName,
|
||||
UserName: execution.UserName,
|
||||
FilePresent: execution.FilePresent,
|
||||
}
|
||||
|
||||
return operation.NewGetScanDataExportExecutionOK().WithPayload(&sdeExec)
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) DownloadScanData(ctx context.Context, params operation.DownloadScanDataParams) middleware.Responder {
|
||||
err := se.RequireAuthenticated(ctx)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
execution, err := se.scanDataExportCtl.GetExecution(ctx, params.ExecutionID)
|
||||
if err != nil {
|
||||
se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
// check if the execution being downloaded is owned by the current user
|
||||
secContext, err := se.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
|
||||
if secContext.GetUsername() != execution.UserName {
|
||||
return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
repositoryName := fmt.Sprintf("scandata_export_%v", params.ExecutionID)
|
||||
file, err := se.sysArtifactMgr.Read(ctx, strings.ToLower(export.Vendor), repositoryName, execution.ExportDataDigest)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
logger.Infof("reading data from file : %s", repositoryName)
|
||||
|
||||
return middleware.ResponderFunc(func(writer http.ResponseWriter, producer runtime.Producer) {
|
||||
defer se.cleanUpArtifact(ctx, repositoryName, execution.ExportDataDigest, params.ExecutionID, file)
|
||||
|
||||
writer.Header().Set("Content-Type", "text/csv")
|
||||
writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", fmt.Sprintf("%s.csv", repositoryName)))
|
||||
nbytes, err := io.Copy(writer, file)
|
||||
if err != nil {
|
||||
logger.Errorf("Encountered error while copying data: %v", err)
|
||||
} else {
|
||||
logger.Debugf("Copied %v bytes from file to client", nbytes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) GetScanDataExportExecutionList(ctx context.Context, params operation.GetScanDataExportExecutionListParams) middleware.Responder {
|
||||
err := se.RequireAuthenticated(ctx)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
executions, err := se.scanDataExportCtl.ListExecutions(ctx, params.UserName)
|
||||
if err != nil {
|
||||
return se.SendError(ctx, err)
|
||||
}
|
||||
execs := make([]*models.ScanDataExportExecution, 0)
|
||||
for _, execution := range executions {
|
||||
sdeExec := &models.ScanDataExportExecution{
|
||||
EndTime: strfmt.DateTime(execution.EndTime),
|
||||
ID: execution.ID,
|
||||
StartTime: strfmt.DateTime(execution.StartTime),
|
||||
Status: execution.Status,
|
||||
StatusText: execution.StatusMessage,
|
||||
Trigger: execution.Trigger,
|
||||
UserID: execution.UserID,
|
||||
UserName: execution.UserName,
|
||||
JobName: execution.JobName,
|
||||
FilePresent: execution.FilePresent,
|
||||
}
|
||||
execs = append(execs, sdeExec)
|
||||
}
|
||||
sdeExecList := models.ScanDataExportExecutionList{Items: execs}
|
||||
return operation.NewGetScanDataExportExecutionListOK().WithPayload(&sdeExecList)
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) convertToCriteria(requestCriteria *models.ScanDataExportRequest, userName string, userID int) export.Request {
|
||||
return export.Request{
|
||||
UserID: userID,
|
||||
UserName: userName,
|
||||
JobName: requestCriteria.JobName,
|
||||
CVEIds: requestCriteria.CVEIds,
|
||||
Labels: requestCriteria.Labels,
|
||||
Projects: requestCriteria.Projects,
|
||||
Repositories: requestCriteria.Repositories,
|
||||
Tags: requestCriteria.Tags,
|
||||
}
|
||||
}
|
||||
|
||||
func (se *scanDataExportAPI) cleanUpArtifact(ctx context.Context, repositoryName, digest string, execID int64, file io.ReadCloser) {
|
||||
file.Close()
|
||||
logger.Infof("Deleting report artifact : %v:%v", repositoryName, digest)
|
||||
|
||||
// the entire delete operation is executed within a transaction to ensure that any failures
|
||||
// during the blob creation or tracking record creation result in a rollback of the transaction
|
||||
vendor := strings.ToLower(export.Vendor)
|
||||
err := orm.WithTransaction(func(ctx context.Context) error {
|
||||
err := se.sysArtifactMgr.Delete(ctx, vendor, repositoryName, digest)
|
||||
if err != nil {
|
||||
log.Errorf("Error deleting system artifact record for %s/%s/%s: %v", vendor, repositoryName, digest, err)
|
||||
return err
|
||||
}
|
||||
// delete the underlying execution
|
||||
err = se.scanDataExportCtl.DeleteExecution(ctx, execID)
|
||||
if err != nil {
|
||||
log.Errorf("Error deleting csv export job execution for %s/%s/%s: %v", vendor, repositoryName, digest, err)
|
||||
}
|
||||
return err
|
||||
})(ctx)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error deleting system artifact record for %s/%s/%s: %v", vendor, repositoryName, digest, err)
|
||||
}
|
||||
}
|
396
src/server/v2.0/handler/scanexport_test.go
Normal file
396
src/server/v2.0/handler/scanexport_test.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
commonmodels "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||
"github.com/goharbor/harbor/src/testing/controller/scandataexport"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
systemartifacttesting "github.com/goharbor/harbor/src/testing/pkg/systemartifact"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/user"
|
||||
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
|
||||
testifymock "github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"net/http"
|
||||
url2 "net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ScanExportTestSuite struct {
|
||||
htesting.Suite
|
||||
scanExportCtl *scandataexport.Controller
|
||||
sysArtifactMgr *systemartifacttesting.Manager
|
||||
userMgr *user.Manager
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) SetupSuite() {
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) SetupTest() {
|
||||
|
||||
suite.scanExportCtl = &scandataexport.Controller{}
|
||||
suite.sysArtifactMgr = &systemartifacttesting.Manager{}
|
||||
suite.userMgr = &user.Manager{}
|
||||
suite.Config = &restapi.Config{
|
||||
ScanDataExportAPI: &scanDataExportAPI{
|
||||
scanDataExportCtl: suite.scanExportCtl,
|
||||
sysArtifactMgr: suite.sysArtifactMgr,
|
||||
userMgr: suite.userMgr,
|
||||
},
|
||||
}
|
||||
suite.Suite.SetupSuite()
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestAuthorization() {
|
||||
{
|
||||
criteria := models.ScanDataExportRequest{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
reqs := []struct {
|
||||
method string
|
||||
url string
|
||||
body interface{}
|
||||
headers map[string]string
|
||||
}{
|
||||
{http.MethodPost, "/export/cve", criteria, map[string]string{"X-Scan-Data-Type": v1.MimeTypeGenericVulnerabilityReport}},
|
||||
{http.MethodGet, "/export/cve/execution/100", nil, nil},
|
||||
{http.MethodGet, "/export/cve/download/100", nil, nil},
|
||||
}
|
||||
|
||||
suite.Security.On("IsAuthenticated").Return(false).Times(3)
|
||||
for _, req := range reqs {
|
||||
|
||||
if req.body != nil && req.method == http.MethodPost {
|
||||
data, _ := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
res, _ := suite.DoReq(req.method, req.url, buffer, req.headers)
|
||||
suite.Equal(401, res.StatusCode)
|
||||
} else {
|
||||
res, _ := suite.DoReq(req.method, req.url, nil)
|
||||
suite.Equal(401, res.StatusCode)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
func (suite *ScanExportTestSuite) TestExportScanData() {
|
||||
suite.Security.On("GetUsername").Return("test-user")
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
usr := commonmodels.User{UserID: 1000, Username: "test-user"}
|
||||
suite.userMgr.On("GetByName", mock.Anything, "test-user").Return(&usr, nil).Once()
|
||||
// user authenticated and correct headers sent
|
||||
{
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
url := "/export/cve"
|
||||
criteria := models.ScanDataExportRequest{
|
||||
JobName: "test-job",
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["X-Scan-Data-Type"] = v1.MimeTypeGenericVulnerabilityReport
|
||||
|
||||
// data, err := json.Marshal(criteria)
|
||||
mock.OnAnything(suite.scanExportCtl, "Start").Return(int64(100), nil).Once()
|
||||
res, err := suite.DoReq(http.MethodPost, url, buffer, headers)
|
||||
suite.Equal(200, res.StatusCode)
|
||||
|
||||
suite.Equal(nil, err)
|
||||
respData := make(map[string]interface{})
|
||||
json.NewDecoder(res.Body).Decode(&respData)
|
||||
suite.Equal(int64(100), int64(respData["id"].(float64)))
|
||||
|
||||
// validate job name and user name set in the request for job execution
|
||||
jobRequestMatcher := testifymock.MatchedBy(func(req export.Request) bool {
|
||||
return req.UserName == "test-user" && req.JobName == "test-job" && req.Tags == "{test-tag1, test-tag2}" && req.UserID == 1000
|
||||
})
|
||||
suite.scanExportCtl.AssertCalled(suite.T(), "Start", mock.Anything, jobRequestMatcher)
|
||||
}
|
||||
|
||||
// user authenticated but incorrect/unsupported header sent across
|
||||
{
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
url := "/export/cve"
|
||||
|
||||
criteria := models.ScanDataExportRequest{
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["X-Scan-Data-Type"] = "test"
|
||||
|
||||
mock.OnAnything(suite.scanExportCtl, "Start").Return(int64(100), nil).Once()
|
||||
res, err := suite.DoReq(http.MethodPost, url, buffer, headers)
|
||||
suite.Equal(400, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestExportScanDataGetUserIdError() {
|
||||
suite.Security.On("GetUsername").Return("test-user")
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
suite.userMgr.On("GetByName", mock.Anything, "test-user").Return(nil, errors.New("test error")).Once()
|
||||
// user authenticated and correct headers sent
|
||||
{
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
url := "/export/cve"
|
||||
criteria := models.ScanDataExportRequest{
|
||||
JobName: "test-job",
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["X-Scan-Data-Type"] = v1.MimeTypeGenericVulnerabilityReport
|
||||
|
||||
// data, err := json.Marshal(criteria)
|
||||
mock.OnAnything(suite.scanExportCtl, "Start").Return(int64(100), nil).Once()
|
||||
res, err := suite.DoReq(http.MethodPost, url, buffer, headers)
|
||||
suite.Equal(http.StatusInternalServerError, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
|
||||
suite.scanExportCtl.AssertNotCalled(suite.T(), "Start")
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestExportScanDataGetUserIdNotFound() {
|
||||
suite.Security.On("GetUsername").Return("test-user")
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
suite.userMgr.On("GetByName", mock.Anything, "test-user").Return(nil, nil).Once()
|
||||
// user authenticated and correct headers sent
|
||||
{
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
url := "/export/cve"
|
||||
criteria := models.ScanDataExportRequest{
|
||||
JobName: "test-job",
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["X-Scan-Data-Type"] = v1.MimeTypeGenericVulnerabilityReport
|
||||
|
||||
// data, err := json.Marshal(criteria)
|
||||
mock.OnAnything(suite.scanExportCtl, "Start").Return(int64(100), nil).Once()
|
||||
res, err := suite.DoReq(http.MethodPost, url, buffer, headers)
|
||||
suite.Equal(http.StatusForbidden, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
|
||||
suite.scanExportCtl.AssertNotCalled(suite.T(), "Start")
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestExportScanDataNoPrivileges() {
|
||||
suite.Security.On("IsAuthenticated").Return(true).Times(2)
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Once()
|
||||
url := "/export/cve"
|
||||
|
||||
criteria := models.ScanDataExportRequest{
|
||||
JobName: "test-job",
|
||||
CVEIds: "CVE-123",
|
||||
Labels: []int64{100},
|
||||
Projects: []int64{200},
|
||||
Repositories: "test-repo",
|
||||
Tags: "{test-tag1, test-tag2}",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(criteria)
|
||||
buffer := bytes.NewBuffer(data)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["X-Scan-Data-Type"] = v1.MimeTypeGenericVulnerabilityReport
|
||||
|
||||
mock.OnAnything(suite.scanExportCtl, "Start").Return(int64(100), nil).Once()
|
||||
res, err := suite.DoReq(http.MethodPost, url, buffer, headers)
|
||||
suite.Equal(http.StatusForbidden, res.StatusCode)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestGetScanDataExportExecution() {
|
||||
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
url := "/export/cve/execution/100"
|
||||
endTime := time.Now()
|
||||
startTime := endTime.Add(-10 * time.Minute)
|
||||
|
||||
execution := &export.Execution{
|
||||
ID: 100,
|
||||
UserID: 3,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Trigger: "MANUAL",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExportDataDigest: "datadigest",
|
||||
UserName: "test-user",
|
||||
JobName: "test-job",
|
||||
FilePresent: false,
|
||||
}
|
||||
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil).Once()
|
||||
res, err := suite.DoReq(http.MethodGet, url, nil)
|
||||
suite.Equal(200, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
respData := models.ScanDataExportExecution{}
|
||||
json.NewDecoder(res.Body).Decode(&respData)
|
||||
suite.Equal("test-user", respData.UserName)
|
||||
suite.Equal("test-job", respData.JobName)
|
||||
suite.Equal(false, respData.FilePresent)
|
||||
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestDownloadScanData() {
|
||||
suite.Security.On("GetUsername").Return("test-user")
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
|
||||
url := "/export/cve/download/100"
|
||||
endTime := time.Now()
|
||||
startTime := endTime.Add(-10 * time.Minute)
|
||||
|
||||
execution := &export.Execution{
|
||||
ID: int64(100),
|
||||
UserID: int64(3),
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Trigger: "MANUAL",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExportDataDigest: "datadigest",
|
||||
UserName: "test-user",
|
||||
}
|
||||
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil)
|
||||
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
|
||||
|
||||
// all BLOB related operations succeed
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil)
|
||||
|
||||
sampleData := "test,hello,world"
|
||||
data := io.NopCloser(strings.NewReader(sampleData))
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Read").Return(data, nil)
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
|
||||
|
||||
res, err := suite.DoReq(http.MethodGet, url, nil)
|
||||
suite.Equal(200, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
|
||||
// validate the content of the response
|
||||
var responseData bytes.Buffer
|
||||
if _, err := io.Copy(&responseData, res.Body); err == nil {
|
||||
suite.Equal(sampleData, responseData.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestDownloadScanDataUserNotOwnerofExport() {
|
||||
suite.Security.On("GetUsername").Return("test-user1")
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(1)
|
||||
url := "/export/cve/download/100"
|
||||
endTime := time.Now()
|
||||
startTime := endTime.Add(-10 * time.Minute)
|
||||
|
||||
execution := &export.Execution{
|
||||
ID: int64(100),
|
||||
UserID: int64(3),
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Trigger: "MANUAL",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExportDataDigest: "datadigest",
|
||||
UserName: "test-user",
|
||||
}
|
||||
mock.OnAnything(suite.scanExportCtl, "GetExecution").Return(execution, nil)
|
||||
mock.OnAnything(suite.scanExportCtl, "DeleteExecution").Return(nil)
|
||||
|
||||
// all BLOB related operations succeed
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Create").Return(int64(1), nil)
|
||||
|
||||
sampleData := "test,hello,world"
|
||||
data := io.NopCloser(strings.NewReader(sampleData))
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Read").Return(data, nil)
|
||||
mock.OnAnything(suite.sysArtifactMgr, "Delete").Return(nil)
|
||||
|
||||
res, err := suite.DoReq(http.MethodGet, url, nil)
|
||||
suite.Equal(http.StatusUnauthorized, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
}
|
||||
|
||||
func (suite *ScanExportTestSuite) TestGetScanDataExportExecutionList() {
|
||||
|
||||
suite.Security.On("IsAuthenticated").Return(true).Once()
|
||||
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
|
||||
url, err := url2.Parse("/export/cve/executions")
|
||||
params := url2.Values{}
|
||||
params.Add("user_name", "test-user")
|
||||
url.RawQuery = params.Encode()
|
||||
endTime := time.Now()
|
||||
startTime := endTime.Add(-10 * time.Minute)
|
||||
|
||||
execution := &export.Execution{
|
||||
ID: 100,
|
||||
UserID: 3,
|
||||
Status: "Success",
|
||||
StatusMessage: "",
|
||||
Trigger: "MANUAL",
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
ExportDataDigest: "datadigest",
|
||||
JobName: "test-job",
|
||||
UserName: "test-user",
|
||||
}
|
||||
fmt.Println("URL string : ", url.String())
|
||||
mock.OnAnything(suite.scanExportCtl, "ListExecutions").Return([]*export.Execution{execution}, nil).Once()
|
||||
res, err := suite.DoReq(http.MethodGet, url.String(), nil)
|
||||
suite.Equal(200, res.StatusCode)
|
||||
suite.Equal(nil, err)
|
||||
respData := models.ScanDataExportExecutionList{}
|
||||
json.NewDecoder(res.Body).Decode(&respData)
|
||||
suite.Equal(1, len(respData.Items))
|
||||
suite.Equal(int64(100), respData.Items[0].ID)
|
||||
}
|
||||
|
||||
func TestScanExportTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ScanExportTestSuite{})
|
||||
}
|
|
@ -31,3 +31,4 @@ package controller
|
|||
//go:generate mockery --case snake --dir ../../controller/purge --name Controller --output ./purge --outpkg purge
|
||||
//go:generate mockery --case snake --dir ../../controller/jobservice --name SchedulerController --output ./jobservice --outpkg jobservice
|
||||
//go:generate mockery --case snake --dir ../../controller/systemartifact --name Controller --output ./systemartifact --outpkg systemartifact
|
||||
//go:generate mockery --case snake --dir ../../controller/scandataexport --name Controller --output ./scandataexport --outpkg scandataexport
|
||||
|
|
136
src/testing/controller/scandataexport/controller.go
Normal file
136
src/testing/controller/scandataexport/controller.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package scandataexport
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
export "github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
task "github.com/goharbor/harbor/src/pkg/task"
|
||||
)
|
||||
|
||||
// Controller is an autogenerated mock type for the Controller type
|
||||
type Controller struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// DeleteExecution provides a mock function with given fields: ctx, executionID
|
||||
func (_m *Controller) DeleteExecution(ctx context.Context, executionID int64) error {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, executionID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetExecution provides a mock function with given fields: ctx, executionID
|
||||
func (_m *Controller) GetExecution(ctx context.Context, executionID int64) (*export.Execution, error) {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
var r0 *export.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *export.Execution); ok {
|
||||
r0 = rf(ctx, executionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*export.Execution)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, executionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTask provides a mock function with given fields: ctx, executionID
|
||||
func (_m *Controller) GetTask(ctx context.Context, executionID int64) (*task.Task, error) {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
var r0 *task.Task
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *task.Task); ok {
|
||||
r0 = rf(ctx, executionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*task.Task)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, executionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListExecutions provides a mock function with given fields: ctx, userName
|
||||
func (_m *Controller) ListExecutions(ctx context.Context, userName string) ([]*export.Execution, error) {
|
||||
ret := _m.Called(ctx, userName)
|
||||
|
||||
var r0 []*export.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []*export.Execution); ok {
|
||||
r0 = rf(ctx, userName)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*export.Execution)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, userName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Start provides a mock function with given fields: ctx, criteria
|
||||
func (_m *Controller) Start(ctx context.Context, criteria export.Request) (int64, error) {
|
||||
ret := _m.Called(ctx, criteria)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, export.Request) int64); ok {
|
||||
r0 = rf(ctx, criteria)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, export.Request) error); ok {
|
||||
r1 = rf(ctx, criteria)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type NewControllerT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewController creates a new instance of Controller. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewController(t NewControllerT) *Controller {
|
||||
mock := &Controller{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
|
@ -62,3 +62,7 @@ package pkg
|
|||
//go:generate mockery --case snake --dir ../../pkg/systemartifact/ --name Selector --output ./systemartifact/cleanup --outpkg cleanup
|
||||
//go:generate mockery --case snake --dir ../../pkg/systemartifact/dao --name DAO --output ./systemartifact/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/cached/manifest/redis --name CachedManager --output ./cached/manifest/redis --outpkg redis
|
||||
//go:generate mockery --case snake --dir ../../pkg/scan/export --name FilterProcessor --output ./scan/export --outpkg export
|
||||
//go:generate mockery --case snake --dir ../../pkg/scan/export --name Manager --output ./scan/export --outpkg export
|
||||
//go:generate mockery --case snake --dir ../../pkg/scan/export --name ArtifactDigestCalculator --output ./scan/export --outpkg export
|
||||
//go:generate mockery --case snake --dir ../../pkg/registry --name Client --output ./registry --outpkg registry --filename fake_registry_client.go
|
||||
|
|
|
@ -114,7 +114,7 @@ func (f *FakeClient) MountBlob(srcRepository, digest, dstRepository string) (err
|
|||
|
||||
// DeleteBlob ...
|
||||
func (f *FakeClient) DeleteBlob(repository, digest string) (err error) {
|
||||
args := f.Called()
|
||||
args := f.Called(repository, digest)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
|
|
325
src/testing/pkg/registry/fake_registry_client.go
Normal file
325
src/testing/pkg/registry/fake_registry_client.go
Normal file
|
@ -0,0 +1,325 @@
|
|||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
|
||||
distribution "github.com/docker/distribution"
|
||||
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Client is an autogenerated mock type for the Client type
|
||||
type Client struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// BlobExist provides a mock function with given fields: repository, digest
|
||||
func (_m *Client) BlobExist(repository string, digest string) (bool, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
|
||||
r0 = rf(repository, digest)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(repository, digest)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Catalog provides a mock function with given fields:
|
||||
func (_m *Client) Catalog() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Copy provides a mock function with given fields: srcRepository, srcReference, dstRepository, dstReference, override
|
||||
func (_m *Client) Copy(srcRepository string, srcReference string, dstRepository string, dstReference string, override bool) error {
|
||||
ret := _m.Called(srcRepository, srcReference, dstRepository, dstReference, override)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, bool) error); ok {
|
||||
r0 = rf(srcRepository, srcReference, dstRepository, dstReference, override)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteBlob provides a mock function with given fields: repository, digest
|
||||
func (_m *Client) DeleteBlob(repository string, digest string) error {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, digest)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteManifest provides a mock function with given fields: repository, reference
|
||||
func (_m *Client) DeleteManifest(repository string, reference string) error {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(repository, reference)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Do provides a mock function with given fields: req
|
||||
func (_m *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 *http.Response
|
||||
if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*http.Response)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*http.Request) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListTags provides a mock function with given fields: repository
|
||||
func (_m *Client) ListTags(repository string) ([]string, error) {
|
||||
ret := _m.Called(repository)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||
r0 = rf(repository)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(repository)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ManifestExist provides a mock function with given fields: repository, reference
|
||||
func (_m *Client) ManifestExist(repository string, reference string) (bool, *distribution.Descriptor, error) {
|
||||
ret := _m.Called(repository, reference)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
|
||||
r0 = rf(repository, reference)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 *distribution.Descriptor
|
||||
if rf, ok := ret.Get(1).(func(string, string) *distribution.Descriptor); ok {
|
||||
r1 = rf(repository, reference)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*distribution.Descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string, string) error); ok {
|
||||
r2 = rf(repository, reference)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// MountBlob provides a mock function with given fields: srcRepository, digest, dstRepository
|
||||
func (_m *Client) MountBlob(srcRepository string, digest string, dstRepository string) error {
|
||||
ret := _m.Called(srcRepository, digest, dstRepository)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(srcRepository, digest, dstRepository)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Ping provides a mock function with given fields:
|
||||
func (_m *Client) Ping() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PullBlob provides a mock function with given fields: repository, digest
|
||||
func (_m *Client) PullBlob(repository string, digest string) (int64, io.ReadCloser, error) {
|
||||
ret := _m.Called(repository, digest)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
|
||||
r0 = rf(repository, digest)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 io.ReadCloser
|
||||
if rf, ok := ret.Get(1).(func(string, string) io.ReadCloser); ok {
|
||||
r1 = rf(repository, digest)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(io.ReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string, string) error); ok {
|
||||
r2 = rf(repository, digest)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// PullManifest provides a mock function with given fields: repository, reference, acceptedMediaTypes
|
||||
func (_m *Client) PullManifest(repository string, reference string, acceptedMediaTypes ...string) (distribution.Manifest, string, error) {
|
||||
_va := make([]interface{}, len(acceptedMediaTypes))
|
||||
for _i := range acceptedMediaTypes {
|
||||
_va[_i] = acceptedMediaTypes[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, repository, reference)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 distribution.Manifest
|
||||
if rf, ok := ret.Get(0).(func(string, string, ...string) distribution.Manifest); ok {
|
||||
r0 = rf(repository, reference, acceptedMediaTypes...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(distribution.Manifest)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(1).(func(string, string, ...string) string); ok {
|
||||
r1 = rf(repository, reference, acceptedMediaTypes...)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string, string, ...string) error); ok {
|
||||
r2 = rf(repository, reference, acceptedMediaTypes...)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// PushBlob provides a mock function with given fields: repository, digest, size, blob
|
||||
func (_m *Client) PushBlob(repository string, digest string, size int64, blob io.Reader) error {
|
||||
ret := _m.Called(repository, digest, size, blob)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, int64, io.Reader) error); ok {
|
||||
r0 = rf(repository, digest, size, blob)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PushManifest provides a mock function with given fields: repository, reference, mediaType, payload
|
||||
func (_m *Client) PushManifest(repository string, reference string, mediaType string, payload []byte) (string, error) {
|
||||
ret := _m.Called(repository, reference, mediaType, payload)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, []byte) string); ok {
|
||||
r0 = rf(repository, reference, mediaType, payload)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, []byte) error); ok {
|
||||
r1 = rf(repository, reference, mediaType, payload)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type NewClientT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewClient(t NewClientT) *Client {
|
||||
mock := &Client{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
256
src/testing/pkg/scan/dao/scan/vulnerability_record_dao.go
Normal file
256
src/testing/pkg/scan/dao/scan/vulnerability_record_dao.go
Normal file
|
@ -0,0 +1,256 @@
|
|||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
scan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||
)
|
||||
|
||||
// VulnerabilityRecordDao is an autogenerated mock type for the VulnerabilityRecordDao type
|
||||
type VulnerabilityRecordDao struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, vr
|
||||
func (_m *VulnerabilityRecordDao) Create(ctx context.Context, vr *scan.VulnerabilityRecord) (int64, error) {
|
||||
ret := _m.Called(ctx, vr)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord) int64); ok {
|
||||
r0 = rf(ctx, vr)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *scan.VulnerabilityRecord) error); ok {
|
||||
r1 = rf(ctx, vr)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, vr
|
||||
func (_m *VulnerabilityRecordDao) Delete(ctx context.Context, vr *scan.VulnerabilityRecord) error {
|
||||
ret := _m.Called(ctx, vr)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord) error); ok {
|
||||
r0 = rf(ctx, vr)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteForDigests provides a mock function with given fields: ctx, digests
|
||||
func (_m *VulnerabilityRecordDao) DeleteForDigests(ctx context.Context, digests ...string) (int64, error) {
|
||||
_va := make([]interface{}, len(digests))
|
||||
for _i := range digests {
|
||||
_va[_i] = digests[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, ...string) int64); ok {
|
||||
r0 = rf(ctx, digests...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, ...string) error); ok {
|
||||
r1 = rf(ctx, digests...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteForReport provides a mock function with given fields: ctx, reportUUID
|
||||
func (_m *VulnerabilityRecordDao) DeleteForReport(ctx context.Context, reportUUID string) (int64, error) {
|
||||
ret := _m.Called(ctx, reportUUID)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
|
||||
r0 = rf(ctx, reportUUID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, reportUUID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteForScanner provides a mock function with given fields: ctx, registrationUUID
|
||||
func (_m *VulnerabilityRecordDao) DeleteForScanner(ctx context.Context, registrationUUID string) (int64, error) {
|
||||
ret := _m.Called(ctx, registrationUUID)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok {
|
||||
r0 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetForReport provides a mock function with given fields: ctx, reportUUID
|
||||
func (_m *VulnerabilityRecordDao) GetForReport(ctx context.Context, reportUUID string) ([]*scan.VulnerabilityRecord, error) {
|
||||
ret := _m.Called(ctx, reportUUID)
|
||||
|
||||
var r0 []*scan.VulnerabilityRecord
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []*scan.VulnerabilityRecord); ok {
|
||||
r0 = rf(ctx, reportUUID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, reportUUID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetForScanner provides a mock function with given fields: ctx, registrationUUID
|
||||
func (_m *VulnerabilityRecordDao) GetForScanner(ctx context.Context, registrationUUID string) ([]*scan.VulnerabilityRecord, error) {
|
||||
ret := _m.Called(ctx, registrationUUID)
|
||||
|
||||
var r0 []*scan.VulnerabilityRecord
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []*scan.VulnerabilityRecord); ok {
|
||||
r0 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetRecordIdsForScanner provides a mock function with given fields: ctx, registrationUUID
|
||||
func (_m *VulnerabilityRecordDao) GetRecordIdsForScanner(ctx context.Context, registrationUUID string) ([]int, error) {
|
||||
ret := _m.Called(ctx, registrationUUID)
|
||||
|
||||
var r0 []int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) []int); ok {
|
||||
r0 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]int)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, registrationUUID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// InsertForReport provides a mock function with given fields: ctx, reportUUID, vulnerabilityRecordIDs
|
||||
func (_m *VulnerabilityRecordDao) InsertForReport(ctx context.Context, reportUUID string, vulnerabilityRecordIDs ...int64) error {
|
||||
_va := make([]interface{}, len(vulnerabilityRecordIDs))
|
||||
for _i := range vulnerabilityRecordIDs {
|
||||
_va[_i] = vulnerabilityRecordIDs[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, reportUUID)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, ...int64) error); ok {
|
||||
r0 = rf(ctx, reportUUID, vulnerabilityRecordIDs...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *VulnerabilityRecordDao) List(ctx context.Context, query *q.Query) ([]*scan.VulnerabilityRecord, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*scan.VulnerabilityRecord
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*scan.VulnerabilityRecord); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, vr, cols
|
||||
func (_m *VulnerabilityRecordDao) Update(ctx context.Context, vr *scan.VulnerabilityRecord, cols ...string) error {
|
||||
_va := make([]interface{}, len(cols))
|
||||
for _i := range cols {
|
||||
_va[_i] = cols[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, vr)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *scan.VulnerabilityRecord, ...string) error); ok {
|
||||
r0 = rf(ctx, vr, cols...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
50
src/testing/pkg/scan/export/artifact_digest_calculator.go
Normal file
50
src/testing/pkg/scan/export/artifact_digest_calculator.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ArtifactDigestCalculator is an autogenerated mock type for the ArtifactDigestCalculator type
|
||||
type ArtifactDigestCalculator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Calculate provides a mock function with given fields: fileName
|
||||
func (_m *ArtifactDigestCalculator) Calculate(fileName string) (digest.Digest, error) {
|
||||
ret := _m.Called(fileName)
|
||||
|
||||
var r0 digest.Digest
|
||||
if rf, ok := ret.Get(0).(func(string) digest.Digest); ok {
|
||||
r0 = rf(fileName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(digest.Digest)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(fileName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type NewArtifactDigestCalculatorT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewArtifactDigestCalculator creates a new instance of ArtifactDigestCalculator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewArtifactDigestCalculator(t NewArtifactDigestCalculatorT) *ArtifactDigestCalculator {
|
||||
mock := &ArtifactDigestCalculator{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
100
src/testing/pkg/scan/export/filter_processor.go
Normal file
100
src/testing/pkg/scan/export/filter_processor.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
selector "github.com/goharbor/harbor/src/lib/selector"
|
||||
)
|
||||
|
||||
// FilterProcessor is an autogenerated mock type for the FilterProcessor type
|
||||
type FilterProcessor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ProcessProjectFilter provides a mock function with given fields: ctx, userName, projectsToFilter
|
||||
func (_m *FilterProcessor) ProcessProjectFilter(ctx context.Context, userName string, projectsToFilter []int64) ([]int64, error) {
|
||||
ret := _m.Called(ctx, userName, projectsToFilter)
|
||||
|
||||
var r0 []int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []int64) []int64); ok {
|
||||
r0 = rf(ctx, userName, projectsToFilter)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]int64)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, []int64) error); ok {
|
||||
r1 = rf(ctx, userName, projectsToFilter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProcessRepositoryFilter provides a mock function with given fields: ctx, filter, projectIds
|
||||
func (_m *FilterProcessor) ProcessRepositoryFilter(ctx context.Context, filter string, projectIds []int64) ([]*selector.Candidate, error) {
|
||||
ret := _m.Called(ctx, filter, projectIds)
|
||||
|
||||
var r0 []*selector.Candidate
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []int64) []*selector.Candidate); ok {
|
||||
r0 = rf(ctx, filter, projectIds)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*selector.Candidate)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, []int64) error); ok {
|
||||
r1 = rf(ctx, filter, projectIds)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProcessTagFilter provides a mock function with given fields: ctx, filter, repositoryIds
|
||||
func (_m *FilterProcessor) ProcessTagFilter(ctx context.Context, filter string, repositoryIds []int64) ([]*selector.Candidate, error) {
|
||||
ret := _m.Called(ctx, filter, repositoryIds)
|
||||
|
||||
var r0 []*selector.Candidate
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []int64) []*selector.Candidate); ok {
|
||||
r0 = rf(ctx, filter, repositoryIds)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*selector.Candidate)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, []int64) error); ok {
|
||||
r1 = rf(ctx, filter, repositoryIds)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type NewFilterProcessorT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewFilterProcessor creates a new instance of FilterProcessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewFilterProcessor(t NewFilterProcessorT) *FilterProcessor {
|
||||
mock := &FilterProcessor{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
53
src/testing/pkg/scan/export/manager.go
Normal file
53
src/testing/pkg/scan/export/manager.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Code generated by mockery v2.12.3. DO NOT EDIT.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
export "github.com/goharbor/harbor/src/pkg/scan/export"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Manager is an autogenerated mock type for the Manager type
|
||||
type Manager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Fetch provides a mock function with given fields: ctx, params
|
||||
func (_m *Manager) Fetch(ctx context.Context, params export.Params) ([]export.Data, error) {
|
||||
ret := _m.Called(ctx, params)
|
||||
|
||||
var r0 []export.Data
|
||||
if rf, ok := ret.Get(0).(func(context.Context, export.Params) []export.Data); ok {
|
||||
r0 = rf(ctx, params)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]export.Data)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, export.Params) error); ok {
|
||||
r1 = rf(ctx, params)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type NewManagerT interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewManager(t NewManagerT) *Manager {
|
||||
mock := &Manager{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
1
src/vendor/github.com/gocarina/gocsv/.gitignore
generated
vendored
Normal file
1
src/vendor/github.com/gocarina/gocsv/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.idea
|
4
src/vendor/github.com/gocarina/gocsv/.travis.yml
generated
vendored
Normal file
4
src/vendor/github.com/gocarina/gocsv/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: go
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
21
src/vendor/github.com/gocarina/gocsv/LICENSE
generated
vendored
Normal file
21
src/vendor/github.com/gocarina/gocsv/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jonathan Picques
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
168
src/vendor/github.com/gocarina/gocsv/README.md
generated
vendored
Normal file
168
src/vendor/github.com/gocarina/gocsv/README.md
generated
vendored
Normal file
|
@ -0,0 +1,168 @@
|
|||
Go CSV
|
||||
=====
|
||||
|
||||
The GoCSV package aims to provide easy serialization and deserialization functions to use CSV in Golang
|
||||
|
||||
API and techniques inspired from https://godoc.org/gopkg.in/mgo.v2
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/gocarina/gocsv?status.png)](https://godoc.org/github.com/gocarina/gocsv)
|
||||
[![Build Status](https://travis-ci.org/gocarina/gocsv.svg?branch=master)](https://travis-ci.org/gocarina/gocsv)
|
||||
|
||||
Installation
|
||||
=====
|
||||
|
||||
```go get -u github.com/gocarina/gocsv```
|
||||
|
||||
Full example
|
||||
=====
|
||||
|
||||
Consider the following CSV file
|
||||
|
||||
```csv
|
||||
|
||||
client_id,client_name,client_age
|
||||
1,Jose,42
|
||||
2,Daniel,26
|
||||
3,Vincent,32
|
||||
|
||||
```
|
||||
|
||||
Easy binding in Go!
|
||||
---
|
||||
|
||||
```go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
)
|
||||
|
||||
type Client struct { // Our example struct, you can use "-" to ignore a field
|
||||
Id string `csv:"client_id"`
|
||||
Name string `csv:"client_name"`
|
||||
Age string `csv:"client_age"`
|
||||
NotUsed string `csv:"-"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
clientsFile, err := os.OpenFile("clients.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer clientsFile.Close()
|
||||
|
||||
clients := []*Client{}
|
||||
|
||||
if err := gocsv.UnmarshalFile(clientsFile, &clients); err != nil { // Load clients from file
|
||||
panic(err)
|
||||
}
|
||||
for _, client := range clients {
|
||||
fmt.Println("Hello", client.Name)
|
||||
}
|
||||
|
||||
if _, err := clientsFile.Seek(0, 0); err != nil { // Go to the start of the file
|
||||
panic(err)
|
||||
}
|
||||
|
||||
clients = append(clients, &Client{Id: "12", Name: "John", Age: "21"}) // Add clients
|
||||
clients = append(clients, &Client{Id: "13", Name: "Fred"})
|
||||
clients = append(clients, &Client{Id: "14", Name: "James", Age: "32"})
|
||||
clients = append(clients, &Client{Id: "15", Name: "Danny"})
|
||||
csvContent, err := gocsv.MarshalString(&clients) // Get all clients as CSV string
|
||||
//err = gocsv.MarshalFile(&clients, clientsFile) // Use this to save the CSV back to the file
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(csvContent) // Display all clients as CSV string
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Customizable Converters
|
||||
---
|
||||
|
||||
```go
|
||||
|
||||
type DateTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// Convert the internal date as CSV string
|
||||
func (date *DateTime) MarshalCSV() (string, error) {
|
||||
return date.Time.Format("20060201"), nil
|
||||
}
|
||||
|
||||
// You could also use the standard Stringer interface
|
||||
func (date *DateTime) String() (string) {
|
||||
return date.String() // Redundant, just for example
|
||||
}
|
||||
|
||||
// Convert the CSV string as internal date
|
||||
func (date *DateTime) UnmarshalCSV(csv string) (err error) {
|
||||
date.Time, err = time.Parse("20060201", csv)
|
||||
return err
|
||||
}
|
||||
|
||||
type Client struct { // Our example struct with a custom type (DateTime)
|
||||
Id string `csv:"id"`
|
||||
Name string `csv:"name"`
|
||||
Employed DateTime `csv:"employed"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Customizable CSV Reader / Writer
|
||||
---
|
||||
|
||||
```go
|
||||
|
||||
func main() {
|
||||
...
|
||||
|
||||
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||
r := csv.NewReader(in)
|
||||
r.Comma = '|'
|
||||
return r // Allows use pipe as delimiter
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||
r := csv.NewReader(in)
|
||||
r.LazyQuotes = true
|
||||
r.Comma = '.'
|
||||
return r // Allows use dot as delimiter and use quotes in CSV
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
|
||||
//return csv.NewReader(in)
|
||||
return gocsv.LazyCSVReader(in) // Allows use of quotes in CSV
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
gocsv.UnmarshalFile(file, &clients)
|
||||
|
||||
...
|
||||
|
||||
gocsv.SetCSVWriter(func(out io.Writer) *SafeCSVWriter {
|
||||
writer := csv.NewWriter(out)
|
||||
writer.Comma = '|'
|
||||
return gocsv.NewSafeCSVWriter(writer)
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
gocsv.MarshalFile(&clients, file)
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
```
|
517
src/vendor/github.com/gocarina/gocsv/csv.go
generated
vendored
Normal file
517
src/vendor/github.com/gocarina/gocsv/csv.go
generated
vendored
Normal file
|
@ -0,0 +1,517 @@
|
|||
// Copyright 2014 Jonathan Picques. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license
|
||||
// The license can be found in the LICENSE file.
|
||||
|
||||
// The GoCSV package aims to provide easy CSV serialization and deserialization to the golang programming language
|
||||
|
||||
package gocsv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FailIfUnmatchedStructTags indicates whether it is considered an error when there is an unmatched
|
||||
// struct tag.
|
||||
var FailIfUnmatchedStructTags = false
|
||||
|
||||
// FailIfDoubleHeaderNames indicates whether it is considered an error when a header name is repeated
|
||||
// in the csv header.
|
||||
var FailIfDoubleHeaderNames = false
|
||||
|
||||
// ShouldAlignDuplicateHeadersWithStructFieldOrder indicates whether we should align duplicate CSV
|
||||
// headers per their alignment in the struct definition.
|
||||
var ShouldAlignDuplicateHeadersWithStructFieldOrder = false
|
||||
|
||||
// TagName defines key in the struct field's tag to scan
|
||||
var TagName = "csv"
|
||||
|
||||
// TagSeparator defines seperator string for multiple csv tags in struct fields
|
||||
var TagSeparator = ","
|
||||
|
||||
// Normalizer is a function that takes and returns a string. It is applied to
|
||||
// struct and header field values before they are compared. It can be used to alter
|
||||
// names for comparison. For instance, you could allow case insensitive matching
|
||||
// or convert '-' to '_'.
|
||||
type Normalizer func(string) string
|
||||
|
||||
type ErrorHandler func(*csv.ParseError) bool
|
||||
|
||||
// normalizeName function initially set to a nop Normalizer.
|
||||
var normalizeName = DefaultNameNormalizer()
|
||||
|
||||
// DefaultNameNormalizer is a nop Normalizer.
|
||||
func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } }
|
||||
|
||||
// SetHeaderNormalizer sets the normalizer used to normalize struct and header field names.
|
||||
func SetHeaderNormalizer(f Normalizer) {
|
||||
normalizeName = f
|
||||
// Need to clear the cache hen the header normalizer changes.
|
||||
structInfoCache = sync.Map{}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// CSVWriter used to format CSV
|
||||
|
||||
var selfCSVWriter = DefaultCSVWriter
|
||||
|
||||
// DefaultCSVWriter is the default SafeCSVWriter used to format CSV (cf. csv.NewWriter)
|
||||
func DefaultCSVWriter(out io.Writer) *SafeCSVWriter {
|
||||
writer := NewSafeCSVWriter(csv.NewWriter(out))
|
||||
|
||||
// As only one rune can be defined as a CSV separator, we are going to trim
|
||||
// the custom tag separator and use the first rune.
|
||||
if runes := []rune(strings.TrimSpace(TagSeparator)); len(runes) > 0 {
|
||||
writer.Comma = runes[0]
|
||||
}
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
// SetCSVWriter sets the SafeCSVWriter used to format CSV.
|
||||
func SetCSVWriter(csvWriter func(io.Writer) *SafeCSVWriter) {
|
||||
selfCSVWriter = csvWriter
|
||||
}
|
||||
|
||||
func getCSVWriter(out io.Writer) *SafeCSVWriter {
|
||||
return selfCSVWriter(out)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// CSVReader used to parse CSV
|
||||
|
||||
var selfCSVReader = DefaultCSVReader
|
||||
|
||||
// DefaultCSVReader is the default CSV reader used to parse CSV (cf. csv.NewReader)
|
||||
func DefaultCSVReader(in io.Reader) CSVReader {
|
||||
return csv.NewReader(in)
|
||||
}
|
||||
|
||||
// LazyCSVReader returns a lazy CSV reader, with LazyQuotes and TrimLeadingSpace.
|
||||
func LazyCSVReader(in io.Reader) CSVReader {
|
||||
csvReader := csv.NewReader(in)
|
||||
csvReader.LazyQuotes = true
|
||||
csvReader.TrimLeadingSpace = true
|
||||
return csvReader
|
||||
}
|
||||
|
||||
// SetCSVReader sets the CSV reader used to parse CSV.
|
||||
func SetCSVReader(csvReader func(io.Reader) CSVReader) {
|
||||
selfCSVReader = csvReader
|
||||
}
|
||||
|
||||
func getCSVReader(in io.Reader) CSVReader {
|
||||
return selfCSVReader(in)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Marshal functions
|
||||
|
||||
// MarshalFile saves the interface as CSV in the file.
|
||||
func MarshalFile(in interface{}, file *os.File) (err error) {
|
||||
return Marshal(in, file)
|
||||
}
|
||||
|
||||
// MarshalString returns the CSV string from the interface.
|
||||
func MarshalString(in interface{}) (out string, err error) {
|
||||
bufferString := bytes.NewBufferString(out)
|
||||
if err := Marshal(in, bufferString); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return bufferString.String(), nil
|
||||
}
|
||||
|
||||
// MarshalBytes returns the CSV bytes from the interface.
|
||||
func MarshalBytes(in interface{}) (out []byte, err error) {
|
||||
bufferString := bytes.NewBuffer(out)
|
||||
if err := Marshal(in, bufferString); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufferString.Bytes(), nil
|
||||
}
|
||||
|
||||
// Marshal returns the CSV in writer from the interface.
|
||||
func Marshal(in interface{}, out io.Writer) (err error) {
|
||||
writer := getCSVWriter(out)
|
||||
return writeTo(writer, in, false)
|
||||
}
|
||||
|
||||
// MarshalWithoutHeaders returns the CSV in writer from the interface.
|
||||
func MarshalWithoutHeaders(in interface{}, out io.Writer) (err error) {
|
||||
writer := getCSVWriter(out)
|
||||
return writeTo(writer, in, true)
|
||||
}
|
||||
|
||||
// MarshalChan returns the CSV read from the channel.
|
||||
func MarshalChan(c <-chan interface{}, out CSVWriter) error {
|
||||
return writeFromChan(out, c)
|
||||
}
|
||||
|
||||
// MarshalCSV returns the CSV in writer from the interface.
|
||||
func MarshalCSV(in interface{}, out CSVWriter) (err error) {
|
||||
return writeTo(out, in, false)
|
||||
}
|
||||
|
||||
// MarshalCSVWithoutHeaders returns the CSV in writer from the interface.
|
||||
func MarshalCSVWithoutHeaders(in interface{}, out CSVWriter) (err error) {
|
||||
return writeTo(out, in, true)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Unmarshal functions
|
||||
|
||||
// UnmarshalFile parses the CSV from the file in the interface.
|
||||
func UnmarshalFile(in *os.File, out interface{}) error {
|
||||
return Unmarshal(in, out)
|
||||
}
|
||||
|
||||
// UnmarshalFile parses the CSV from the file in the interface.
|
||||
func UnmarshalFileWithErrorHandler(in *os.File, errHandler ErrorHandler, out interface{}) error {
|
||||
return UnmarshalWithErrorHandler(in, errHandler, out)
|
||||
}
|
||||
|
||||
// UnmarshalString parses the CSV from the string in the interface.
|
||||
func UnmarshalString(in string, out interface{}) error {
|
||||
return Unmarshal(strings.NewReader(in), out)
|
||||
}
|
||||
|
||||
// UnmarshalBytes parses the CSV from the bytes in the interface.
|
||||
func UnmarshalBytes(in []byte, out interface{}) error {
|
||||
return Unmarshal(bytes.NewReader(in), out)
|
||||
}
|
||||
|
||||
// Unmarshal parses the CSV from the reader in the interface.
|
||||
func Unmarshal(in io.Reader, out interface{}) error {
|
||||
return readTo(newSimpleDecoderFromReader(in), out)
|
||||
}
|
||||
|
||||
// Unmarshal parses the CSV from the reader in the interface.
|
||||
func UnmarshalWithErrorHandler(in io.Reader, errHandle ErrorHandler, out interface{}) error {
|
||||
return readToWithErrorHandler(newSimpleDecoderFromReader(in), errHandle, out)
|
||||
}
|
||||
|
||||
// UnmarshalWithoutHeaders parses the CSV from the reader in the interface.
|
||||
func UnmarshalWithoutHeaders(in io.Reader, out interface{}) error {
|
||||
return readToWithoutHeaders(newSimpleDecoderFromReader(in), out)
|
||||
}
|
||||
|
||||
// UnmarshalCSVWithoutHeaders parses a headerless CSV with passed in CSV reader
|
||||
func UnmarshalCSVWithoutHeaders(in CSVReader, out interface{}) error {
|
||||
return readToWithoutHeaders(csvDecoder{in}, out)
|
||||
}
|
||||
|
||||
// UnmarshalDecoder parses the CSV from the decoder in the interface
|
||||
func UnmarshalDecoder(in Decoder, out interface{}) error {
|
||||
return readTo(in, out)
|
||||
}
|
||||
|
||||
// UnmarshalCSV parses the CSV from the reader in the interface.
|
||||
func UnmarshalCSV(in CSVReader, out interface{}) error {
|
||||
return readTo(csvDecoder{in}, out)
|
||||
}
|
||||
|
||||
// UnmarshalCSVToMap parses a CSV of 2 columns into a map.
|
||||
func UnmarshalCSVToMap(in CSVReader, out interface{}) error {
|
||||
decoder := NewSimpleDecoderFromCSVReader(in)
|
||||
header, err := decoder.getCSVRow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(header) != 2 {
|
||||
return fmt.Errorf("maps can only be created for csv of two columns")
|
||||
}
|
||||
outValue, outType := getConcreteReflectValueAndType(out)
|
||||
if outType.Kind() != reflect.Map {
|
||||
return fmt.Errorf("cannot use " + outType.String() + ", only map supported")
|
||||
}
|
||||
keyType := outType.Key()
|
||||
valueType := outType.Elem()
|
||||
outValue.Set(reflect.MakeMap(outType))
|
||||
for {
|
||||
key := reflect.New(keyType)
|
||||
value := reflect.New(valueType)
|
||||
line, err := decoder.getCSVRow()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setField(key, line[0], false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setField(value, line[1], false); err != nil {
|
||||
return err
|
||||
}
|
||||
outValue.SetMapIndex(key.Elem(), value.Elem())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalToChan parses the CSV from the reader and send each value in the chan c.
|
||||
// The channel must have a concrete type.
|
||||
func UnmarshalToChan(in io.Reader, c interface{}) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("goscv: channel is %v", c)
|
||||
}
|
||||
return readEach(newSimpleDecoderFromReader(in), c)
|
||||
}
|
||||
|
||||
// UnmarshalToChanWithoutHeaders parses the CSV from the reader and send each value in the chan c.
|
||||
// The channel must have a concrete type.
|
||||
func UnmarshalToChanWithoutHeaders(in io.Reader, c interface{}) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("goscv: channel is %v", c)
|
||||
}
|
||||
return readEachWithoutHeaders(newSimpleDecoderFromReader(in), c)
|
||||
}
|
||||
|
||||
// UnmarshalDecoderToChan parses the CSV from the decoder and send each value in the chan c.
|
||||
// The channel must have a concrete type.
|
||||
func UnmarshalDecoderToChan(in SimpleDecoder, c interface{}) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("goscv: channel is %v", c)
|
||||
}
|
||||
return readEach(in, c)
|
||||
}
|
||||
|
||||
// UnmarshalStringToChan parses the CSV from the string and send each value in the chan c.
|
||||
// The channel must have a concrete type.
|
||||
func UnmarshalStringToChan(in string, c interface{}) error {
|
||||
return UnmarshalToChan(strings.NewReader(in), c)
|
||||
}
|
||||
|
||||
// UnmarshalBytesToChan parses the CSV from the bytes and send each value in the chan c.
|
||||
// The channel must have a concrete type.
|
||||
func UnmarshalBytesToChan(in []byte, c interface{}) error {
|
||||
return UnmarshalToChan(bytes.NewReader(in), c)
|
||||
}
|
||||
|
||||
// UnmarshalToCallback parses the CSV from the reader and send each value to the given func f.
|
||||
// The func must look like func(Struct).
|
||||
func UnmarshalToCallback(in io.Reader, f interface{}) error {
|
||||
valueFunc := reflect.ValueOf(f)
|
||||
t := reflect.TypeOf(f)
|
||||
if t.NumIn() != 1 {
|
||||
return fmt.Errorf("the given function must have exactly one parameter")
|
||||
}
|
||||
cerr := make(chan error)
|
||||
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||
go func() {
|
||||
cerr <- UnmarshalToChan(in, c.Interface())
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case err := <-cerr:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
v, notClosed := c.Recv()
|
||||
if !notClosed || v.Interface() == nil {
|
||||
break
|
||||
}
|
||||
valueFunc.Call([]reflect.Value{v})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalDecoderToCallback parses the CSV from the decoder and send each value to the given func f.
|
||||
// The func must look like func(Struct).
|
||||
func UnmarshalDecoderToCallback(in SimpleDecoder, f interface{}) error {
|
||||
valueFunc := reflect.ValueOf(f)
|
||||
t := reflect.TypeOf(f)
|
||||
if t.NumIn() != 1 {
|
||||
return fmt.Errorf("the given function must have exactly one parameter")
|
||||
}
|
||||
cerr := make(chan error)
|
||||
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||
go func() {
|
||||
cerr <- UnmarshalDecoderToChan(in, c.Interface())
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case err := <-cerr:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
v, notClosed := c.Recv()
|
||||
if !notClosed || v.Interface() == nil {
|
||||
break
|
||||
}
|
||||
valueFunc.Call([]reflect.Value{v})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalBytesToCallback parses the CSV from the bytes and send each value to the given func f.
|
||||
// The func must look like func(Struct).
|
||||
func UnmarshalBytesToCallback(in []byte, f interface{}) error {
|
||||
return UnmarshalToCallback(bytes.NewReader(in), f)
|
||||
}
|
||||
|
||||
// UnmarshalStringToCallback parses the CSV from the string and send each value to the given func f.
|
||||
// The func must look like func(Struct).
|
||||
func UnmarshalStringToCallback(in string, c interface{}) (err error) {
|
||||
return UnmarshalToCallback(strings.NewReader(in), c)
|
||||
}
|
||||
|
||||
// UnmarshalToCallbackWithError parses the CSV from the reader and
|
||||
// send each value to the given func f.
|
||||
//
|
||||
// If func returns error, it will stop processing, drain the
|
||||
// parser and propagate the error to caller.
|
||||
//
|
||||
// The func must look like func(Struct) error.
|
||||
func UnmarshalToCallbackWithError(in io.Reader, f interface{}) error {
|
||||
valueFunc := reflect.ValueOf(f)
|
||||
t := reflect.TypeOf(f)
|
||||
if t.NumIn() != 1 {
|
||||
return fmt.Errorf("the given function must have exactly one parameter")
|
||||
}
|
||||
if t.NumOut() != 1 {
|
||||
return fmt.Errorf("the given function must have exactly one return value")
|
||||
}
|
||||
if !isErrorType(t.Out(0)) {
|
||||
return fmt.Errorf("the given function must only return error.")
|
||||
}
|
||||
|
||||
cerr := make(chan error)
|
||||
c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0)
|
||||
go func() {
|
||||
cerr <- UnmarshalToChan(in, c.Interface())
|
||||
}()
|
||||
|
||||
var fErr error
|
||||
for {
|
||||
select {
|
||||
case err := <-cerr:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fErr
|
||||
default:
|
||||
}
|
||||
v, notClosed := c.Recv()
|
||||
if !notClosed || v.Interface() == nil {
|
||||
if err := <- cerr; err != nil {
|
||||
fErr = err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// callback f has already returned an error, stop processing but keep draining the chan c
|
||||
if fErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
results := valueFunc.Call([]reflect.Value{v})
|
||||
|
||||
// If the callback f returns an error, stores it and returns it in future.
|
||||
errValue := results[0].Interface()
|
||||
if errValue != nil {
|
||||
fErr = errValue.(error)
|
||||
}
|
||||
}
|
||||
return fErr
|
||||
}
|
||||
|
||||
// UnmarshalBytesToCallbackWithError parses the CSV from the bytes and
|
||||
// send each value to the given func f.
|
||||
//
|
||||
// If func returns error, it will stop processing, drain the
|
||||
// parser and propagate the error to caller.
|
||||
//
|
||||
// The func must look like func(Struct) error.
|
||||
func UnmarshalBytesToCallbackWithError(in []byte, f interface{}) error {
|
||||
return UnmarshalToCallbackWithError(bytes.NewReader(in), f)
|
||||
}
|
||||
|
||||
// UnmarshalStringToCallbackWithError parses the CSV from the string and
|
||||
// send each value to the given func f.
|
||||
//
|
||||
// If func returns error, it will stop processing, drain the
|
||||
// parser and propagate the error to caller.
|
||||
//
|
||||
// The func must look like func(Struct) error.
|
||||
func UnmarshalStringToCallbackWithError(in string, c interface{}) (err error) {
|
||||
return UnmarshalToCallbackWithError(strings.NewReader(in), c)
|
||||
}
|
||||
|
||||
// CSVToMap creates a simple map from a CSV of 2 columns.
|
||||
func CSVToMap(in io.Reader) (map[string]string, error) {
|
||||
decoder := newSimpleDecoderFromReader(in)
|
||||
header, err := decoder.getCSVRow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(header) != 2 {
|
||||
return nil, fmt.Errorf("maps can only be created for csv of two columns")
|
||||
}
|
||||
m := make(map[string]string)
|
||||
for {
|
||||
line, err := decoder.getCSVRow()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[line[0]] = line[1]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CSVToMaps takes a reader and returns an array of dictionaries, using the header row as the keys
|
||||
func CSVToMaps(reader io.Reader) ([]map[string]string, error) {
|
||||
r := csv.NewReader(reader)
|
||||
rows := []map[string]string{}
|
||||
var header []string
|
||||
for {
|
||||
record, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if header == nil {
|
||||
header = record
|
||||
} else {
|
||||
dict := map[string]string{}
|
||||
for i := range header {
|
||||
dict[header[i]] = record[i]
|
||||
}
|
||||
rows = append(rows, dict)
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// CSVToChanMaps parses the CSV from the reader and send a dictionary in the chan c, using the header row as the keys.
|
||||
func CSVToChanMaps(reader io.Reader, c chan<- map[string]string) error {
|
||||
r := csv.NewReader(reader)
|
||||
var header []string
|
||||
for {
|
||||
record, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if header == nil {
|
||||
header = record
|
||||
} else {
|
||||
dict := map[string]string{}
|
||||
for i := range header {
|
||||
dict[header[i]] = record[i]
|
||||
}
|
||||
c <- dict
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
463
src/vendor/github.com/gocarina/gocsv/decode.go
generated
vendored
Normal file
463
src/vendor/github.com/gocarina/gocsv/decode.go
generated
vendored
Normal file
|
@ -0,0 +1,463 @@
|
|||
package gocsv
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Decoder .
|
||||
type Decoder interface {
|
||||
getCSVRows() ([][]string, error)
|
||||
}
|
||||
|
||||
// SimpleDecoder .
|
||||
type SimpleDecoder interface {
|
||||
getCSVRow() ([]string, error)
|
||||
getCSVRows() ([][]string, error)
|
||||
}
|
||||
|
||||
type CSVReader interface {
|
||||
Read() ([]string, error)
|
||||
ReadAll() ([][]string, error)
|
||||
}
|
||||
|
||||
type csvDecoder struct {
|
||||
CSVReader
|
||||
}
|
||||
|
||||
func newSimpleDecoderFromReader(r io.Reader) SimpleDecoder {
|
||||
return csvDecoder{getCSVReader(r)}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrEmptyCSVFile = errors.New("empty csv file given")
|
||||
ErrNoStructTags = errors.New("no csv struct tags found")
|
||||
)
|
||||
|
||||
// NewSimpleDecoderFromCSVReader creates a SimpleDecoder, which may be passed
|
||||
// to the UnmarshalDecoder* family of functions, from a CSV reader. Note that
|
||||
// encoding/csv.Reader implements CSVReader, so you can pass one of those
|
||||
// directly here.
|
||||
func NewSimpleDecoderFromCSVReader(r CSVReader) SimpleDecoder {
|
||||
return csvDecoder{r}
|
||||
}
|
||||
|
||||
func (c csvDecoder) getCSVRows() ([][]string, error) {
|
||||
return c.ReadAll()
|
||||
}
|
||||
|
||||
func (c csvDecoder) getCSVRow() ([]string, error) {
|
||||
return c.Read()
|
||||
}
|
||||
|
||||
func mismatchStructFields(structInfo []fieldInfo, headers []string) []string {
|
||||
missing := make([]string, 0)
|
||||
if len(structInfo) == 0 {
|
||||
return missing
|
||||
}
|
||||
|
||||
headerMap := make(map[string]struct{}, len(headers))
|
||||
for idx := range headers {
|
||||
headerMap[headers[idx]] = struct{}{}
|
||||
}
|
||||
|
||||
for _, info := range structInfo {
|
||||
found := false
|
||||
for _, key := range info.keys {
|
||||
if _, ok := headerMap[key]; ok {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
missing = append(missing, info.keys...)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func mismatchHeaderFields(structInfo []fieldInfo, headers []string) []string {
|
||||
missing := make([]string, 0)
|
||||
if len(headers) == 0 {
|
||||
return missing
|
||||
}
|
||||
|
||||
keyMap := make(map[string]struct{})
|
||||
for _, info := range structInfo {
|
||||
for _, key := range info.keys {
|
||||
keyMap[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
if _, ok := keyMap[header]; !ok {
|
||||
missing = append(missing, header)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func maybeMissingStructFields(structInfo []fieldInfo, headers []string) error {
|
||||
missing := mismatchStructFields(structInfo, headers)
|
||||
if len(missing) != 0 {
|
||||
return fmt.Errorf("found unmatched struct field with tags %v", missing)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check that no header name is repeated twice
|
||||
func maybeDoubleHeaderNames(headers []string) error {
|
||||
headerMap := make(map[string]bool, len(headers))
|
||||
for _, v := range headers {
|
||||
if _, ok := headerMap[v]; ok {
|
||||
return fmt.Errorf("repeated header name: %v", v)
|
||||
}
|
||||
headerMap[v] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply normalizer func to headers
|
||||
func normalizeHeaders(headers []string) []string {
|
||||
out := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
out[i] = normalizeName(h)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func readTo(decoder Decoder, out interface{}) error {
|
||||
return readToWithErrorHandler(decoder, nil, out)
|
||||
}
|
||||
|
||||
func readToWithErrorHandler(decoder Decoder, errHandler ErrorHandler, out interface{}) error {
|
||||
outValue, outType := getConcreteReflectValueAndType(out) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||
if err := ensureOutType(outType); err != nil {
|
||||
return err
|
||||
}
|
||||
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||
return err
|
||||
}
|
||||
csvRows, err := decoder.getCSVRows() // Get the CSV csvRows
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(csvRows) == 0 {
|
||||
return ErrEmptyCSVFile
|
||||
}
|
||||
if err := ensureOutCapacity(&outValue, len(csvRows)); err != nil { // Ensure the container is big enough to hold the CSV content
|
||||
return err
|
||||
}
|
||||
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||
if len(outInnerStructInfo.Fields) == 0 {
|
||||
return ErrNoStructTags
|
||||
}
|
||||
|
||||
headers := normalizeHeaders(csvRows[0])
|
||||
body := csvRows[1:]
|
||||
|
||||
csvHeadersLabels := make(map[int]*fieldInfo, len(outInnerStructInfo.Fields)) // Used to store the correspondance header <-> position in CSV
|
||||
|
||||
headerCount := map[string]int{}
|
||||
for i, csvColumnHeader := range headers {
|
||||
curHeaderCount := headerCount[csvColumnHeader]
|
||||
if fieldInfo := getCSVFieldPosition(csvColumnHeader, outInnerStructInfo, curHeaderCount); fieldInfo != nil {
|
||||
csvHeadersLabels[i] = fieldInfo
|
||||
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||
curHeaderCount++
|
||||
headerCount[csvColumnHeader] = curHeaderCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if FailIfUnmatchedStructTags {
|
||||
if err := maybeMissingStructFields(outInnerStructInfo.Fields, headers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if FailIfDoubleHeaderNames {
|
||||
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var withFieldsOK bool
|
||||
var fieldTypeUnmarshallerWithKeys TypeUnmarshalCSVWithFields
|
||||
|
||||
for i, csvRow := range body {
|
||||
objectIface := reflect.New(outValue.Index(i).Type()).Interface()
|
||||
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||
for j, csvColumnContent := range csvRow {
|
||||
if fieldInfo, ok := csvHeadersLabels[j]; ok { // Position found accordingly to header name
|
||||
|
||||
if outInner.CanInterface() {
|
||||
fieldTypeUnmarshallerWithKeys, withFieldsOK = objectIface.(TypeUnmarshalCSVWithFields)
|
||||
if withFieldsOK {
|
||||
if err := fieldTypeUnmarshallerWithKeys.UnmarshalCSVWithFields(fieldInfo.getFirstKey(), csvColumnContent); err != nil {
|
||||
parseError := csv.ParseError{
|
||||
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||
Column: j + 1,
|
||||
Err: err,
|
||||
}
|
||||
return &parseError
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
value := csvColumnContent
|
||||
if value == "" {
|
||||
value = fieldInfo.defaultValue
|
||||
}
|
||||
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, value, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||
parseError := csv.ParseError{
|
||||
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||
Column: j + 1,
|
||||
Err: err,
|
||||
}
|
||||
if errHandler == nil || !errHandler(&parseError) {
|
||||
return &parseError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if withFieldsOK {
|
||||
reflectedObject := reflect.ValueOf(objectIface)
|
||||
outInner = reflectedObject.Elem()
|
||||
}
|
||||
|
||||
outValue.Index(i).Set(outInner)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readEach(decoder SimpleDecoder, c interface{}) error {
|
||||
outValue, outType := getConcreteReflectValueAndType(c) // Get the concrete type (not pointer)
|
||||
if outType.Kind() != reflect.Chan {
|
||||
return fmt.Errorf("cannot use %v with type %s, only channel supported", c, outType)
|
||||
}
|
||||
defer outValue.Close()
|
||||
|
||||
headers, err := decoder.getCSVRow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers = normalizeHeaders(headers)
|
||||
|
||||
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||
return err
|
||||
}
|
||||
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||
if len(outInnerStructInfo.Fields) == 0 {
|
||||
return ErrNoStructTags
|
||||
}
|
||||
csvHeadersLabels := make(map[int]*fieldInfo, len(outInnerStructInfo.Fields)) // Used to store the correspondance header <-> position in CSV
|
||||
headerCount := map[string]int{}
|
||||
for i, csvColumnHeader := range headers {
|
||||
curHeaderCount := headerCount[csvColumnHeader]
|
||||
if fieldInfo := getCSVFieldPosition(csvColumnHeader, outInnerStructInfo, curHeaderCount); fieldInfo != nil {
|
||||
csvHeadersLabels[i] = fieldInfo
|
||||
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||
curHeaderCount++
|
||||
headerCount[csvColumnHeader] = curHeaderCount
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := maybeMissingStructFields(outInnerStructInfo.Fields, headers); err != nil {
|
||||
if FailIfUnmatchedStructTags {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if FailIfDoubleHeaderNames {
|
||||
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
for {
|
||||
line, err := decoder.getCSVRow()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||
for j, csvColumnContent := range line {
|
||||
if fieldInfo, ok := csvHeadersLabels[j]; ok { // Position found accordingly to header name
|
||||
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||
return &csv.ParseError{
|
||||
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||
Column: j + 1,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outValue.Send(outInner)
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readEachWithoutHeaders(decoder SimpleDecoder, c interface{}) error {
|
||||
outValue, outType := getConcreteReflectValueAndType(c) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||
if err := ensureOutType(outType); err != nil {
|
||||
return err
|
||||
}
|
||||
defer outValue.Close()
|
||||
|
||||
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||
return err
|
||||
}
|
||||
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||
if len(outInnerStructInfo.Fields) == 0 {
|
||||
return ErrNoStructTags
|
||||
}
|
||||
|
||||
i := 0
|
||||
for {
|
||||
line, err := decoder.getCSVRow()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||
for j, csvColumnContent := range line {
|
||||
fieldInfo := outInnerStructInfo.Fields[j]
|
||||
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||
return &csv.ParseError{
|
||||
Line: i + 2, //add 2 to account for the header & 0-indexing of arrays
|
||||
Column: j + 1,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
outValue.Send(outInner)
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readToWithoutHeaders(decoder Decoder, out interface{}) error {
|
||||
outValue, outType := getConcreteReflectValueAndType(out) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||
if err := ensureOutType(outType); err != nil {
|
||||
return err
|
||||
}
|
||||
outInnerWasPointer, outInnerType := getConcreteContainerInnerType(outType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||
if err := ensureOutInnerType(outInnerType); err != nil {
|
||||
return err
|
||||
}
|
||||
csvRows, err := decoder.getCSVRows() // Get the CSV csvRows
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(csvRows) == 0 {
|
||||
return ErrEmptyCSVFile
|
||||
}
|
||||
if err := ensureOutCapacity(&outValue, len(csvRows)+1); err != nil { // Ensure the container is big enough to hold the CSV content
|
||||
return err
|
||||
}
|
||||
outInnerStructInfo := getStructInfo(outInnerType) // Get the inner struct info to get CSV annotations
|
||||
if len(outInnerStructInfo.Fields) == 0 {
|
||||
return ErrNoStructTags
|
||||
}
|
||||
|
||||
for i, csvRow := range csvRows {
|
||||
outInner := createNewOutInner(outInnerWasPointer, outInnerType)
|
||||
for j, csvColumnContent := range csvRow {
|
||||
fieldInfo := outInnerStructInfo.Fields[j]
|
||||
if err := setInnerField(&outInner, outInnerWasPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||
return &csv.ParseError{
|
||||
Line: i + 1,
|
||||
Column: j + 1,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
outValue.Index(i).Set(outInner)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the outType is an array or a slice
|
||||
func ensureOutType(outType reflect.Type) error {
|
||||
switch outType.Kind() {
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Chan:
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot use " + outType.String() + ", only slice or array supported")
|
||||
}
|
||||
|
||||
// Check if the outInnerType is of type struct
|
||||
func ensureOutInnerType(outInnerType reflect.Type) error {
|
||||
switch outInnerType.Kind() {
|
||||
case reflect.Struct:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot use " + outInnerType.String() + ", only struct supported")
|
||||
}
|
||||
|
||||
func ensureOutCapacity(out *reflect.Value, csvLen int) error {
|
||||
switch out.Kind() {
|
||||
case reflect.Array:
|
||||
if out.Len() < csvLen-1 { // Array is not big enough to hold the CSV content (arrays are not addressable)
|
||||
return fmt.Errorf("array capacity problem: cannot store %d %s in %s", csvLen-1, out.Type().Elem().String(), out.Type().String())
|
||||
}
|
||||
case reflect.Slice:
|
||||
if !out.CanAddr() && out.Len() < csvLen-1 { // Slice is not big enough tho hold the CSV content and is not addressable
|
||||
return fmt.Errorf("slice capacity problem and is not addressable (did you forget &?)")
|
||||
} else if out.CanAddr() && out.Len() < csvLen-1 {
|
||||
out.Set(reflect.MakeSlice(out.Type(), csvLen-1, csvLen-1)) // Slice is not big enough, so grows it
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCSVFieldPosition(key string, structInfo *structInfo, curHeaderCount int) *fieldInfo {
|
||||
matchedFieldCount := 0
|
||||
for _, field := range structInfo.Fields {
|
||||
if field.matchesKey(key) {
|
||||
if matchedFieldCount >= curHeaderCount {
|
||||
return &field
|
||||
}
|
||||
matchedFieldCount++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNewOutInner(outInnerWasPointer bool, outInnerType reflect.Type) reflect.Value {
|
||||
if outInnerWasPointer {
|
||||
return reflect.New(outInnerType)
|
||||
}
|
||||
return reflect.New(outInnerType).Elem()
|
||||
}
|
||||
|
||||
func setInnerField(outInner *reflect.Value, outInnerWasPointer bool, index []int, value string, omitEmpty bool) error {
|
||||
oi := *outInner
|
||||
if outInnerWasPointer {
|
||||
// initialize nil pointer
|
||||
if oi.IsNil() {
|
||||
setField(oi, "", omitEmpty)
|
||||
}
|
||||
oi = outInner.Elem()
|
||||
}
|
||||
// because pointers can be nil need to recurse one index at a time and perform nil check
|
||||
if len(index) > 1 {
|
||||
nextField := oi.Field(index[0])
|
||||
return setInnerField(&nextField, nextField.Kind() == reflect.Ptr, index[1:], value, omitEmpty)
|
||||
}
|
||||
return setField(oi.FieldByIndex(index), value, omitEmpty)
|
||||
}
|
147
src/vendor/github.com/gocarina/gocsv/encode.go
generated
vendored
Normal file
147
src/vendor/github.com/gocarina/gocsv/encode.go
generated
vendored
Normal file
|
@ -0,0 +1,147 @@
|
|||
package gocsv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func newEncoder(out io.Writer) *encoder {
|
||||
return &encoder{out}
|
||||
}
|
||||
|
||||
func writeFromChan(writer CSVWriter, c <-chan interface{}) error {
|
||||
// Get the first value. It wil determine the header structure.
|
||||
firstValue, ok := <-c
|
||||
if !ok {
|
||||
return fmt.Errorf("channel is closed")
|
||||
}
|
||||
inValue, inType := getConcreteReflectValueAndType(firstValue) // Get the concrete type
|
||||
if err := ensureStructOrPtr(inType); err != nil {
|
||||
return err
|
||||
}
|
||||
inInnerWasPointer := inType.Kind() == reflect.Ptr
|
||||
inInnerStructInfo := getStructInfo(inType) // Get the inner struct info to get CSV annotations
|
||||
csvHeadersLabels := make([]string, len(inInnerStructInfo.Fields))
|
||||
for i, fieldInfo := range inInnerStructInfo.Fields { // Used to write the header (first line) in CSV
|
||||
csvHeadersLabels[i] = fieldInfo.getFirstKey()
|
||||
}
|
||||
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
write := func(val reflect.Value) error {
|
||||
for j, fieldInfo := range inInnerStructInfo.Fields {
|
||||
csvHeadersLabels[j] = ""
|
||||
inInnerFieldValue, err := getInnerField(val, inInnerWasPointer, fieldInfo.IndexChain) // Get the correct field header <-> position
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
csvHeadersLabels[j] = inInnerFieldValue
|
||||
}
|
||||
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := write(inValue); err != nil {
|
||||
return err
|
||||
}
|
||||
for v := range c {
|
||||
val, _ := getConcreteReflectValueAndType(v) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||
if err := ensureStructOrPtr(inType); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := write(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writer.Flush()
|
||||
return writer.Error()
|
||||
}
|
||||
|
||||
func writeTo(writer CSVWriter, in interface{}, omitHeaders bool) error {
|
||||
inValue, inType := getConcreteReflectValueAndType(in) // Get the concrete type (not pointer) (Slice<?> or Array<?>)
|
||||
if err := ensureInType(inType); err != nil {
|
||||
return err
|
||||
}
|
||||
inInnerWasPointer, inInnerType := getConcreteContainerInnerType(inType) // Get the concrete inner type (not pointer) (Container<"?">)
|
||||
if err := ensureInInnerType(inInnerType); err != nil {
|
||||
return err
|
||||
}
|
||||
inInnerStructInfo := getStructInfo(inInnerType) // Get the inner struct info to get CSV annotations
|
||||
csvHeadersLabels := make([]string, len(inInnerStructInfo.Fields))
|
||||
for i, fieldInfo := range inInnerStructInfo.Fields { // Used to write the header (first line) in CSV
|
||||
csvHeadersLabels[i] = fieldInfo.getFirstKey()
|
||||
}
|
||||
if !omitHeaders {
|
||||
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
inLen := inValue.Len()
|
||||
for i := 0; i < inLen; i++ { // Iterate over container rows
|
||||
for j, fieldInfo := range inInnerStructInfo.Fields {
|
||||
csvHeadersLabels[j] = ""
|
||||
inInnerFieldValue, err := getInnerField(inValue.Index(i), inInnerWasPointer, fieldInfo.IndexChain) // Get the correct field header <-> position
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
csvHeadersLabels[j] = inInnerFieldValue
|
||||
}
|
||||
if err := writer.Write(csvHeadersLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writer.Flush()
|
||||
return writer.Error()
|
||||
}
|
||||
|
||||
func ensureStructOrPtr(t reflect.Type) error {
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
fallthrough
|
||||
case reflect.Ptr:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot use " + t.String() + ", only slice or array supported")
|
||||
}
|
||||
|
||||
// Check if the inType is an array or a slice
|
||||
func ensureInType(outType reflect.Type) error {
|
||||
switch outType.Kind() {
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot use " + outType.String() + ", only slice or array supported")
|
||||
}
|
||||
|
||||
// Check if the inInnerType is of type struct
|
||||
func ensureInInnerType(outInnerType reflect.Type) error {
|
||||
switch outInnerType.Kind() {
|
||||
case reflect.Struct:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot use " + outInnerType.String() + ", only struct supported")
|
||||
}
|
||||
|
||||
func getInnerField(outInner reflect.Value, outInnerWasPointer bool, index []int) (string, error) {
|
||||
oi := outInner
|
||||
if outInnerWasPointer {
|
||||
if oi.IsNil() {
|
||||
return "", nil
|
||||
}
|
||||
oi = outInner.Elem()
|
||||
}
|
||||
// because pointers can be nil need to recurse one index at a time and perform nil check
|
||||
if len(index) > 1 {
|
||||
nextField := oi.Field(index[0])
|
||||
return getInnerField(nextField, nextField.Kind() == reflect.Ptr, index[1:])
|
||||
}
|
||||
return getFieldAsString(oi.FieldByIndex(index))
|
||||
}
|
3
src/vendor/github.com/gocarina/gocsv/go.mod
generated
vendored
Normal file
3
src/vendor/github.com/gocarina/gocsv/go.mod
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
module github.com/gocarina/gocsv
|
||||
|
||||
go 1.13
|
143
src/vendor/github.com/gocarina/gocsv/reflect.go
generated
vendored
Normal file
143
src/vendor/github.com/gocarina/gocsv/reflect.go
generated
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
package gocsv
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Reflection helpers
|
||||
|
||||
type structInfo struct {
|
||||
Fields []fieldInfo
|
||||
}
|
||||
|
||||
// fieldInfo is a struct field that should be mapped to a CSV column, or vice-versa
|
||||
// Each IndexChain element before the last is the index of an the embedded struct field
|
||||
// that defines Key as a tag
|
||||
type fieldInfo struct {
|
||||
keys []string
|
||||
omitEmpty bool
|
||||
IndexChain []int
|
||||
defaultValue string
|
||||
}
|
||||
|
||||
func (f fieldInfo) getFirstKey() string {
|
||||
return f.keys[0]
|
||||
}
|
||||
|
||||
func (f fieldInfo) matchesKey(key string) bool {
|
||||
for _, k := range f.keys {
|
||||
if key == k || strings.TrimSpace(key) == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var structInfoCache sync.Map
|
||||
var structMap = make(map[reflect.Type]*structInfo)
|
||||
var structMapMutex sync.RWMutex
|
||||
|
||||
func getStructInfo(rType reflect.Type) *structInfo {
|
||||
stInfo, ok := structInfoCache.Load(rType)
|
||||
if ok {
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
fieldsList := getFieldInfos(rType, []int{})
|
||||
stInfo = &structInfo{fieldsList}
|
||||
structInfoCache.Store(rType, stInfo)
|
||||
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
func getFieldInfos(rType reflect.Type, parentIndexChain []int) []fieldInfo {
|
||||
fieldsCount := rType.NumField()
|
||||
fieldsList := make([]fieldInfo, 0, fieldsCount)
|
||||
for i := 0; i < fieldsCount; i++ {
|
||||
field := rType.Field(i)
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var cpy = make([]int, len(parentIndexChain))
|
||||
copy(cpy, parentIndexChain)
|
||||
indexChain := append(cpy, i)
|
||||
|
||||
// if the field is a pointer to a struct, follow the pointer then create fieldinfo for each field
|
||||
if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
|
||||
// unless it implements marshalText or marshalCSV. Structs that implement this
|
||||
// should result in one value and not have their fields exposed
|
||||
if !(canMarshal(field.Type.Elem())) {
|
||||
fieldsList = append(fieldsList, getFieldInfos(field.Type.Elem(), indexChain)...)
|
||||
}
|
||||
}
|
||||
// if the field is a struct, create a fieldInfo for each of its fields
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
// unless it implements marshalText or marshalCSV. Structs that implement this
|
||||
// should result in one value and not have their fields exposed
|
||||
if !(canMarshal(field.Type)) {
|
||||
fieldsList = append(fieldsList, getFieldInfos(field.Type, indexChain)...)
|
||||
}
|
||||
}
|
||||
|
||||
// if the field is an embedded struct, ignore the csv tag
|
||||
if field.Anonymous {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldInfo := fieldInfo{IndexChain: indexChain}
|
||||
fieldTag := field.Tag.Get(TagName)
|
||||
fieldTags := strings.Split(fieldTag, TagSeparator)
|
||||
filteredTags := []string{}
|
||||
for _, fieldTagEntry := range fieldTags {
|
||||
if fieldTagEntry == "omitempty" {
|
||||
fieldInfo.omitEmpty = true
|
||||
} else if strings.HasPrefix(fieldTagEntry, "default=") {
|
||||
fieldInfo.defaultValue = strings.TrimPrefix(fieldTagEntry, "default=")
|
||||
} else {
|
||||
filteredTags = append(filteredTags, normalizeName(fieldTagEntry))
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredTags) == 1 && filteredTags[0] == "-" {
|
||||
continue
|
||||
} else if len(filteredTags) > 0 && filteredTags[0] != "" {
|
||||
fieldInfo.keys = filteredTags
|
||||
} else {
|
||||
fieldInfo.keys = []string{normalizeName(field.Name)}
|
||||
}
|
||||
fieldsList = append(fieldsList, fieldInfo)
|
||||
}
|
||||
return fieldsList
|
||||
}
|
||||
|
||||
func getConcreteContainerInnerType(in reflect.Type) (inInnerWasPointer bool, inInnerType reflect.Type) {
|
||||
inInnerType = in.Elem()
|
||||
inInnerWasPointer = false
|
||||
if inInnerType.Kind() == reflect.Ptr {
|
||||
inInnerWasPointer = true
|
||||
inInnerType = inInnerType.Elem()
|
||||
}
|
||||
return inInnerWasPointer, inInnerType
|
||||
}
|
||||
|
||||
func getConcreteReflectValueAndType(in interface{}) (reflect.Value, reflect.Type) {
|
||||
value := reflect.ValueOf(in)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
return value, value.Type()
|
||||
}
|
||||
|
||||
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
func isErrorType(outType reflect.Type) bool {
|
||||
if outType.Kind() != reflect.Interface {
|
||||
return false
|
||||
}
|
||||
|
||||
return outType.Implements(errorInterface)
|
||||
}
|
38
src/vendor/github.com/gocarina/gocsv/safe_csv.go
generated
vendored
Normal file
38
src/vendor/github.com/gocarina/gocsv/safe_csv.go
generated
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
package gocsv
|
||||
|
||||
//Wraps around SafeCSVWriter and makes it thread safe.
|
||||
import (
|
||||
"encoding/csv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type CSVWriter interface {
|
||||
Write(row []string) error
|
||||
Flush()
|
||||
Error() error
|
||||
}
|
||||
|
||||
type SafeCSVWriter struct {
|
||||
*csv.Writer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func NewSafeCSVWriter(original *csv.Writer) *SafeCSVWriter {
|
||||
return &SafeCSVWriter{
|
||||
Writer: original,
|
||||
}
|
||||
}
|
||||
|
||||
//Override write
|
||||
func (w *SafeCSVWriter) Write(row []string) error {
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
return w.Writer.Write(row)
|
||||
}
|
||||
|
||||
//Override flush
|
||||
func (w *SafeCSVWriter) Flush() {
|
||||
w.m.Lock()
|
||||
w.Writer.Flush()
|
||||
w.m.Unlock()
|
||||
}
|
456
src/vendor/github.com/gocarina/gocsv/types.go
generated
vendored
Normal file
456
src/vendor/github.com/gocarina/gocsv/types.go
generated
vendored
Normal file
|
@ -0,0 +1,456 @@
|
|||
package gocsv
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Conversion interfaces
|
||||
|
||||
// TypeMarshaller is implemented by any value that has a MarshalCSV method
|
||||
// This converter is used to convert the value to it string representation
|
||||
type TypeMarshaller interface {
|
||||
MarshalCSV() (string, error)
|
||||
}
|
||||
|
||||
// TypeUnmarshaller is implemented by any value that has an UnmarshalCSV method
|
||||
// This converter is used to convert a string to your value representation of that string
|
||||
type TypeUnmarshaller interface {
|
||||
UnmarshalCSV(string) error
|
||||
}
|
||||
|
||||
// TypeUnmarshalCSVWithFields can be implemented on whole structs to allow for whole structures to customized internal vs one off fields
|
||||
type TypeUnmarshalCSVWithFields interface {
|
||||
UnmarshalCSVWithFields(key, value string) error
|
||||
}
|
||||
|
||||
// NoUnmarshalFuncError is the custom error type to be raised in case there is no unmarshal function defined on type
|
||||
type NoUnmarshalFuncError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e NoUnmarshalFuncError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// NoMarshalFuncError is the custom error type to be raised in case there is no marshal function defined on type
|
||||
type NoMarshalFuncError struct {
|
||||
ty reflect.Type
|
||||
}
|
||||
|
||||
func (e NoMarshalFuncError) Error() string {
|
||||
return "No known conversion from " + e.ty.String() + " to string, " + e.ty.String() + " does not implement TypeMarshaller nor Stringer"
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Conversion helpers
|
||||
|
||||
func toString(in interface{}) (string, error) {
|
||||
inValue := reflect.ValueOf(in)
|
||||
|
||||
switch inValue.Kind() {
|
||||
case reflect.String:
|
||||
return inValue.String(), nil
|
||||
case reflect.Bool:
|
||||
b := inValue.Bool()
|
||||
if b {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return fmt.Sprintf("%v", inValue.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return fmt.Sprintf("%v", inValue.Uint()), nil
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(inValue.Float(), byte('f'), -1, 32), nil
|
||||
case reflect.Float64:
|
||||
return strconv.FormatFloat(inValue.Float(), byte('f'), -1, 64), nil
|
||||
}
|
||||
return "", fmt.Errorf("No known conversion from " + inValue.Type().String() + " to string")
|
||||
}
|
||||
|
||||
func toBool(in interface{}) (bool, error) {
|
||||
inValue := reflect.ValueOf(in)
|
||||
|
||||
switch inValue.Kind() {
|
||||
case reflect.String:
|
||||
s := inValue.String()
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.EqualFold(s, "yes") {
|
||||
return true, nil
|
||||
} else if strings.EqualFold(s, "no") || s == "" {
|
||||
return false, nil
|
||||
} else {
|
||||
return strconv.ParseBool(s)
|
||||
}
|
||||
case reflect.Bool:
|
||||
return inValue.Bool(), nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i := inValue.Int()
|
||||
if i != 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
i := inValue.Uint()
|
||||
if i != 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := inValue.Float()
|
||||
if f != 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to bool")
|
||||
}
|
||||
|
||||
func toInt(in interface{}) (int64, error) {
|
||||
inValue := reflect.ValueOf(in)
|
||||
|
||||
switch inValue.Kind() {
|
||||
case reflect.String:
|
||||
s := strings.TrimSpace(inValue.String())
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
out := strings.SplitN(s, ".", 2)
|
||||
return strconv.ParseInt(out[0], 0, 64)
|
||||
case reflect.Bool:
|
||||
if inValue.Bool() {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return inValue.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return int64(inValue.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return int64(inValue.Float()), nil
|
||||
}
|
||||
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to int")
|
||||
}
|
||||
|
||||
func toUint(in interface{}) (uint64, error) {
|
||||
inValue := reflect.ValueOf(in)
|
||||
|
||||
switch inValue.Kind() {
|
||||
case reflect.String:
|
||||
s := strings.TrimSpace(inValue.String())
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// support the float input
|
||||
if strings.Contains(s, ".") {
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
return strconv.ParseUint(s, 0, 64)
|
||||
case reflect.Bool:
|
||||
if inValue.Bool() {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return uint64(inValue.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return inValue.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return uint64(inValue.Float()), nil
|
||||
}
|
||||
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to uint")
|
||||
}
|
||||
|
||||
func toFloat(in interface{}) (float64, error) {
|
||||
inValue := reflect.ValueOf(in)
|
||||
|
||||
switch inValue.Kind() {
|
||||
case reflect.String:
|
||||
s := strings.TrimSpace(inValue.String())
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
return strconv.ParseFloat(s, 64)
|
||||
case reflect.Bool:
|
||||
if inValue.Bool() {
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return float64(inValue.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return float64(inValue.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return inValue.Float(), nil
|
||||
}
|
||||
return 0, fmt.Errorf("No known conversion from " + inValue.Type().String() + " to float")
|
||||
}
|
||||
|
||||
func setField(field reflect.Value, value string, omitEmpty bool) error {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if omitEmpty && value == "" {
|
||||
return nil
|
||||
}
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
|
||||
switch field.Interface().(type) {
|
||||
case string:
|
||||
s, err := toString(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetString(s)
|
||||
case bool:
|
||||
b, err := toBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetBool(b)
|
||||
case int, int8, int16, int32, int64:
|
||||
i, err := toInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetInt(i)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
ui, err := toUint(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetUint(ui)
|
||||
case float32, float64:
|
||||
f, err := toFloat(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetFloat(f)
|
||||
default:
|
||||
// Not a native type, check for unmarshal method
|
||||
if err := unmarshall(field, value); err != nil {
|
||||
if _, ok := err.(NoUnmarshalFuncError); !ok {
|
||||
return err
|
||||
}
|
||||
// Could not unmarshal, check for kind, e.g. renamed type from basic type
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
s, err := toString(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetString(s)
|
||||
case reflect.Bool:
|
||||
b, err := toBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetBool(b)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i, err := toInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetInt(i)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
ui, err := toUint(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetUint(ui)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f, err := toFloat(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetFloat(f)
|
||||
case reflect.Slice, reflect.Struct:
|
||||
err := json.Unmarshal([]byte(value), field.Addr().Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFieldAsString(field reflect.Value) (str string, err error) {
|
||||
switch field.Kind() {
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
if field.IsNil() {
|
||||
return "", nil
|
||||
}
|
||||
return getFieldAsString(field.Elem())
|
||||
default:
|
||||
// Check if field is go native type
|
||||
switch field.Interface().(type) {
|
||||
case string:
|
||||
return field.String(), nil
|
||||
case bool:
|
||||
if field.Bool() {
|
||||
return "true", nil
|
||||
} else {
|
||||
return "false", nil
|
||||
}
|
||||
case int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%v", field.Int()), nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprintf("%v", field.Uint()), nil
|
||||
case float32:
|
||||
str, err = toString(float32(field.Float()))
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
case float64:
|
||||
str, err = toString(field.Float())
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
default:
|
||||
// Not a native type, check for marshal method
|
||||
str, err = marshall(field)
|
||||
if err != nil {
|
||||
if _, ok := err.(NoMarshalFuncError); !ok {
|
||||
return str, err
|
||||
}
|
||||
// If not marshal method, is field compatible with/renamed from native type
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
return field.String(), nil
|
||||
case reflect.Bool:
|
||||
str, err = toString(field.Bool())
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
str, err = toString(field.Int())
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
str, err = toString(field.Uint())
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
case reflect.Float32:
|
||||
str, err = toString(float32(field.Float()))
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
case reflect.Float64:
|
||||
str, err = toString(field.Float())
|
||||
if err != nil {
|
||||
return str, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return str, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Un/serializations helpers
|
||||
|
||||
func canMarshal(t reflect.Type) bool {
|
||||
// unless it implements marshalText or marshalCSV. Structs that implement this
|
||||
// should result in one value and not have their fields exposed
|
||||
_, canMarshalText := t.MethodByName("MarshalText")
|
||||
_, canMarshalCSV := t.MethodByName("MarshalCSV")
|
||||
return canMarshalCSV || canMarshalText
|
||||
}
|
||||
|
||||
func unmarshall(field reflect.Value, value string) error {
|
||||
dupField := field
|
||||
unMarshallIt := func(finalField reflect.Value) error {
|
||||
if finalField.CanInterface() {
|
||||
fieldIface := finalField.Interface()
|
||||
|
||||
fieldTypeUnmarshaller, ok := fieldIface.(TypeUnmarshaller)
|
||||
if ok {
|
||||
return fieldTypeUnmarshaller.UnmarshalCSV(value)
|
||||
}
|
||||
|
||||
// Otherwise try to use TextUnmarshaler
|
||||
fieldTextUnmarshaler, ok := fieldIface.(encoding.TextUnmarshaler)
|
||||
if ok {
|
||||
return fieldTextUnmarshaler.UnmarshalText([]byte(value))
|
||||
}
|
||||
}
|
||||
|
||||
return NoUnmarshalFuncError{"No known conversion from string to " + field.Type().String() + ", " + field.Type().String() + " does not implement TypeUnmarshaller"}
|
||||
}
|
||||
for dupField.Kind() == reflect.Interface || dupField.Kind() == reflect.Ptr {
|
||||
if dupField.IsNil() {
|
||||
dupField = reflect.New(field.Type().Elem())
|
||||
field.Set(dupField)
|
||||
return unMarshallIt(dupField)
|
||||
}
|
||||
dupField = dupField.Elem()
|
||||
}
|
||||
if dupField.CanAddr() {
|
||||
return unMarshallIt(dupField.Addr())
|
||||
}
|
||||
return NoUnmarshalFuncError{"No known conversion from string to " + field.Type().String() + ", " + field.Type().String() + " does not implement TypeUnmarshaller"}
|
||||
}
|
||||
|
||||
func marshall(field reflect.Value) (value string, err error) {
|
||||
dupField := field
|
||||
marshallIt := func(finalField reflect.Value) (string, error) {
|
||||
if finalField.CanInterface() {
|
||||
fieldIface := finalField.Interface()
|
||||
|
||||
// Use TypeMarshaller when possible
|
||||
fieldTypeMarhaller, ok := fieldIface.(TypeMarshaller)
|
||||
if ok {
|
||||
return fieldTypeMarhaller.MarshalCSV()
|
||||
}
|
||||
|
||||
// Otherwise try to use TextMarshaller
|
||||
fieldTextMarshaler, ok := fieldIface.(encoding.TextMarshaler)
|
||||
if ok {
|
||||
text, err := fieldTextMarshaler.MarshalText()
|
||||
return string(text), err
|
||||
}
|
||||
|
||||
// Otherwise try to use Stringer
|
||||
fieldStringer, ok := fieldIface.(fmt.Stringer)
|
||||
if ok {
|
||||
return fieldStringer.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return value, NoMarshalFuncError{field.Type()}
|
||||
}
|
||||
for dupField.Kind() == reflect.Interface || dupField.Kind() == reflect.Ptr {
|
||||
if dupField.IsNil() {
|
||||
return value, nil
|
||||
}
|
||||
dupField = dupField.Elem()
|
||||
}
|
||||
if dupField.CanAddr() {
|
||||
dupField = dupField.Addr()
|
||||
}
|
||||
return marshallIt(dupField)
|
||||
}
|
117
src/vendor/github.com/gocarina/gocsv/unmarshaller.go
generated
vendored
Normal file
117
src/vendor/github.com/gocarina/gocsv/unmarshaller.go
generated
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
package gocsv
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Unmarshaller is a CSV to struct unmarshaller.
|
||||
type Unmarshaller struct {
|
||||
reader *csv.Reader
|
||||
Headers []string
|
||||
fieldInfoMap []*fieldInfo
|
||||
MismatchedHeaders []string
|
||||
MismatchedStructFields []string
|
||||
outType reflect.Type
|
||||
}
|
||||
|
||||
// NewUnmarshaller creates an unmarshaller from a csv.Reader and a struct.
|
||||
func NewUnmarshaller(reader *csv.Reader, out interface{}) (*Unmarshaller, error) {
|
||||
headers, err := reader.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers = normalizeHeaders(headers)
|
||||
|
||||
um := &Unmarshaller{reader: reader, outType: reflect.TypeOf(out)}
|
||||
err = validate(um, out, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return um, nil
|
||||
}
|
||||
|
||||
// Read returns an interface{} whose runtime type is the same as the struct that
|
||||
// was used to create the Unmarshaller.
|
||||
func (um *Unmarshaller) Read() (interface{}, error) {
|
||||
row, err := um.reader.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return um.unmarshalRow(row, nil)
|
||||
}
|
||||
|
||||
// ReadUnmatched is same as Read(), but returns a map of the columns that didn't match a field in the struct
|
||||
func (um *Unmarshaller) ReadUnmatched() (interface{}, map[string]string, error) {
|
||||
row, err := um.reader.Read()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
unmatched := make(map[string]string)
|
||||
value, err := um.unmarshalRow(row, unmatched)
|
||||
return value, unmatched, err
|
||||
}
|
||||
|
||||
// validate ensures that a struct was used to create the Unmarshaller, and validates
|
||||
// CSV headers against the CSV tags in the struct.
|
||||
func validate(um *Unmarshaller, s interface{}, headers []string) error {
|
||||
concreteType := reflect.TypeOf(s)
|
||||
if concreteType.Kind() == reflect.Ptr {
|
||||
concreteType = concreteType.Elem()
|
||||
}
|
||||
if err := ensureOutInnerType(concreteType); err != nil {
|
||||
return err
|
||||
}
|
||||
structInfo := getStructInfo(concreteType) // Get struct info to get CSV annotations.
|
||||
if len(structInfo.Fields) == 0 {
|
||||
return ErrNoStructTags
|
||||
}
|
||||
csvHeadersLabels := make([]*fieldInfo, len(headers)) // Used to store the corresponding header <-> position in CSV
|
||||
headerCount := map[string]int{}
|
||||
for i, csvColumnHeader := range headers {
|
||||
curHeaderCount := headerCount[csvColumnHeader]
|
||||
if fieldInfo := getCSVFieldPosition(csvColumnHeader, structInfo, curHeaderCount); fieldInfo != nil {
|
||||
csvHeadersLabels[i] = fieldInfo
|
||||
if ShouldAlignDuplicateHeadersWithStructFieldOrder {
|
||||
curHeaderCount++
|
||||
headerCount[csvColumnHeader] = curHeaderCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if FailIfDoubleHeaderNames {
|
||||
if err := maybeDoubleHeaderNames(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
um.Headers = headers
|
||||
um.fieldInfoMap = csvHeadersLabels
|
||||
um.MismatchedHeaders = mismatchHeaderFields(structInfo.Fields, headers)
|
||||
um.MismatchedStructFields = mismatchStructFields(structInfo.Fields, headers)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalRow converts a CSV row to a struct, based on CSV struct tags.
|
||||
// If unmatched is non nil, it is populated with any columns that don't map to a struct field
|
||||
func (um *Unmarshaller) unmarshalRow(row []string, unmatched map[string]string) (interface{}, error) {
|
||||
isPointer := false
|
||||
concreteOutType := um.outType
|
||||
if um.outType.Kind() == reflect.Ptr {
|
||||
isPointer = true
|
||||
concreteOutType = concreteOutType.Elem()
|
||||
}
|
||||
outValue := createNewOutInner(isPointer, concreteOutType)
|
||||
for j, csvColumnContent := range row {
|
||||
if j < len(um.fieldInfoMap) && um.fieldInfoMap[j] != nil {
|
||||
fieldInfo := um.fieldInfoMap[j]
|
||||
if err := setInnerField(&outValue, isPointer, fieldInfo.IndexChain, csvColumnContent, fieldInfo.omitEmpty); err != nil { // Set field of struct
|
||||
return nil, fmt.Errorf("cannot assign field at %v to %s through index chain %v: %v", j, outValue.Type(), fieldInfo.IndexChain, err)
|
||||
}
|
||||
} else if unmatched != nil {
|
||||
unmatched[um.Headers[j]] = csvColumnContent
|
||||
}
|
||||
}
|
||||
return outValue.Interface(), nil
|
||||
}
|
3
src/vendor/modules.txt
vendored
3
src/vendor/modules.txt
vendored
|
@ -399,6 +399,9 @@ github.com/go-sql-driver/mysql
|
|||
# github.com/go-stack/stack v1.8.0
|
||||
## explicit
|
||||
github.com/go-stack/stack
|
||||
# github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
|
||||
## explicit
|
||||
github.com/gocarina/gocsv
|
||||
# github.com/gocraft/work v0.5.1
|
||||
## explicit
|
||||
github.com/gocraft/work
|
||||
|
|
Loading…
Reference in New Issue
Block a user