diff --git a/api/admin/storage.go b/api/admin/storage.go new file mode 100644 index 000000000..8120863b6 --- /dev/null +++ b/api/admin/storage.go @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: Apache-2.0 + +package admin + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/types" + "github.com/go-vela/server/storage" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" + "net/http" +) + +// swagger:operation POST /api/v1/admin/storage/bucket admin CreateBucket + +// +// Create a new bucket +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: The bucket name to be created +// required: true +// schema: +// type: object +// properties: +// bucketName: +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the bucket +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" + +// CreateBucket represents the API handler to create a new bucket. +func CreateBucket(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: creating bucket") + + // capture body from API request + input := new(types.Bucket) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for bucket %s: %w", input.BucketName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + l.Debugf("bucket name: %s", input.BucketName) + err = storage.FromGinContext(c).CreateBucket(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to create bucket: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.Status(http.StatusCreated) +} + +// swagger:operation DELETE /api/v1/admin/storage/bucket admin DeleteBucket +// +// Delete a bucket +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: The bucket name to be deleted +// required: true +// schema: +// type: object +// properties: +// bucketName: +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the bucket +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" + +// DeleteBucket represents the API handler to delete a bucket. +func DeleteBucket(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: deleting bucket") + + // capture body from API request + input := new(types.Bucket) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for bucket %s: %w", input.BucketName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + err = storage.FromGinContext(c).DeleteBucket(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to delete bucket: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.Status(http.StatusOK) +} + +// swagger:operation GET /api/v1/admin/storage/bucket/lifecycle admin GetBucketLifecycle +// +// # Get bucket lifecycle configuration +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: bucketName +// description: The name of the bucket +// required: true +// type: string +// +// security: +// - ApiKeyAuth: [] +// +// responses: +// +// '200': +// description: Successfully retrieved the bucket lifecycle configuration +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" +func GetBucketLifecycle(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: getting bucket lifecycle configuration") + + // capture query parameters from API request + bucketName := c.Query("bucketName") + + if bucketName == "" { + retErr := fmt.Errorf("bucketName is required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + + input := &types.Bucket{ + BucketName: bucketName, + } + + lifecycleConfig, err := storage.FromGinContext(c).GetBucketLifecycle(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to get bucket lifecycle configuration: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.JSON(http.StatusOK, lifecycleConfig) +} + +// swagger:operation PUT /api/v1/admin/storage/bucket/lifecycle admin AdminSetBucketLifecycle +// +// Set bucket lifecycle configuration +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: The bucket lifecycle configuration +// required: true +// schema: +// type: object +// properties: +// bucketName: +// type: string +// lifecycleConfig: +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully set the bucket lifecycle configuration +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" + +// SetBucketLifecycle represents the API handler to set bucket lifecycle configuration. +func SetBucketLifecycle(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: setting bucket lifecycle configuration") + + // capture body from API request + input := new(types.Bucket) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for bucket %s: %w", input.BucketName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + err = storage.FromGinContext(c).SetBucketLifecycle(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to set bucket lifecycle configuration: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.Status(http.StatusOK) +} + +// swagger:operation GET /api/v1/admin/storage/bucket/download admin DownloadObject +// +// # Download an object from a bucket +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: bucketName +// description: The name of the bucket +// required: true +// type: string +// - in: query +// name: objectName +// description: The name of the object +// required: true +// type: string +// +// security: +// - ApiKeyAuth: [] +// +// responses: +// +// '200': +// description: Successfully downloaded the object +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" +// +// DownloadObject represents the API handler to download an object from a bucket. +func DownloadObject(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: downloading object") + + // capture body from API request + input := new(types.Object) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for object %s: %w", input.ObjectName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + if input.Bucket.BucketName == "" || input.ObjectName == "" { + retErr := fmt.Errorf("bucketName and objectName are required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + if input.FilePath == "" { + retErr := fmt.Errorf("file path is required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + err = storage.FromGinContext(c).Download(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to download object: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("File has been downloaded to %s", input.FilePath)}) +} + +// swagger:operation GET /api/v1/admin/storage/presign admin GetPresignedURL +// +// # Generate a presigned URL for an object +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: bucketName +// description: The name of the bucket +// required: true +// type: string +// - in: query +// name: objectName +// description: The name of the object +// required: true +// type: string +// +// security: +// - ApiKeyAuth: [] +// +// responses: +// +// '200': +// description: Successfully generated the presigned URL +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" +func GetPresignedURL(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: generating presigned URL") + + // capture query parameters from API request + bucketName := c.Query("bucketName") + objectName := c.Query("objectName") + + if bucketName == "" || objectName == "" { + retErr := fmt.Errorf("bucketName and objectName are required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + + input := &types.Object{ + Bucket: types.Bucket{BucketName: bucketName}, + ObjectName: objectName, + } + + url, err := storage.FromGinContext(c).PresignedGetObject(ctx, input) + if err != nil || url == "" { + retErr := fmt.Errorf("unable to generate presigned URL: %w", err) + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + + c.JSON(http.StatusOK, url) +} diff --git a/api/build/storage.go b/api/build/storage.go new file mode 100644 index 000000000..bd94b97d5 --- /dev/null +++ b/api/build/storage.go @@ -0,0 +1,87 @@ +package build + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/types" + "github.com/go-vela/server/storage" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" + "net/http" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/storage/upload builds UploadObject +// +// # Upload an object to a bucket +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: The object to be uploaded +// required: true +// schema: +// type: object +// properties: +// bucketName: +// type: string +// objectName: +// type: string +// objectData: +// type: string +// +// security: +// - ApiKeyAuth: [] +// +// responses: +// +// '201': +// description: Successfully uploaded the object +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" +// +// UploadObject represents the API handler to upload an object to a bucket. +func UploadObject(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: uploading object") + + // capture body from API request + input := new(types.Object) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for object %s: %w", input.ObjectName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + if input.Bucket.BucketName == "" || input.ObjectName == "" { + retErr := fmt.Errorf("bucketName and objectName are required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + if input.FilePath == "" { + retErr := fmt.Errorf("file path is required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + err = storage.FromGinContext(c).Upload(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to upload object: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.Status(http.StatusCreated) +} diff --git a/api/storage/doc.go b/api/storage/doc.go new file mode 100644 index 000000000..aae3e99dd --- /dev/null +++ b/api/storage/doc.go @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package storage provides the storage handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/storage" +package storage diff --git a/api/storage/storage.go b/api/storage/storage.go new file mode 100644 index 000000000..1981e92ea --- /dev/null +++ b/api/storage/storage.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 + +package storage + +import ( + "fmt" + "github.com/go-vela/server/storage" + "github.com/go-vela/server/util" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/go-vela/server/api/types" +) + +// swagger:operation POST /api/v1/storage/info storage Info +// +// Get storage credentials +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved storage credentials +// schema: +// "$ref": "#/definitions/StorageInfo" +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" + +// Info represents the API handler to +// retrieve storage credentials as part of worker onboarding. +func Info(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + + l.Info("requesting storage credentials with registration token") + + // extract the public key that was packed into gin context + k := c.MustGet("access-key").(string) + + // extract the storage-address that was packed into gin context + a := c.MustGet("storage-address").(string) + + // extract the secret key that was packed into gin context + s := c.MustGet("secret-key").(string) + + wr := types.StorageInfo{ + StorageAccessKey: &k, + StorageAddress: &a, + StorageSecretKey: &s, + } + + c.JSON(http.StatusOK, wr) +} + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/storage/upload builds UploadObject +// +// # Upload an object to a bucket +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: The object to be uploaded +// required: true +// schema: +// type: object +// properties: +// bucketName: +// type: string +// objectName: +// type: string +// objectData: +// type: string +// +// security: +// - ApiKeyAuth: [] +// +// responses: +// +// '201': +// description: Successfully uploaded the object +// '400': +// description: Invalid request payload +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unexpected server error +// schema: +// "$ref": "#/definitions/Error" +// +// UploadObject represents the API handler to upload an object to a bucket. +func UploadObject(c *gin.Context) { + l := c.MustGet("logger").(*logrus.Entry) + ctx := c.Request.Context() + + l.Debug("platform admin: uploading object") + + // capture body from API request + input := new(types.Object) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for object %s: %w", input.ObjectName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + if input.Bucket.BucketName == "" || input.ObjectName == "" { + retErr := fmt.Errorf("bucketName and objectName are required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + if input.FilePath == "" { + retErr := fmt.Errorf("file path is required") + util.HandleError(c, http.StatusBadRequest, retErr) + return + } + err = storage.FromGinContext(c).Upload(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to upload object: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + return + } + + c.Status(http.StatusCreated) +} diff --git a/api/types/pipeline.go b/api/types/pipeline.go index dd0ff617d..6fcb2744c 100644 --- a/api/types/pipeline.go +++ b/api/types/pipeline.go @@ -24,6 +24,7 @@ type Pipeline struct { Stages *bool `json:"stages,omitempty"` Steps *bool `json:"steps,omitempty"` Templates *bool `json:"templates,omitempty"` + TestReport *bool `json:"test_report,omitempty"` Warnings *[]string `json:"warnings,omitempty"` // swagger:strfmt base64 Data *[]byte `json:"data,omitempty"` @@ -237,6 +238,19 @@ func (p *Pipeline) GetData() []byte { return *p.Data } +// GetTestReport returns the TestReport results field. +// +// When the provided Pipeline type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *Pipeline) GetTestReport() bool { + // return zero value if Pipeline type or TestReport field is nil + if p == nil || p.TestReport == nil { + return false + } + + return *p.TestReport +} + // SetID sets the ID field. // // When the provided Pipeline type is nil, it @@ -419,6 +433,19 @@ func (p *Pipeline) SetTemplates(v bool) { p.Templates = &v } +// SetTestReport sets the TestReport field. +// +// When the provided Pipeline type is nil, it +// will set nothing and immediately return. +func (p *Pipeline) SetTestReport(v bool) { + // return if Pipeline type is nil + if p == nil { + return + } + + p.TestReport = &v +} + // SetWarnings sets the Warnings field. // // When the provided Pipeline type is nil, it @@ -461,6 +488,7 @@ func (p *Pipeline) String() string { Stages: %t, Steps: %t, Templates: %t, + TestReport: %t, Type: %s, Version: %s, Warnings: %v, @@ -478,6 +506,7 @@ func (p *Pipeline) String() string { p.GetStages(), p.GetSteps(), p.GetTemplates(), + p.GetTestReport(), p.GetType(), p.GetVersion(), p.GetWarnings(), diff --git a/api/types/storage.go b/api/types/storage.go new file mode 100644 index 000000000..70bc7ee9a --- /dev/null +++ b/api/types/storage.go @@ -0,0 +1,23 @@ +package types + +import "github.com/minio/minio-go/v7/pkg/lifecycle" + +// Bucket is the API types representation of an object storage. +// +// swagger:model CreateBucket +type Bucket struct { + BucketName string `json:"bucket_name,omitempty"` + Options BucketOptions `json:"options,omitempty"` + LifecycleConfig lifecycle.Configuration `json:"life_cycle_config,omitempty"` +} + +type BucketOptions struct { + Region string `json:"region,omitempty"` + ObjectLocking bool `json:"object_locking,omitempty"` +} + +type Object struct { + ObjectName string `json:"object_name,omitempty"` + Bucket Bucket `json:"bucket,omitempty"` + FilePath string `json:"file_path,omitempty"` +} diff --git a/api/types/storage_info.go b/api/types/storage_info.go new file mode 100644 index 000000000..94ab1650d --- /dev/null +++ b/api/types/storage_info.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +// StorageInfo is the API representation of a StorageInfo. +// +// swagger:model StorageInfo +type StorageInfo struct { + StorageAccessKey *string `json:"storage_access_key,omitempty"` + StorageSecretKey *string `json:"storage_secret_key,omitempty"` + StorageAddress *string `json:"storage_address,omitempty"` +} + +// GetAccessKey returns the StorageAccessKey field. +// +// When the provided StorageInfo type is nil, or the field within +// the type is nil, it returns an empty string for the field. +func (w *StorageInfo) GetAccessKey() string { + // return zero value if StorageInfo type or StorageAccessKey field is nil + if w == nil || w.StorageAccessKey == nil { + return "" + } + + return *w.StorageAccessKey +} + +// GetSecretKey returns the StorageSecretKey field. +// +// When the provided StorageInfo type is nil, or the field within +// the type is nil, it returns an empty string for the field. +func (w *StorageInfo) GetSecretKey() string { + // return zero value if StorageInfo type or StorageSecretKey field is nil + if w == nil || w.StorageSecretKey == nil { + return "" + } + + return *w.StorageSecretKey +} + +// GetStorageAddress returns the StorageAddress field. +// +// When the provided StorageInfo type is nil, or the field within +// the type is nil, it returns an empty string for the field. +func (w *StorageInfo) GetStorageAddress() string { + // return zero value if StorageInfo type or StorageAddress field is nil + if w == nil || w.StorageAddress == nil { + return "" + } + + return *w.StorageAddress +} + +// SetAccessKey sets the StorageAccessKey field. +// +// When the provided StorageInfo type is nil, it +// will set nothing and immediately return. +func (w *StorageInfo) SetAccessKey(v string) { + // return if StorageInfo type is nil + if w == nil { + return + } + + w.StorageAccessKey = &v +} + +// SetSecretKey sets the StorageSecretKey field. +// +// When the provided StorageInfo type is nil, it +// will set nothing and immediately return. +func (w *StorageInfo) SetSecretKey(v string) { + // return if StorageInfo type is nil + if w == nil { + return + } + + w.StorageSecretKey = &v +} + +// SetStorageAddress sets the StorageAddress field. +// +// When the provided StorageInfo type is nil, it +// will set nothing and immediately return. +func (w *StorageInfo) SetStorageAddress(v string) { + // return if StorageInfo type is nil + if w == nil { + return + } + + w.StorageAddress = &v +} diff --git a/api/types/storage_info_test.go b/api/types/storage_info_test.go new file mode 100644 index 000000000..7dd75f004 --- /dev/null +++ b/api/types/storage_info_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" +) + +func TestTypes_StorageInfo_Getters(t *testing.T) { + // setup tests + tests := []struct { + sI *StorageInfo + want *StorageInfo + }{ + { + sI: testStorageInfo(), + want: testStorageInfo(), + }, + { + sI: new(StorageInfo), + want: new(StorageInfo), + }, + } + + // run tests + for _, test := range tests { + if test.sI.GetAccessKey() != test.want.GetAccessKey() { + t.Errorf("GetAccessKey is %v, want %v", test.sI.GetAccessKey(), test.want.GetAccessKey()) + } + + if test.sI.GetSecretKey() != test.want.GetSecretKey() { + t.Errorf("GetSecretKey is %v, want %v", test.sI.GetSecretKey(), test.want.GetSecretKey()) + } + + if test.sI.GetStorageAddress() != test.want.GetStorageAddress() { + t.Errorf("GetStorageAddress is %v, want %v", test.sI.GetStorageAddress(), test.want.GetStorageAddress()) + } + } +} + +func TestTypes_StorageInfo_Setters(t *testing.T) { + // setup types + var sI *StorageInfo + + // setup tests + tests := []struct { + sI *StorageInfo + want *StorageInfo + }{ + { + sI: testStorageInfo(), + want: testStorageInfo(), + }, + { + sI: sI, + want: new(StorageInfo), + }, + } + + // run tests + for _, test := range tests { + test.sI.SetAccessKey(test.want.GetAccessKey()) + test.sI.SetSecretKey(test.want.GetSecretKey()) + test.sI.SetStorageAddress(test.want.GetStorageAddress()) + + if test.sI.GetAccessKey() != test.want.GetAccessKey() { + t.Errorf("GetAccessKey is %v, want %v", test.sI.GetAccessKey(), test.want.GetAccessKey()) + } + + if test.sI.GetSecretKey() != test.want.GetSecretKey() { + t.Errorf("GetSecretKey is %v, want %v", test.sI.GetSecretKey(), test.want.GetSecretKey()) + } + + if test.sI.GetStorageAddress() != test.want.GetStorageAddress() { + t.Errorf("GetStorageAddress is %v, want %v", test.sI.GetStorageAddress(), test.want.GetStorageAddress()) + } + } +} + +// testStorageInfo is a test helper function to register a StorageInfo +// type with all fields set to a fake value. +func testStorageInfo() *StorageInfo { + sI := new(StorageInfo) + sI.SetAccessKey("fakeAccessKey") + sI.SetSecretKey("fakeSecretKey") + sI.SetStorageAddress("http://localhost:8080") + + return sI +} diff --git a/api/types/test-report/code_coverage_file.go b/api/types/test-report/code_coverage_file.go new file mode 100644 index 000000000..31b513b93 --- /dev/null +++ b/api/types/test-report/code_coverage_file.go @@ -0,0 +1,214 @@ +package test_report + +// CodeCoverageFile is the API types representation of a code coverage file for a pipeline. +// +// swagger:model CodeCoverageFile +type CodeCoverageFile struct { + ID *int64 `json:"id,omitempty"` + CodeCoverageRun *CodeCoverageRun `json:"code_coverage_run,omitempty"` + CodeCoverageGroup *CodeCoverageGroup `json:"code_coverage_group,omitempty"` + CodeCoverageStats *CodeCoverageStats `json:"code_coverage_stats,omitempty"` + DirectoryName *string `json:"directory_name,omitempty"` + FileName *string `json:"file_name,omitempty"` + MissedLines *int `json:"missed_lines,omitempty"` + PartialLines *int `json:"partial_lines,omitempty"` + FilePath *string `json:"file_path,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetID() int64 { + if c == nil || c.ID == nil { + return 0 + } + return *c.ID +} + +// SetID sets the ID field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetID(v int64) { + if c == nil { + return + } + c.ID = &v +} + +// GetCodeCoverageRun returns the CodeCoverageRun field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetCodeCoverageRun() *CodeCoverageRun { + if c == nil || c.CodeCoverageRun == nil { + return new(CodeCoverageRun) + } + return c.CodeCoverageRun +} + +// SetCodeCoverageRun sets the CodeCoverageRun field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetCodeCoverageRun(v *CodeCoverageRun) { + if c == nil { + return + } + c.CodeCoverageRun = v +} + +// GetCodeCoverageGroup returns the CodeCoverageGroup field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetCodeCoverageGroup() *CodeCoverageGroup { + if c == nil || c.CodeCoverageGroup == nil { + return new(CodeCoverageGroup) + } + return c.CodeCoverageGroup +} + +// SetCodeCoverageGroup sets the CodeCoverageGroup field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetCodeCoverageGroup(v *CodeCoverageGroup) { + if c == nil { + return + } + c.CodeCoverageGroup = v +} + +// GetCodeCoverageStats returns the CodeCoverageStats field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetCodeCoverageStats() *CodeCoverageStats { + if c == nil || c.CodeCoverageStats == nil { + return new(CodeCoverageStats) + } + return c.CodeCoverageStats +} + +// SetCodeCoverageStats sets the CodeCoverageStats field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetCodeCoverageStats(v *CodeCoverageStats) { + if c == nil { + return + } + c.CodeCoverageStats = v +} + +// GetDirectoryName returns the DirectoryName field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetDirectoryName() string { + if c == nil || c.DirectoryName == nil { + return "" + } + return *c.DirectoryName +} + +// SetDirectoryName sets the DirectoryName field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetDirectoryName(v string) { + if c == nil { + return + } + c.DirectoryName = &v +} + +// GetFileName returns the FileName field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetFileName() string { + if c == nil || c.FileName == nil { + return "" + } + return *c.FileName +} + +// SetFileName sets the FileName field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetFileName(v string) { + if c == nil { + return + } + c.FileName = &v +} + +// GetMissedLines returns the MissedLines field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetMissedLines() int { + if c == nil || c.MissedLines == nil { + return 0 + } + return *c.MissedLines +} + +// SetMissedLines sets the MissedLines field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetMissedLines(v int) { + if c == nil { + return + } + c.MissedLines = &v +} + +// GetPartialLines returns the PartialLines field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetPartialLines() int { + if c == nil || c.PartialLines == nil { + return 0 + } + return *c.PartialLines +} + +// SetPartialLines sets the PartialLines field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetPartialLines(v int) { + if c == nil { + return + } + c.PartialLines = &v +} + +// GetFilePath returns the FilePath field. +// +// When the provided CodeCoverageFile type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageFile) GetFilePath() string { + if c == nil || c.FilePath == nil { + return "" + } + return *c.FilePath +} + +// SetFilePath sets the FilePath field. +// +// When the provided CodeCoverageFile type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageFile) SetFilePath(v string) { + if c == nil { + return + } + c.FilePath = &v +} diff --git a/api/types/test-report/code_coverage_group.go b/api/types/test-report/code_coverage_group.go new file mode 100644 index 000000000..88e832701 --- /dev/null +++ b/api/types/test-report/code_coverage_group.go @@ -0,0 +1,99 @@ +package test_report + +// CodeCoverageGroup is the API types representation of a group of code coverage. +// +// swagger:model CodeCoverageGroup +type CodeCoverageGroup struct { + ID *int64 `json:"id,omitempty"` + CodeCoverageRun *CodeCoverageRun `json:"code_coverage_run,omitempty"` + Name *string `json:"name,omitempty"` + CodeCoverageStats *CodeCoverageStats `json:"code_coverage_stats,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided CodeCoverageGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageGroup) GetID() int64 { + if c == nil || c.ID == nil { + return 0 + } + return *c.ID +} + +// SetID sets the ID field. +// +// When the provided CodeCoverageGroup type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageGroup) SetID(v int64) { + if c == nil { + return + } + c.ID = &v +} + +// GetCodeCoverageRun returns the CodeCoverageRun field. +// +// When the provided CodeCoverageGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageGroup) GetCodeCoverageRun() *CodeCoverageRun { + if c == nil || c.CodeCoverageRun == nil { + return new(CodeCoverageRun) + } + return c.CodeCoverageRun +} + +// SetCodeCoverageRun sets the CodeCoverageRun field. +// +// When the provided CodeCoverageGroup type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageGroup) SetCodeCoverageRun(v *CodeCoverageRun) { + if c == nil { + return + } + c.CodeCoverageRun = v +} + +// GetName returns the Name field. +// +// When the provided CodeCoverageGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageGroup) GetName() string { + if c == nil || c.Name == nil { + return "" + } + return *c.Name +} + +// SetName sets the Name field. +// +// When the provided CodeCoverageGroup type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageGroup) SetName(v string) { + if c == nil { + return + } + c.Name = &v +} + +// GetCodeCoverageStats returns the CodeCoverageStats field. +// +// When the provided CodeCoverageGroup type is nil, or the field within +// the type is nil, it returns a new CodeCoverageStats instance. +func (c *CodeCoverageGroup) GetCodeCoverageStats() *CodeCoverageStats { + if c == nil || c.CodeCoverageStats == nil { + return new(CodeCoverageStats) + } + return c.CodeCoverageStats +} + +// SetCodeCoverageStats sets the CodeCoverageStats field. +// +// When the provided CodeCoverageGroup type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageGroup) SetCodeCoverageStats(v *CodeCoverageStats) { + if c == nil { + return + } + c.CodeCoverageStats = v +} diff --git a/api/types/test-report/code_coverage_run.go b/api/types/test-report/code_coverage_run.go new file mode 100644 index 000000000..23128be83 --- /dev/null +++ b/api/types/test-report/code_coverage_run.go @@ -0,0 +1,57 @@ +package test_report + +// CodeCoverageRun is the API types representation of a code coverage run for a pipeline. +// +// swagger:model CodeCoverageRun +type CodeCoverageRun struct { + ID *int64 `json:"id,omitempty"` + TestRunPublicID *string `json:"test_run_public_id,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided CodeCoverageRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageRun) GetID() int64 { + if c == nil || c.ID == nil { + return 0 + } + return *c.ID +} + +// GetTestRunPublicID returns the TestRunPublicID field. +// +// When the provided CodeCoverageRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageRun) GetTestRunPublicID() string { + if c == nil || c.TestRunPublicID == nil { + return "" + } + return *c.TestRunPublicID +} + +// SetID sets the ID field. +// +// When the provided CodeCoverageRun type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageRun) SetID(v int64) { + // return if CodeCoverageRun type is nil + if c == nil { + return + } + + c.ID = &v +} + +// SetTestRunPublicID sets the TestRunPublicID field. +// +// When the provided CodeCoverageRun type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageRun) SetTestRunPublicID(v string) { + // return if CodeCoverageRun type is nil + if c == nil { + return + } + + c.TestRunPublicID = &v +} diff --git a/api/types/test-report/code_coverage_stats.go b/api/types/test-report/code_coverage_stats.go new file mode 100644 index 000000000..5fce2aca8 --- /dev/null +++ b/api/types/test-report/code_coverage_stats.go @@ -0,0 +1,214 @@ +package test_report + +// CodeCoverageStats is the API types representation of code coverage stats for a pipeline. +// +// swagger:model CodeCoverageStats +type CodeCoverageStats struct { + ID *int64 `json:"id,omitempty"` + CodeCoverageRun *CodeCoverageRun `json:"code_coverage_run,omitempty"` + Scope *string `json:"scope,omitempty"` + StatementCovered *int `json:"statement_covered,omitempty"` + StatementMissed *int `json:"statement_missed,omitempty"` + LineCovered *int `json:"line_covered,omitempty"` + LineMissed *int `json:"line_missed,omitempty"` + BranchCovered *int `json:"branch_covered,omitempty"` + BranchMissed *int `json:"branch_missed,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetID() int64 { + if c == nil || c.ID == nil { + return 0 + } + return *c.ID +} + +// SetID sets the ID field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetID(v int64) { + if c == nil { + return + } + c.ID = &v +} + +// GetCodeCoverageRun returns the CodeCoverageRun field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetCodeCoverageRun() *CodeCoverageRun { + if c == nil || c.CodeCoverageRun == nil { + return new(CodeCoverageRun) + } + return c.CodeCoverageRun +} + +// SetCodeCoverageRun sets the CodeCoverageRun field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetCodeCoverageRun(v *CodeCoverageRun) { + if c == nil { + return + } + c.CodeCoverageRun = v +} + +// GetScope returns the Scope field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetScope() string { + if c == nil || c.Scope == nil { + return "" + } + return *c.Scope +} + +// SetScope sets the Scope field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetScope(v string) { + if c == nil { + return + } + c.Scope = &v +} + +// GetStatementCovered returns the StatementCovered field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetStatementCovered() int { + if c == nil || c.StatementCovered == nil { + return 0 + } + return *c.StatementCovered +} + +// SetStatementCovered sets the StatementCovered field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetStatementCovered(v int) { + if c == nil { + return + } + c.StatementCovered = &v +} + +// GetStatementMissed returns the StatementMissed field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetStatementMissed() int { + if c == nil || c.StatementMissed == nil { + return 0 + } + return *c.StatementMissed +} + +// SetStatementMissed sets the StatementMissed field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetStatementMissed(v int) { + if c == nil { + return + } + c.StatementMissed = &v +} + +// GetLineCovered returns the LineCovered field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetLineCovered() int { + if c == nil || c.LineCovered == nil { + return 0 + } + return *c.LineCovered +} + +// SetLineCovered sets the LineCovered field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetLineCovered(v int) { + if c == nil { + return + } + c.LineCovered = &v +} + +// GetLineMissed returns the LineMissed field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetLineMissed() int { + if c == nil || c.LineMissed == nil { + return 0 + } + return *c.LineMissed +} + +// SetLineMissed sets the LineMissed field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetLineMissed(v int) { + if c == nil { + return + } + c.LineMissed = &v +} + +// GetBranchCovered returns the BranchCovered field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetBranchCovered() int { + if c == nil || c.BranchCovered == nil { + return 0 + } + return *c.BranchCovered +} + +// SetBranchCovered sets the BranchCovered field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetBranchCovered(v int) { + if c == nil { + return + } + c.BranchCovered = &v +} + +// GetBranchMissed returns the BranchMissed field. +// +// When the provided CodeCoverageStats type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (c *CodeCoverageStats) GetBranchMissed() int { + if c == nil || c.BranchMissed == nil { + return 0 + } + return *c.BranchMissed +} + +// SetBranchMissed sets the BranchMissed field. +// +// When the provided CodeCoverageStats type is nil, it +// will set nothing and immediately return. +func (c *CodeCoverageStats) SetBranchMissed(v int) { + if c == nil { + return + } + c.BranchMissed = &v +} diff --git a/api/types/test-report/performance_results.go b/api/types/test-report/performance_results.go new file mode 100644 index 000000000..f29b6da7c --- /dev/null +++ b/api/types/test-report/performance_results.go @@ -0,0 +1,130 @@ +package test_report + +// PerformanceResults is the API types representation of performance results for a test run. +// +// swagger:model PerformanceResults +type PerformanceResults struct { + ID *int64 `json:"id,omitempty"` + TestRun *TestRun `json:"test_run,omitempty"` + TestRunPublicID *string `json:"test_run_public_id,omitempty"` + Name *string `json:"name,omitempty"` + RequestCount *int64 `json:"request_count,omitempty"` + RequestsPerSecond *float64 `json:"requests_per_second,omitempty"` + Average *float64 `json:"average,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + P95 *float64 `json:"p95,omitempty"` +} // GetID returns the ID field. +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetID() int64 { + // return zero value if PerformanceResults type or ID field is nil + if p == nil || p.ID == nil { + return 0 + } + + return *p.ID +} + +// GetTestRun returns the TestRun field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetTestRun() TestRun { + // return zero value if PerformanceResults type or TestRun field is nil + if p == nil || p.TestRun == nil { + return TestRun{} + } + + return *p.TestRun +} + +// GetTestRunPublicID returns the TestRunPublicID field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetTestRunPublicID() string { + // return zero value if PerformanceResults type or TestRunPublicID field is nil + if p == nil || p.TestRunPublicID == nil { + return "" + } + + return *p.TestRunPublicID +} + +// GetName returns the Name field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetName() string { + // return zero value if PerformanceResults type or Name field is nil + if p == nil || p.Name == nil { + return "" + } + + return *p.Name +} + +// GetRequestCount returns the RequestCount field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetRequestCount() int64 { + // return zero value if PerformanceResults type or RequestCount field is nil + if p == nil || p.RequestCount == nil { + return 0 + } + + return *p.RequestCount +} + +// GetRequestsPerSecond returns the RequestsPerSecond field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetRequestsPerSecond() float64 { + // return zero value if PerformanceResults type or RequestsPerSecond field is nil + if p == nil || p.RequestsPerSecond == nil { + return 0.0 + } + + return *p.RequestsPerSecond +} + +// GetAverage returns the Average field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetAverage() float64 { + // return zero value if PerformanceResults type or Average field is nil + if p == nil || p.Average == nil { + return 0.0 + } + + return *p.Average +} + +// GetMaximum returns the Maximum field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetMaximum() float64 { + // return zero value if PerformanceResults type or Maximum field is nil + if p == nil || p.Maximum == nil { + return 0.0 + } + + return *p.Maximum +} + +// GetP95 returns the P95 field. +// +// When the provided PerformanceResults type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (p *PerformanceResults) GetP95() float64 { + // return zero value if PerformanceResults type or P95 field is nil + if p == nil || p.P95 == nil { + return 0.0 + } + + return *p.P95 +} diff --git a/api/types/test-report/results_metadata.go b/api/types/test-report/results_metadata.go new file mode 100644 index 000000000..d9a33333c --- /dev/null +++ b/api/types/test-report/results_metadata.go @@ -0,0 +1,115 @@ +package test_report + +// ResultsMetadata is the API types representation of metadata for a test run. +// +// swagger:model ResultsMetadata +type ResultsMetadata struct { + ID *int64 `json:"id,omitempty"` + TestRun *TestRun `json:"test_run,omitempty"` + CI *bool `json:"ci,omitempty"` + Group *string `json:"group,omitempty"` +} + +// SetID sets the ID field. +// +// When the provided ResultsMetadata type is nil, it +// will set nothing and immediately return. +func (r *ResultsMetadata) SetID(v int64) { + // return if ResultsMetadata type is nil + if r == nil { + return + } + + r.ID = &v +} + +// GetID returns the ID field. +// +// When the provided ResultsMetadata type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsMetadata) GetID() int64 { + // return zero value if ResultsMetadata type or ID field is nil + if r == nil || r.ID == nil { + return 0 + } + + return *r.ID +} + +// SetTestRun sets the TestRun field. +// +// When the provided ResultsMetadata type is nil, it +// will set nothing and immediately return. +func (r *ResultsMetadata) SetTestRun(v TestRun) { + // return if ResultsMetadata type is nil + if r == nil { + return + } + + r.TestRun = &v +} + +// GetTestRun returns the TestRun field. +// +// When the provided ResultsMetadata type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsMetadata) GetTestRun() TestRun { + // return zero value if ResultsMetadata type or TestRun field is nil + if r == nil || r.TestRun == nil { + return TestRun{} + } + + return *r.TestRun +} + +// SetCI sets the CI field. +// +// When the provided ResultsMetadata type is nil, it +// will set nothing and immediately return. +func (r *ResultsMetadata) SetCI(v bool) { + // return if ResultsMetadata type is nil + if r == nil { + return + } + + r.CI = &v +} + +// GetCI returns the CI field. +// +// When the provided ResultsMetadata type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsMetadata) GetCI() bool { + // return zero value if ResultsMetadata type or CI field is nil + if r == nil || r.CI == nil { + return false + } + + return *r.CI +} + +// SetGroup sets the Group field. +// +// When the provided ResultsMetadata type is nil, it +// will set nothing and immediately return. +func (r *ResultsMetadata) SetGroup(v string) { + // return if ResultsMetadata type is nil + if r == nil { + return + } + + r.Group = &v +} + +// GetGroup returns the Group field. +// +// When the provided ResultsMetadata type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsMetadata) GetGroup() string { + // return zero value if ResultsMetadata type or Group field is nil + if r == nil || r.Group == nil { + return "" + } + + return *r.Group +} diff --git a/api/types/test-report/results_processing.go b/api/types/test-report/results_processing.go new file mode 100644 index 000000000..840aea5c4 --- /dev/null +++ b/api/types/test-report/results_processing.go @@ -0,0 +1,118 @@ +package test_report + +// ResultsProcessing is the API types representation of processing results for a test run. +// +// swagger:model ResultsProcessing +type ResultsProcessing struct { + ID *int64 `json:"id,omitempty"` + Status *string `json:"status,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + Created *int64 `json:"created,omitempty"` + //Created replaced created_timestamp in Projektor model + // Following the same pattern as other fields in Vela project + // PublicID is replaced by ID for consistency across all models +} + +// SetID sets the PublicID field. +// +// When the provided ResultsProcessing type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessing) SetID(v int64) { + // return if ResultsProcessing type is nil + if r == nil { + return + } + + r.ID = &v +} + +// GetPublicID returns the PublicID field. +// +// When the provided ResultsProcessing type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessing) GetPublicID() int64 { + // return zero value if ResultsProcessing type or PublicID field is nil + if r == nil || r.ID == nil { + return 0 + } + + return *r.ID +} + +// SetStatus sets the Status field. +// +// When the provided ResultsProcessing type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessing) SetStatus(v string) { + // return if ResultsProcessing type is nil + if r == nil { + return + } + + r.Status = &v +} + +// GetStatus returns the Status field. +// +// When the provided ResultsProcessing type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessing) GetStatus() string { + // return zero value if ResultsProcessing type or Status field is nil + if r == nil || r.Status == nil { + return "" + } + + return *r.Status +} + +// SetErrorMessage sets the ErrorMessage field. +// +// When the provided ResultsProcessing type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessing) SetErrorMessage(v string) { + // return if ResultsProcessing type is nil + if r == nil { + return + } + + r.ErrorMessage = &v +} + +// GetErrorMessage returns the ErrorMessage field. +// +// When the provided ResultsProcessing type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessing) GetErrorMessage() string { + // return zero value if ResultsProcessing type or ErrorMessage field is nil + if r == nil || r.ErrorMessage == nil { + return "" + } + + return *r.ErrorMessage +} + +// SetCreated sets the Created field. +// +// When the provided ResultsProcessing type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessing) SetCreated(v int64) { + // return if ResultsProcessing type is nil + if r == nil { + return + } + + r.Created = &v +} + +// GetCreated returns the Created field. +// +// When the provided ResultsProcessing type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessing) GetCreated() int64 { + // return zero value if ResultsProcessing type or Created field is nil + if r == nil || r.Created == nil { + return 0 + } + + return *r.Created +} diff --git a/api/types/test-report/results_processing_failure.go b/api/types/test-report/results_processing_failure.go new file mode 100644 index 000000000..ad9ffd26a --- /dev/null +++ b/api/types/test-report/results_processing_failure.go @@ -0,0 +1,169 @@ +package test_report + +// ResultsProcessingFailure is the API types representation of a failure processing test results. +// +// swagger:model ResultsProcessingFailure +type ResultsProcessingFailure struct { + ID *int64 `json:"id,omitempty"` + Body *string `json:"body,omitempty"` + Created *int64 `json:"created,omitempty"` + FailureMessage *string `json:"failure_message,omitempty"` + FailureType *string `json:"failure_type,omitempty"` + BodyType *string `json:"body_type,omitempty"` +} + +// SetID sets the ID field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetID(v int64) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.ID = &v +} + +// GetID returns the ID field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetID() int64 { + // return zero value if ResultsProcessingFailure type or ID field is nil + if r == nil || r.ID == nil { + return 0 + } + + return *r.ID +} + +// SetBody sets the Body field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetBody(v string) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.Body = &v +} + +// GetBody returns the Body field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetBody() string { + // return zero value if ResultsProcessingFailure type or Body field is nil + if r == nil || r.Body == nil { + return "" + } + + return *r.Body +} + +// SetCreated sets the Created field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetCreated(v int64) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.Created = &v +} + +// GetCreated returns the Created field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetCreated() int64 { + // return zero value if ResultsProcessingFailure type or Created field is nil + if r == nil || r.Created == nil { + return 0 + } + + return *r.Created +} + +// SetFailureMessage sets the FailureMessage field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetFailureMessage(v string) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.FailureMessage = &v +} + +// GetFailureMessage returns the FailureMessage field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetFailureMessage() string { + // return zero value if ResultsProcessingFailure type or FailureMessage field is nil + if r == nil || r.FailureMessage == nil { + return "" + } + + return *r.FailureMessage +} + +// SetFailureType sets the FailureType field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetFailureType(v string) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.FailureType = &v +} + +// GetFailureType returns the FailureType field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetFailureType() string { + // return zero value if ResultsProcessingFailure type or FailureType field is nil + if r == nil || r.FailureType == nil { + return "" + } + + return *r.FailureType +} + +// SetBodyType sets the BodyType field. +// +// When the provided ResultsProcessingFailure type is nil, it +// will set nothing and immediately return. +func (r *ResultsProcessingFailure) SetBodyType(v string) { + // return if ResultsProcessingFailure type is nil + if r == nil { + return + } + + r.BodyType = &v +} + +// GetBodyType returns the BodyType field. +// +// When the provided ResultsProcessingFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (r *ResultsProcessingFailure) GetBodyType() string { + // return zero value if ResultsProcessingFailure type or BodyType field is nil + if r == nil || r.BodyType == nil { + return "" + } + + return *r.BodyType +} diff --git a/api/types/test-report/shedlock.go b/api/types/test-report/shedlock.go new file mode 100644 index 000000000..767b987dc --- /dev/null +++ b/api/types/test-report/shedlock.go @@ -0,0 +1,115 @@ +package test_report + +// Shedlock is the API types representation of a shedlock lock. +// +// swagger:model Shedlock +type Shedlock struct { + Name *string `json:"name,omitempty"` + LockUntil *int64 `json:"lock_until,omitempty"` + LockAt *int64 `json:"lock_at,omitempty"` + LockedBy *string `json:"locked_by,omitempty"` +} + +// SetName sets the Name field. +// +// When the provided Shedlock type is nil, it +// will set nothing and immediately return. +func (s *Shedlock) SetName(v string) { + // return if Shedlock type is nil + if s == nil { + return + } + + s.Name = &v +} + +// GetName returns the Name field. +// +// When the provided Shedlock type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (s *Shedlock) GetName() string { + // return zero value if Shedlock type or Name field is nil + if s == nil || s.Name == nil { + return "" + } + + return *s.Name +} + +// SetLockUntil sets the LockUntil field. +// +// When the provided Shedlock type is nil, it +// will set nothing and immediately return. +func (s *Shedlock) SetLockUntil(v int64) { + // return if Shedlock type is nil + if s == nil { + return + } + + s.LockUntil = &v +} + +// GetLockUntil returns the LockUntil field. +// +// When the provided Shedlock type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (s *Shedlock) GetLockUntil() int64 { + // return zero value if Shedlock type or LockUntil field is nil + if s == nil || s.LockUntil == nil { + return 0 + } + + return *s.LockUntil +} + +// SetLockAt sets the LockAt field. +// +// When the provided Shedlock type is nil, it +// will set nothing and immediately return. +func (s *Shedlock) SetLockAt(v int64) { + // return if Shedlock type is nil + if s == nil { + return + } + + s.LockAt = &v +} + +// GetLockAt returns the LockAt field. +// +// When the provided Shedlock type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (s *Shedlock) GetLockAt() int64 { + // return zero value if Shedlock type or LockAt field is nil + if s == nil || s.LockAt == nil { + return 0 + } + + return *s.LockAt +} + +// SetLockedBy sets the LockedBy field. +// +// When the provided Shedlock type is nil, it +// will set nothing and immediately return. +func (s *Shedlock) SetLockedBy(v string) { + // return if Shedlock type is nil + if s == nil { + return + } + + s.LockedBy = &v +} + +// GetLockedBy returns the LockedBy field. +// +// When the provided Shedlock type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (s *Shedlock) GetLockedBy() string { + // return zero value if Shedlock type or LockedBy field is nil + if s == nil || s.LockedBy == nil { + return "" + } + + return *s.LockedBy +} diff --git a/api/types/test-report/test_case.go b/api/types/test-report/test_case.go new file mode 100644 index 000000000..2ad1e16cc --- /dev/null +++ b/api/types/test-report/test_case.go @@ -0,0 +1,419 @@ +package test_report + +import "time" + +// TestCases is the API types representation of a test case. +// +// swagger:model TestCase + +type TestCase struct { + ID *int64 `json:"id,omitempty"` + TestSuite *TestSuite `json:"test_suite,omitempty"` + Idx *int64 `json:"idx,omitempty"` + Name *string `json:"name,omitempty"` + PackageName *string `json:"package_name,omitempty"` + ClassName *string `json:"class_name,omitempty"` + Created *int64 `json:"created,omitempty"` + Started *int64 `json:"started,omitempty"` + Finished *int64 `json:"finished,omitempty"` + Passed *bool `json:"passed,omitempty"` + Skipped *bool `json:"skipped,omitempty"` + SystemOut *string `json:"system_out,omitempty"` + SystemErr *string `json:"system_err,omitempty"` + HasSystemOut *bool `json:"has_system_out,omitempty"` + HasSystemErr *bool `json:"has_system_err,omitempty"` +} + +// Duration calculates and returns the total amount of +// time the test case ran for in a human-readable format. +func (t *TestCase) Duration() string { + // check if the test case doesn't have a started timestamp + if t.GetStarted() == 0 { + return "..." + } + + // capture started unix timestamp from the test case + started := time.Unix(t.GetStarted(), 0) + + // check if the test case doesn't have a finished timestamp + if t.GetFinished() == 0 { + // return the duration in a human-readable form by + // subtracting the test case started time from the + // current time rounded to the nearest second + return time.Since(started).Round(time.Second).String() + } + + // capture finished unix timestamp from the test case + finished := time.Unix(t.GetFinished(), 0) + + // calculate the duration by subtracting the test case + // started time from the test case finished time + duration := finished.Sub(started) + + // return the duration in a human-readable form + return duration.String() +} + +// SetID sets the ID field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetID(v int64) { + // return if TestCase type is nil + if t == nil { + return + } + + t.ID = &v +} + +// GetID returns the ID field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetID() int64 { + // return zero value if TestCase type or ID field is nil + if t == nil || t.ID == nil { + return 0 + } + + return *t.ID +} + +// SetIdx sets the Idx field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetIdx(v int64) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Idx = &v +} + +// GetIdx returns the Idx field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetIdx() int64 { + // return zero value if TestCase type or Idx field is nil + if t == nil || t.Idx == nil { + return 0 + } + + return *t.Idx +} + +// SetName sets the Name field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetName(v string) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Name = &v +} + +// GetName returns the Name field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetName() string { + // return zero value if TestCase type or Name field is nil + if t == nil || t.Name == nil { + return "" + } + + return *t.Name +} + +// SetPackageName sets the PackageName field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetPackageName(v string) { + // return if TestCase type is nil + if t == nil { + return + } + + t.PackageName = &v +} + +// GetPackageName returns the PackageName field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetPackageName() string { + // return zero value if TestCase type or PackageName field is nil + if t == nil || t.PackageName == nil { + return "" + } + + return *t.PackageName +} + +// SetClassName sets the ClassName field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetClassName(v string) { + // return if TestCase type is nil + if t == nil { + return + } + + t.ClassName = &v +} + +// GetClassName returns the ClassName field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetClassName() string { + // return zero value if TestCase type or ClassName field is nil + if t == nil || t.ClassName == nil { + return "" + } + + return *t.ClassName +} + +// SetCreated sets the Created field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetCreated(v int64) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Created = &v +} + +// GetCreated returns the Created field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetCreated() int64 { + // return zero value if TestCase type or Created field is nil + if t == nil || t.Created == nil { + return 0 + } + + return *t.Created +} + +// SetStarted sets the Started field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetStarted(v int64) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Started = &v +} + +// GetStarted returns the Started field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetStarted() int64 { + // return zero value if TestCase type or Started field is nil + if t == nil || t.Started == nil { + return 0 + } + + return *t.Started +} + +// SetFinished sets the Finished field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetFinished(v int64) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Finished = &v +} + +// GetFinished returns the Finished field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetFinished() int64 { + // return zero value if TestCase type or Finished field is nil + if t == nil || t.Finished == nil { + return 0 + } + + return *t.Finished +} + +// SetPassed sets the Passed field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetPassed(v bool) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Passed = &v +} + +// GetPassed returns the Passed field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetPassed() bool { + // return zero value if TestCase type or Passed field is nil + if t == nil || t.Passed == nil { + return false + } + + return *t.Passed +} + +// SetSkipped sets the Skipped field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetSkipped(v bool) { + // return if TestCase type is nil + if t == nil { + return + } + + t.Skipped = &v +} + +// GetSkipped returns the Skipped field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetSkipped() bool { + // return zero value if TestCase type or Skipped field is nil + if t == nil || t.Skipped == nil { + return false + } + + return *t.Skipped +} + +// SetSystemOut sets the SystemOut field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetSystemOut(v string) { + // return if TestCase type is nil + if t == nil { + return + } + + t.SystemOut = &v +} + +// GetSystemOut returns the SystemOut field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetSystemOut() string { + // return zero value if TestCase type or SystemOut field is nil + if t == nil || t.SystemOut == nil { + return "" + } + + return *t.SystemOut +} + +// SetSystemErr sets the SystemErr field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetSystemErr(v string) { + // return if TestCase type is nil + if t == nil { + return + } + + t.SystemErr = &v +} + +// GetSystemErr returns the SystemErr field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetSystemErr() string { + // return zero value if TestCase type or SystemErr field is nil + if t == nil || t.SystemErr == nil { + return "" + } + + return *t.SystemErr +} + +// SetHasSystemOut sets the HasSystemOut field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetHasSystemOut(v bool) { + // return if TestCase type is nil + if t == nil { + return + } + + t.HasSystemOut = &v +} + +// GetHasSystemOut returns the HasSystemOut field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetHasSystemOut() bool { + // return zero value if TestCase type or HasSystemOut field is nil + if t == nil || t.HasSystemOut == nil { + return false + } + + return *t.HasSystemOut +} + +// SetHasSystemErr sets the HasSystemErr field. +// +// When the provided TestCase type is nil, it +// will set nothing and immediately return. +func (t *TestCase) SetHasSystemErr(v bool) { + // return if TestCase type is nil + if t == nil { + return + } + + t.HasSystemErr = &v +} + +// GetHasSystemErr returns the HasSystemErr field. +// +// When the provided TestCase type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestCase) GetHasSystemErr() bool { + // return zero value if TestCase type or HasSystemErr field is nil + if t == nil || t.HasSystemErr == nil { + return false + } + + return *t.HasSystemErr +} diff --git a/api/types/test-report/test_failure.go b/api/types/test-report/test_failure.go new file mode 100644 index 000000000..62cc83dfb --- /dev/null +++ b/api/types/test-report/test_failure.go @@ -0,0 +1,132 @@ +package test_report + +// TestFailure is the API types representation of a test for a pipeline. +// +// swagger:model TestFailure +type TestFailure struct { + ID *int64 `json:"id,omitempty"` + TestCaseID *int64 `json:"test_case_id,omitempty"` + FailureMessage *string `json:"failure_message,omitempty"` + FailureType *string `json:"failure_type,omitempty"` + FailureText *string `json:"failure_text,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided TestFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestFailure) GetID() int64 { + if t == nil || t.ID == nil { + return 0 + } + return *t.ID +} + +// GetTestCaseID returns the TestCaseID field. +// +// When the provided TestFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestFailure) GetTestCaseID() int64 { + if t == nil || t.TestCaseID == nil { + return 0 + } + return *t.TestCaseID +} + +// GetFailureMessage returns the FailureMessage field. +// +// When the provided TestFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestFailure) GetFailureMessage() string { + if t == nil || t.FailureMessage == nil { + return "" + } + return *t.FailureMessage +} + +// GetFailureType returns the FailureType field. +// +// When the provided TestFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestFailure) GetFailureType() string { + if t == nil || t.FailureType == nil { + return "" + } + return *t.FailureType +} + +// GetFailureText returns the FailureText field. +// +// When the provided TestFailure type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestFailure) GetFailureText() string { + if t == nil || t.FailureText == nil { + return "" + } + return *t.FailureText +} + +// SetID sets the ID field. +// +// When the provided TestFailure type is nil, it +// will set nothing and immediately return. +func (t *TestFailure) SetID(v int64) { + // return if TestFailure type is nil + if t == nil { + return + } + + t.ID = &v +} + +// SetTestCaseID sets the TestCaseID field. +// +// When the provided TestFailure type is nil, it +// will set nothing and immediately return. +func (t *TestFailure) SetTestCaseID(v int64) { + // return if TestFailure type is nil + if t == nil { + return + } + + t.TestCaseID = &v +} + +// SetFailureMessage sets the FailureMessage field. +// +// When the provided TestFailure type is nil, it +// will set nothing and immediately return. +func (t *TestFailure) SetFailureMessage(v string) { + // return if TestFailure type is nil + if t == nil { + return + } + + t.FailureMessage = &v +} + +// SetFailureType sets the FailureType field. +// +// When the provided TestFailure type is nil, it +// will set nothing and immediately return. +func (t *TestFailure) SetFailureType(v string) { + // return if TestFailure type is nil + if t == nil { + return + } + + t.FailureType = &v +} + +// SetFailureText sets the FailureText field. +// +// When the provided TestFailure type is nil, it +// will set nothing and immediately return. +func (t *TestFailure) SetFailureText(v string) { + // return if TestFailure type is nil + if t == nil { + return + } + + t.FailureText = &v +} diff --git a/api/types/test-report/test_run.go b/api/types/test-report/test_run.go new file mode 100644 index 000000000..2e9eaee52 --- /dev/null +++ b/api/types/test-report/test_run.go @@ -0,0 +1,319 @@ +package test_report + +import "time" + +// TestRun is the API types representation of a test run for a pipeline. +// +// swagger:model TestRun +type TestRun struct { + ID *int64 `json:"id,omitempty"` + TestRunSystemAttributes *TestRunSystemAttributes `json:"test_run_system_attributes,omitempty"` + TotalTestCount *int `json:"total_test_count,omitempty"` + TotalPassingCount *int `json:"total_passing_count,omitempty"` + TotalSkipCount *int `json:"total_skip_count,omitempty"` + TotalFailureCount *int `json:"total_failure_count,omitempty"` + Passed *bool `json:"passed,omitempty"` + Created *int64 `json:"created,omitempty"` + Started *int64 `json:"started,omitempty"` + Finished *int64 `json:"finished,omitempty"` + AverageDuration *int64 `json:"average_duration,omitempty"` + SlowestTestCaseDuration *int64 `json:"slowest_test_case_duration,omitempty"` + WallClockDuration *int64 `json:"wall_clock_duration,omitempty"` +} + +// In Vela, int64 is used to stored Unix timestamps instead of float64 for time.Time +// created_timestamp is replaced by Created +// Original Projektor model has cumulative_duration field which is not present in this model +// Created, Started, Finished are Vela standard model fields +// AverageDuration, SlowestTestCaseDuration, WallClockDuration might be taken out and calculated on the fly +// like in the Duration method + +// Duration calculates and returns the total amount of +// time the build ran for in a human-readable format. +func (b *TestRun) Duration() string { + // check if the build doesn't have a started timestamp + if b.GetStarted() == 0 { + return "..." + } + + // capture started unix timestamp from the build + started := time.Unix(b.GetStarted(), 0) + + // check if the build doesn't have a finished timestamp + if b.GetFinished() == 0 { + // return the duration in a human-readable form by + // subtracting the build started time from the + // current time rounded to the nearest second + return time.Since(started).Round(time.Second).String() + } + + // capture finished unix timestamp from the build + finished := time.Unix(b.GetFinished(), 0) + + // calculate the duration by subtracting the build + // started time from the build finished time + duration := finished.Sub(started) + + // return the duration in a human-readable form + return duration.String() +} + +// GetID returns the ID field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetID() int64 { + // return zero value if TestRun type or ID field is nil + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// SetID sets the ID field. +func (b *TestRun) SetID(v int64) { + if b == nil { + return + } + b.ID = &v +} + +// GetTestRunSystemAttributes returns the TestRunSystemAttributes field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetTestRunSystemAttributes() TestRunSystemAttributes { + // return zero value if TestRun type or TestRunSystemAttributes field is nil + if b == nil || b.TestRunSystemAttributes == nil { + return TestRunSystemAttributes{} + } + return *b.TestRunSystemAttributes +} + +// SetTestRunSystemAttributes sets the TestRunSystemAttributes field. +func (b *TestRun) SetTestRunSystemAttributes(v TestRunSystemAttributes) { + if b == nil { + return + } + b.TestRunSystemAttributes = &v +} + +// GetTotalTestCount returns the TotalTestCount field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetTotalTestCount() int { + // return zero value if TestRun type or TotalTestCount field is nil + if b == nil || b.TotalTestCount == nil { + return 0 + } + return *b.TotalTestCount +} + +// SetTotalTestCount sets the TotalTestCount field. +func (b *TestRun) SetTotalTestCount(v int) { + if b == nil { + return + } + b.TotalTestCount = &v +} + +// GetTotalPassingCount returns the TotalPassingCount field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetTotalPassingCount() int { + // return zero value if TestRun type or TotalPassingCount field is nil + if b == nil || b.TotalPassingCount == nil { + return 0 + } + return *b.TotalPassingCount +} + +// SetTotalPassingCount sets the TotalPassingCount field. +func (b *TestRun) SetTotalPassingCount(v int) { + if b == nil { + return + } + b.TotalPassingCount = &v +} + +// GetTotalSkipCount returns the TotalSkipCount field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetTotalSkipCount() int { + // return zero value if TestRun type or TotalSkipCount field is nil + if b == nil || b.TotalSkipCount == nil { + return 0 + } + return *b.TotalSkipCount +} + +// SetTotalSkipCount sets the TotalSkipCount field. +func (b *TestRun) SetTotalSkipCount(v int) { + if b == nil { + return + } + b.TotalSkipCount = &v +} + +// GetTotalFailureCount returns the TotalFailureCount field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetTotalFailureCount() int { + // return zero value if TestRun type or TotalFailureCount field is nil + if b == nil || b.TotalFailureCount == nil { + return 0 + } + return *b.TotalFailureCount +} + +// SetTotalFailureCount sets the TotalFailureCount field. +func (b *TestRun) SetTotalFailureCount(v int) { + if b == nil { + return + } + b.TotalFailureCount = &v +} + +// GetPassed returns the Passed field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetPassed() bool { + // return zero value if TestRun type or Passed field is nil + if b == nil || b.Passed == nil { + return false + } + return *b.Passed +} + +// SetPassed sets the Passed field. +func (b *TestRun) SetPassed(v bool) { + if b == nil { + return + } + b.Passed = &v +} + +// GetCreated returns the Created field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetCreated() int64 { + // return zero value if TestRun type or Created field is nil + if b == nil || b.Created == nil { + return 0 + } + return *b.Created +} + +// SetCreated sets the Created field. +func (b *TestRun) SetCreated(v int64) { + if b == nil { + return + } + b.Created = &v +} + +// GetStarted returns the Started field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetStarted() int64 { + // return zero value if TestRun type or Started field is nil + if b == nil || b.Started == nil { + return 0 + } + return *b.Started +} + +// SetStarted sets the Started field. +func (b *TestRun) SetStarted(v int64) { + if b == nil { + return + } + b.Started = &v +} + +// GetFinished returns the Finished field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetFinished() int64 { + // return zero value if TestRun type or Finished field is nil + if b == nil || b.Finished == nil { + return 0 + } + return *b.Finished +} + +// SetFinished sets the Finished field. +func (b *TestRun) SetFinished(v int64) { + if b == nil { + return + } + b.Finished = &v +} + +// GetAverageDuration returns the AverageDuration field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetAverageDuration() int64 { + // return zero value if TestRun type or AverageDuration field is nil + if b == nil || b.AverageDuration == nil { + return 0 + } + return *b.AverageDuration +} + +// SetAverageDuration sets the AverageDuration field. +func (b *TestRun) SetAverageDuration(v int64) { + if b == nil { + return + } + b.AverageDuration = &v +} + +// GetSlowestTestCaseDuration returns the SlowestTestCaseDuration field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetSlowestTestCaseDuration() int64 { + // return zero value if TestRun type or SlowestTestCaseDuration field is nil + if b == nil || b.SlowestTestCaseDuration == nil { + return 0 + } + return *b.SlowestTestCaseDuration +} + +// SetSlowestTestCaseDuration sets the SlowestTestCaseDuration field. +func (b *TestRun) SetSlowestTestCaseDuration(v int64) { + if b == nil { + return + } + b.SlowestTestCaseDuration = &v +} + +// GetWallClockDuration returns the WallClockDuration field. +// +// When the provided TestRun type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRun) GetWallClockDuration() int64 { + // return zero value if TestRun type or WallClockDuration field is nil + if b == nil || b.WallClockDuration == nil { + return 0 + } + return *b.WallClockDuration +} + +// SetWallClockDuration sets the WallClockDuration field. +func (b *TestRun) SetWallClockDuration(v int64) { + if b == nil { + return + } + b.WallClockDuration = &v +} diff --git a/api/types/test-report/test_run_attachment.go b/api/types/test-report/test_run_attachment.go new file mode 100644 index 000000000..c5ca02d7e --- /dev/null +++ b/api/types/test-report/test_run_attachment.go @@ -0,0 +1,11 @@ +package test_report + +// TestRunAttachment is the API types representation of an attachment for a test run. +// +// swagger:model TestRunAttachment +type TestRunAttachment struct { + ID *int64 `json:"id,omitempty"` + FileName *string `json:"file_name,omitempty"` + ObjectName *string `json:"object_name,omitempty"` + FileSize *int64 `json:"file_size,omitempty"` +} diff --git a/api/types/test-report/test_run_system_attributes.go b/api/types/test-report/test_run_system_attributes.go new file mode 100644 index 000000000..6efb5ad9a --- /dev/null +++ b/api/types/test-report/test_run_system_attributes.go @@ -0,0 +1,47 @@ +package test_report + +// TestRunSystemAttributes is the API types representation of system attributes for a test run. +// +// swagger:model TestRunSystemAttributes +type TestRunSystemAttributes struct { + ID *int64 `json:"id,omitempty"` + Pinned *bool `json:"pinned,omitempty"` +} + +// GetID returns the ID field. +// +// When the provided TestRunSystemAttributes type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRunSystemAttributes) GetID() int64 { + if b == nil || b.ID == nil { + return 0 + } + return *b.ID +} + +// SetID sets the ID field. +func (b *TestRunSystemAttributes) SetID(v int64) { + if b == nil { + return + } + b.ID = &v +} + +// GetPinned returns the Pinned field. +// +// When the provided TestRunSystemAttributes type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (b *TestRunSystemAttributes) GetPinned() bool { + if b == nil || b.Pinned == nil { + return false + } + return *b.Pinned +} + +// SetPinned sets the Pinned field. +func (b *TestRunSystemAttributes) SetPinned(v bool) { + if b == nil { + return + } + b.Pinned = &v +} diff --git a/api/types/test-report/test_suite.go b/api/types/test-report/test_suite.go new file mode 100644 index 000000000..6ad451f48 --- /dev/null +++ b/api/types/test-report/test_suite.go @@ -0,0 +1,552 @@ +package test_report + +import "time" + +// TestSuite is the API types representation of a test suite for a test run. +// +// swagger:model TestSuite +type TestSuite struct { + ID *int64 `json:"id,omitempty"` + TestRun *TestRun `json:"test_run,omitempty"` + TestSuiteGroup *TestSuiteGroup `json:"test_suite_group,omitempty"` + Idx *int64 `json:"idx,omitempty"` + PackageName *string `json:"package_name,omitempty"` + ClassName *string `json:"class_name,omitempty"` + TestCount *int64 `json:"test_count,omitempty"` + PassingCount *int64 `json:"passing_count,omitempty"` + SkippedCount *int64 `json:"skipped_count,omitempty"` + FailureCount *int64 `json:"failure_count,omitempty"` + StartTs *int64 `json:"start_ts,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Started *int64 `json:"started,omitempty"` + Finished *int64 `json:"finished,omitempty"` + SystemOut *string `json:"system_out,omitempty"` + SystemErr *string `json:"system_err,omitempty"` + HasSystemOut *bool `json:"has_system_out,omitempty"` + HasSystemErr *bool `json:"has_system_err,omitempty"` + FileName *string `json:"file_name,omitempty"` +} + +// Duration calculates and returns the total amount of +// time the test suite ran for in a human-readable format. +func (t *TestSuite) Duration() string { + // check if the test suite doesn't have a started timestamp + if t.GetStarted() == 0 { + return "..." + } + + // capture started unix timestamp from the test suite + started := time.Unix(t.GetStarted(), 0) + + // check if the test suite doesn't have a finished timestamp + if t.GetFinished() == 0 { + // return the duration in a human-readable form by + // subtracting the test suite started time from the + // current time rounded to the nearest second + return time.Since(started).Round(time.Second).String() + } + + // capture finished unix timestamp from the test suite + finished := time.Unix(t.GetFinished(), 0) + + // calculate the duration by subtracting the test suite + // started time from the test suite finished time + duration := finished.Sub(started) + + // return the duration in a human-readable form + return duration.String() +} + +// SetID sets the ID field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetID(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.ID = &v +} + +// GetID returns the ID field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetID() int64 { + // return zero value if TestSuite type or ID field is nil + if t == nil || t.ID == nil { + return 0 + } + + return *t.ID +} + +// SetTestRun sets the TestRun field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetTestRun(v TestRun) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.TestRun = &v +} + +// GetTestRun returns the TestRun field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetTestRun() TestRun { + // return zero value if TestSuite type or TestRun field is nil + if t == nil || t.TestRun == nil { + return TestRun{} + } + + return *t.TestRun +} + +// SetTestSuiteGroup sets the TestSuiteGroup field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetTestSuiteGroup(v TestSuiteGroup) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.TestSuiteGroup = &v +} + +// GetTestSuiteGroup returns the TestSuiteGroup field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetTestSuiteGroup() TestSuiteGroup { + // return zero value if TestSuite type or TestSuiteGroup field is nil + if t == nil || t.TestSuiteGroup == nil { + return TestSuiteGroup{} + } + + return *t.TestSuiteGroup +} + +// SetIdx sets the Idx field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetIdx(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.Idx = &v +} + +// GetIdx returns the Idx field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetIdx() int64 { + // return zero value if TestSuite type or Idx field is nil + if t == nil || t.Idx == nil { + return 0 + } + + return *t.Idx +} + +// SetPackageName sets the PackageName field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetPackageName(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.PackageName = &v +} + +// GetPackageName returns the PackageName field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetPackageName() string { + // return zero value if TestSuite type or PackageName field is nil + if t == nil || t.PackageName == nil { + return "" + } + + return *t.PackageName +} + +// SetClassName sets the ClassName field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetClassName(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.ClassName = &v +} + +// GetClassName returns the ClassName field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetClassName() string { + // return zero value if TestSuite type or ClassName field is nil + if t == nil || t.ClassName == nil { + return "" + } + + return *t.ClassName +} + +// SetTestCount sets the TestCount field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetTestCount(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.TestCount = &v +} + +// GetTestCount returns the TestCount field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetTestCount() int64 { + // return zero value if TestSuite type or TestCount field is nil + if t == nil || t.TestCount == nil { + return 0 + } + + return *t.TestCount +} + +// SetPassingCount sets the PassingCount field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetPassingCount(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.PassingCount = &v +} + +// GetPassingCount returns the PassingCount field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetPassingCount() int64 { + // return zero value if TestSuite type or PassingCount field is nil + if t == nil || t.PassingCount == nil { + return 0 + } + + return *t.PassingCount +} + +// SetSkippedCount sets the SkippedCount field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetSkippedCount(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.SkippedCount = &v +} + +// GetSkippedCount returns the SkippedCount field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetSkippedCount() int64 { + // return zero value if TestSuite type or SkippedCount field is nil + if t == nil || t.SkippedCount == nil { + return 0 + } + + return *t.SkippedCount +} + +// SetFailureCount sets the FailureCount field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetFailureCount(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.FailureCount = &v +} + +// GetFailureCount returns the FailureCount field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetFailureCount() int64 { + // return zero value if TestSuite type or FailureCount field is nil + if t == nil || t.FailureCount == nil { + return 0 + } + + return *t.FailureCount +} + +// SetStartTs sets the StartTs field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetStartTs(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.StartTs = &v +} + +// GetStartTs returns the StartTs field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetStartTs() int64 { + // return zero value if TestSuite type or StartTs field is nil + if t == nil || t.StartTs == nil { + return 0 + } + + return *t.StartTs +} + +// SetHostname sets the Hostname field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetHostname(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.Hostname = &v +} + +// GetHostname returns the Hostname field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetHostname() string { + // return zero value if TestSuite type or Hostname field is nil + if t == nil || t.Hostname == nil { + return "" + } + + return *t.Hostname +} + +// SetStarted sets the Started field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetStarted(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.Started = &v +} + +// GetStarted returns the Started field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetStarted() int64 { + // return zero value if TestSuite type or Started field is nil + if t == nil || t.Started == nil { + return 0 + } + + return *t.Started +} + +// SetFinished sets the Finished field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetFinished(v int64) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.Finished = &v +} + +// GetFinished returns the Finished field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetFinished() int64 { + // return zero value if TestSuite type or Finished field is nil + if t == nil || t.Finished == nil { + return 0 + } + + return *t.Finished +} + +// SetSystemOut sets the SystemOut field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetSystemOut(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.SystemOut = &v +} + +// GetSystemOut returns the SystemOut field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetSystemOut() string { + // return zero value if TestSuite type or SystemOut field is nil + if t == nil || t.SystemOut == nil { + return "" + } + + return *t.SystemOut +} + +// SetSystemErr sets the SystemErr field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetSystemErr(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.SystemErr = &v +} + +// GetSystemErr returns the SystemErr field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetSystemErr() string { + // return zero value if TestSuite type or SystemErr field is nil + if t == nil || t.SystemErr == nil { + return "" + } + + return *t.SystemErr +} + +// SetHasSystemOut sets the HasSystemOut field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetHasSystemOut(v bool) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.HasSystemOut = &v +} + +// GetHasSystemOut returns the HasSystemOut field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetHasSystemOut() bool { + // return zero value if TestSuite type or HasSystemOut field is nil + if t == nil || t.HasSystemOut == nil { + return false + } + + return *t.HasSystemOut +} + +// SetHasSystemErr sets the HasSystemErr field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetHasSystemErr(v bool) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.HasSystemErr = &v +} + +// GetHasSystemErr returns the HasSystemErr field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetHasSystemErr() bool { + // return zero value if TestSuite type or HasSystemErr field is nil + if t == nil || t.HasSystemErr == nil { + return false + } + + return *t.HasSystemErr +} + +// SetFileName sets the FileName field. +// +// When the provided TestSuite type is nil, it +// will set nothing and immediately return. +func (t *TestSuite) SetFileName(v string) { + // return if TestSuite type is nil + if t == nil { + return + } + + t.FileName = &v +} + +// GetFileName returns the FileName field. +// +// When the provided TestSuite type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuite) GetFileName() string { + // return zero value if TestSuite type or FileName field is nil + if t == nil || t.FileName == nil { + return "" + } + + return *t.FileName +} diff --git a/api/types/test-report/test_suite_group.go b/api/types/test-report/test_suite_group.go new file mode 100644 index 000000000..78ccc1d8f --- /dev/null +++ b/api/types/test-report/test_suite_group.go @@ -0,0 +1,142 @@ +package test_report + +// TestSuiteGroup is the API types representation of a group of test suites for a test run. +// +// swagger:model TestSuiteGroup +type TestSuiteGroup struct { + ID *int64 `json:"id,omitempty"` + TestRun *TestRun `json:"test_run,omitempty"` + GroupName *string `json:"group_name,omitempty"` + GroupLabel *string `json:"group_label,omitempty"` + Directory *string `json:"directory,omitempty"` +} + +// SetID sets the ID field. +// +// When the provided TestSuiteGroup type is nil, it +// will set nothing and immediately return. +func (t *TestSuiteGroup) SetID(v int64) { + // return if TestSuiteGroup type is nil + if t == nil { + return + } + + t.ID = &v +} + +// GetID returns the ID field. +// +// When the provided TestSuiteGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuiteGroup) GetID() int64 { + // return zero value if TestSuiteGroup type or ID field is nil + if t == nil || t.ID == nil { + return 0 + } + + return *t.ID +} + +// SetTestRun sets the TestRun field. +// +// When the provided TestSuiteGroup type is nil, it +// will set nothing and immediately return. +func (t *TestSuiteGroup) SetTestRun(v TestRun) { + // return if TestSuiteGroup type is nil + if t == nil { + return + } + + t.TestRun = &v +} + +// GetTestRun returns the TestRun field. +// +// When the provided TestSuiteGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuiteGroup) GetTestRun() TestRun { + // return zero value if TestSuiteGroup type or TestRun field is nil + if t == nil || t.TestRun == nil { + return TestRun{} + } + + return *t.TestRun +} + +// SetGroupName sets the GroupName field. +// +// When the provided TestSuiteGroup type is nil, it +// will set nothing and immediately return. +func (t *TestSuiteGroup) SetGroupName(v string) { + // return if TestSuiteGroup type is nil + if t == nil { + return + } + + t.GroupName = &v +} + +// GetGroupName returns the GroupName field. +// +// When the provided TestSuiteGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuiteGroup) GetGroupName() string { + // return zero value if TestSuiteGroup type or GroupName field is nil + if t == nil || t.GroupName == nil { + return "" + } + + return *t.GroupName +} + +// SetGroupLabel sets the GroupLabel field. +// +// When the provided TestSuiteGroup type is nil, it +// will set nothing and immediately return. +func (t *TestSuiteGroup) SetGroupLabel(v string) { + // return if TestSuiteGroup type is nil + if t == nil { + return + } + + t.GroupLabel = &v +} + +// GetGroupLabel returns the GroupLabel field. +// +// When the provided TestSuiteGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuiteGroup) GetGroupLabel() string { + // return zero value if TestSuiteGroup type or GroupLabel field is nil + if t == nil || t.GroupLabel == nil { + return "" + } + + return *t.GroupLabel +} + +// SetDirectory sets the Directory field. +// +// When the provided TestSuiteGroup type is nil, it +// will set nothing and immediately return. +func (t *TestSuiteGroup) SetDirectory(v string) { + // return if TestSuiteGroup type is nil + if t == nil { + return + } + + t.Directory = &v +} + +// GetDirectory returns the Directory field. +// +// When the provided TestSuiteGroup type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestSuiteGroup) GetDirectory() string { + // return zero value if TestSuiteGroup type or Directory field is nil + if t == nil || t.Directory == nil { + return "" + } + + return *t.Directory +} diff --git a/api/types/test_report.go b/api/types/test_report.go new file mode 100644 index 000000000..a6cb7c7da --- /dev/null +++ b/api/types/test_report.go @@ -0,0 +1,62 @@ +package types + +import "fmt" + +// TestReport is the API representation of a test report for a pipeline. +// +// swagger:model TestReport +type TestReport struct { + Results *string `json:"results,omitempty"` + Attachments *string `json:"attachments,omitempty"` +} + +// GetResults returns the Results field. +// +// When the provided TestReport type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestReport) GetResults() string { + // return zero value if TestReport type or Results field is nil + if t == nil || t.Results == nil { + return "" + } + + return *t.Results +} + +// GetAttachments returns the Attachments field. +// +// When the provided TestReport type is nil, or the field within +// the type is nil, it returns the zero value for the field. +func (t *TestReport) GetAttachments() string { + // return zero value if TestReport type or Attachments field is nil + if t == nil || t.Attachments == nil { + return "" + } + + return *t.Attachments +} + +// SetResults sets the Results field. +func (t *TestReport) SetResults(v string) { + // return if TestReport type is nil + if t == nil { + return + } + // set the Results field + t.Results = &v +} + +// SetAttachments sets the Attachments field. +func (t *TestReport) SetAttachments(v string) { + // return if TestReport type is nil + if t == nil { + return + } + // set the Attachments field + t.Attachments = &v +} + +// String implements the Stringer interface for the TestReport type. +func (t *TestReport) String() string { + return fmt.Sprintf("Results: %s, Attachments: %s", t.GetResults(), t.GetAttachments()) +} diff --git a/cmd/vela-server/main.go b/cmd/vela-server/main.go index 128401bbc..864c8fce6 100644 --- a/cmd/vela-server/main.go +++ b/cmd/vela-server/main.go @@ -5,6 +5,7 @@ package main import ( "encoding/json" "fmt" + "github.com/go-vela/server/storage" "os" "time" @@ -289,6 +290,9 @@ func main() { // Add Tracing Flags app.Flags = append(app.Flags, tracing.Flags...) + // Add S3 Flags + app.Flags = append(app.Flags, storage.Flags...) + if err = app.Run(os.Args); err != nil { logrus.Fatal(err) } diff --git a/cmd/vela-server/metadata.go b/cmd/vela-server/metadata.go index 11da44af4..b09d34ed0 100644 --- a/cmd/vela-server/metadata.go +++ b/cmd/vela-server/metadata.go @@ -45,6 +45,13 @@ func setupMetadata(c *cli.Context) (*internal.Metadata, error) { m.Vela = vela + storage, err := metadataStorage(c) + if err != nil { + return nil, err + } + + m.Storage = storage + return m, nil } @@ -93,6 +100,21 @@ func metadataSource(c *cli.Context) (*internal.Source, error) { }, nil } +// helper function to capture the queue metadata from the CLI arguments. +func metadataStorage(c *cli.Context) (*internal.Storage, error) { + logrus.Trace("creating storage metadata from CLI configuration") + + u, err := url.Parse(c.String("storage.addr")) + if err != nil { + return nil, err + } + + return &internal.Storage{ + Driver: c.String("storage.driver"), + Host: u.Host, + }, nil +} + // helper function to capture the Vela metadata from the CLI arguments. // //nolint:unparam // ignore unparam for now diff --git a/cmd/vela-server/server.go b/cmd/vela-server/server.go index ebed088f0..ebca360dd 100644 --- a/cmd/vela-server/server.go +++ b/cmd/vela-server/server.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "github.com/go-vela/server/storage" "net/http" "net/url" "os" @@ -113,6 +114,11 @@ func server(c *cli.Context) error { return err } + st, err := storage.FromCLIContext(c) + if err != nil { + return err + } + metadata, err := setupMetadata(c) if err != nil { return err @@ -187,10 +193,12 @@ func server(c *cli.Context) error { middleware.Metadata(metadata), middleware.TokenManager(tm), middleware.Queue(queue), + middleware.Storage(st), middleware.RequestVersion, middleware.Secret(c.String("vela-secret")), middleware.Secrets(secrets), middleware.Scm(scm), + middleware.Storage(st), middleware.QueueSigningPrivateKey(c.String("queue.private-key")), middleware.QueueSigningPublicKey(c.String("queue.public-key")), middleware.QueueAddress(c.String("queue.addr")), @@ -207,6 +215,9 @@ func server(c *cli.Context) error { middleware.ScheduleFrequency(c.Duration("schedule-minimum-frequency")), middleware.TracingClient(tc), middleware.TracingInstrumentation(tc), + middleware.StorageAddress(c.String("storage.addr")), + middleware.StorageAccessKey(c.String("storage.access.key")), + middleware.StorageSecretKey(c.String("storage.secret.key")), ) addr, err := url.Parse(c.String("server-addr")) diff --git a/compiler/native/validate.go b/compiler/native/validate.go index 77a8578f2..ca83442b3 100644 --- a/compiler/native/validate.go +++ b/compiler/native/validate.go @@ -152,7 +152,9 @@ func validateSteps(s yaml.StepSlice) error { if len(step.Commands) == 0 && len(step.Environment) == 0 && len(step.Parameters) == 0 && len(step.Secrets) == 0 && - len(step.Template.Name) == 0 && !step.Detach { + len(step.Template.Name) == 0 && len(step.TestReport.Results) == 0 && + len(step.TestReport.Attachments) == 0 && !step.Detach { + return fmt.Errorf("no commands, environment, parameters, secrets or template provided for step %s", step.Name) } } diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index a5771e953..016173527 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -624,3 +624,38 @@ func TestNative_Validate_MultiReportAs(t *testing.T) { t.Errorf("Validate should have returned err") } } + +func TestNative_Validate_TestReport(t *testing.T) { + // setup types + set := flag.NewFlagSet("test", 0) + set.String("clone-image", defaultCloneImage, "doc") + c := cli.NewContext(nil, set, nil) + + str := "foo" + p := &yaml.Build{ + Version: "v1", + Steps: yaml.StepSlice{ + &yaml.Step{ + Commands: raw.StringSlice{"echo hello"}, + Image: "alpine", + Name: str, + Pull: "always", + //TestReport: yaml.TestReport{ + // Results: []string{"results.xml"}, + // Attachments: []string{"attachments"}, + //}, + }, + }, + } + + // run test + compiler, err := FromCLIContext(c) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + err = compiler.Validate(p) + if err != nil { + t.Errorf("Validate returned err: %v", err) + } +} diff --git a/compiler/types/pipeline/container.go b/compiler/types/pipeline/container.go index 5cfb2acf9..0c06fb691 100644 --- a/compiler/types/pipeline/container.go +++ b/compiler/types/pipeline/container.go @@ -48,6 +48,7 @@ type ( Pull string `json:"pull,omitempty" yaml:"pull,omitempty"` Ruleset Ruleset `json:"ruleset,omitempty" yaml:"ruleset,omitempty"` Secrets StepSecretSlice `json:"secrets,omitempty" yaml:"secrets,omitempty"` + TestReport TestReport `json:"test_report,omitempty" yaml:"test_report,omitempty"` Ulimits UlimitSlice `json:"ulimits,omitempty" yaml:"ulimits,omitempty"` Volumes VolumeSlice `json:"volumes,omitempty" yaml:"volumes,omitempty"` User string `json:"user,omitempty" yaml:"user,omitempty"` @@ -137,7 +138,8 @@ func (c *Container) Empty() bool { len(c.Volumes) == 0 && len(c.User) == 0 && len(c.ReportAs) == 0 && - len(c.IDRequest) == 0 { + len(c.IDRequest) == 0 && + reflect.DeepEqual(c.TestReport, TestReport{}) { return true } diff --git a/compiler/types/pipeline/test_report.go b/compiler/types/pipeline/test_report.go new file mode 100644 index 000000000..bd4f81fd4 --- /dev/null +++ b/compiler/types/pipeline/test_report.go @@ -0,0 +1,54 @@ +package pipeline + +// TestReport represents the structure for test report configuration. +type ( + // TestReportSlice is the pipleine representation + //of a slice of TestReport. + // + // swagger:model PipelineTestReportSlice + TestReportSlice []*TestReport + + // TestReport is the pipeline representation + // of a test report for a pipeline. + // + // swagger:model PipelineTestReport + TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + } +) + +// Purge removes the test report configuration from the pipeline +// if it does not match the provided ruledata. If both results +// and attachments are provided, then an empty test report is returned. +//func (t *TestReport) Purge(r *RuleData) (*TestReport, error) { +// // return an empty test report if both results and attachments are provided +// if len(t.Results) > 0 && len(t.Attachments) > 0 { +// return nil, fmt.Errorf("cannot have both results and attachments in the test report") +// } +// +// // purge results if provided +// if len(t.Results) > 0 { +// t.Results = "" +// } +// +// // purge attachments if provided +// if len(t.Attachments) > 0 { +// t.Attachments = "" +// } +// +// // return the purged test report +// return t, nil +//} + +// Empty returns true if the provided test report is empty. +func (t *TestReport) Empty() bool { + // return true if every test report field is empty + if len(t.Results) == 0 && + len(t.Attachments) == 0 { + return true + } + + // return false if any of the test report fields are provided + return false +} diff --git a/compiler/types/pipeline/test_report_test.go b/compiler/types/pipeline/test_report_test.go new file mode 100644 index 000000000..0e3db067c --- /dev/null +++ b/compiler/types/pipeline/test_report_test.go @@ -0,0 +1,29 @@ +package pipeline + +import "testing" + +func TestPipeline_TestReport_Empty(t *testing.T) { + // setup tests + tests := []struct { + report *TestReport + want bool + }{ + { + report: &TestReport{Results: []string{"foo"}}, + want: false, + }, + { + report: new(TestReport), + want: true, + }, + } + + // run tests + for _, test := range tests { + got := test.report.Empty() + + if got != test.want { + t.Errorf("Empty is %v, want %t", got, test.want) + } + } +} diff --git a/compiler/types/yaml/buildkite/step.go b/compiler/types/yaml/buildkite/step.go index 4b36b93fd..7389c0754 100644 --- a/compiler/types/yaml/buildkite/step.go +++ b/compiler/types/yaml/buildkite/step.go @@ -24,6 +24,7 @@ type ( Commands raw.StringSlice `yaml:"commands,omitempty" json:"commands,omitempty" jsonschema:"description=Execution instructions to run inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-commands-key"` Entrypoint raw.StringSlice `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty" jsonschema:"description=Command to execute inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-entrypoint-key"` Secrets StepSecretSlice `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"description=Sensitive variables injected into the container environment.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-secrets-key"` + TestReport TestReport `yaml:"test_report,omitempty" json:"test_report,omitempty" jsonschema:"description=Test report configuration for the step.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-test_report-key"` Template StepTemplate `yaml:"template,omitempty" json:"template,omitempty" jsonschema:"oneof_required=template,description=Name of template to expand in the pipeline.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-template-key"` Ulimits UlimitSlice `yaml:"ulimits,omitempty" json:"ulimits,omitempty" jsonschema:"description=Set the user limits for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ulimits-key"` Volumes VolumeSlice `yaml:"volumes,omitempty" json:"volumes,omitempty" jsonschema:"description=Mount volumes for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-volume-key"` @@ -60,6 +61,7 @@ func (s *StepSlice) ToPipeline() *pipeline.ContainerSlice { Pull: step.Pull, Ruleset: *step.Ruleset.ToPipeline(), Secrets: *step.Secrets.ToPipeline(), + TestReport: *step.TestReport.ToPipeline(), Ulimits: *step.Ulimits.ToPipeline(), Volumes: *step.Volumes.ToPipeline(), User: step.User, @@ -165,12 +167,13 @@ func (s *Step) ToYAML() *yaml.Step { Ruleset: *s.Ruleset.ToYAML(), Secrets: *s.Secrets.ToYAML(), Template: s.Template.ToYAML(), - Ulimits: *s.Ulimits.ToYAML(), - Volumes: *s.Volumes.ToYAML(), - Parameters: s.Parameters, - User: s.User, - ReportAs: s.ReportAs, - IDRequest: s.IDRequest, + //TestReport: s.TestReport.ToYAML(), + Ulimits: *s.Ulimits.ToYAML(), + Volumes: *s.Volumes.ToYAML(), + Parameters: s.Parameters, + User: s.User, + ReportAs: s.ReportAs, + IDRequest: s.IDRequest, } } diff --git a/compiler/types/yaml/buildkite/step_test.go b/compiler/types/yaml/buildkite/step_test.go index da4df26fe..18479c1b1 100644 --- a/compiler/types/yaml/buildkite/step_test.go +++ b/compiler/types/yaml/buildkite/step_test.go @@ -76,6 +76,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, want: &pipeline.ContainerSlice{ @@ -134,6 +138,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: pipeline.TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, }, @@ -213,6 +221,15 @@ func TestYaml_StepSlice_UnmarshalYAML(t *testing.T) { }, }, }, + { + Name: "test-reports", + Pull: "always", + Image: "golang:1.20", + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, + }, }, }, { diff --git a/compiler/types/yaml/buildkite/test_report.go b/compiler/types/yaml/buildkite/test_report.go new file mode 100644 index 000000000..0de529da0 --- /dev/null +++ b/compiler/types/yaml/buildkite/test_report.go @@ -0,0 +1,50 @@ +package buildkite + +import ( + "github.com/go-vela/server/compiler/types/pipeline" + "github.com/go-vela/server/compiler/types/yaml/yaml" +) + +// TestReport represents the structure for test report configuration. +type TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` +} + +// ToPipeline converts the TestReport type +// to a pipeline TestReport type. +func (t *TestReport) ToPipeline() *pipeline.TestReport { + return &pipeline.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} + +// UnmarshalYAML implements the Unmarshaler interface for the TestReport type. +func (t *TestReport) UnmarshalYAML(unmarshal func(interface{}) error) error { + // test report we try unmarshalling to + testReport := new(struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + }) + + // attempt to unmarshal test report type + err := unmarshal(testReport) + if err != nil { + return err + } + + // set the results field + t.Results = testReport.Results + // set the attachments field + t.Attachments = testReport.Attachments + + return nil +} + +func (t *TestReport) ToYAML() yaml.TestReport { + return yaml.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} diff --git a/compiler/types/yaml/buildkite/testdata/step.yml b/compiler/types/yaml/buildkite/testdata/step.yml index 1d6d9cc93..1e15dd239 100644 --- a/compiler/types/yaml/buildkite/testdata/step.yml +++ b/compiler/types/yaml/buildkite/testdata/step.yml @@ -44,3 +44,10 @@ registry: index.docker.io repo: github/octocat tags: [ latest, dev ] + +- name: test-reports + image: golang:1.20 + pull: true + test_report: + results: ["test-results/*.xml"] + attachments: ["screenshots/**/*.png", " video/*.mp4"] \ No newline at end of file diff --git a/compiler/types/yaml/yaml/secret.go b/compiler/types/yaml/yaml/secret.go index 97b836a09..efb0b86a9 100644 --- a/compiler/types/yaml/yaml/secret.go +++ b/compiler/types/yaml/yaml/secret.go @@ -157,7 +157,7 @@ func (o *Origin) Empty() bool { // MergeEnv takes a list of environment variables and attempts // to set them in the secret environment. If the environment -// variable already exists in the secret, than this will +// variable already exists in the secret, then this will // overwrite the existing environment variable. func (o *Origin) MergeEnv(environment map[string]string) error { // check if the secret container is empty diff --git a/compiler/types/yaml/yaml/step.go b/compiler/types/yaml/yaml/step.go index 405e6d598..8289520b3 100644 --- a/compiler/types/yaml/yaml/step.go +++ b/compiler/types/yaml/yaml/step.go @@ -24,6 +24,7 @@ type ( Entrypoint raw.StringSlice `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty" jsonschema:"description=Command to execute inside the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-entrypoint-key"` Secrets StepSecretSlice `yaml:"secrets,omitempty" json:"secrets,omitempty" jsonschema:"description=Sensitive variables injected into the container environment.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-secrets-key"` Template StepTemplate `yaml:"template,omitempty" json:"template,omitempty" jsonschema:"oneof_required=template,description=Name of template to expand in the pipeline.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-template-key"` + TestReport TestReport `yaml:"test_report,omitempty" json:"test_report,omitempty" jsonschema:"description=Test report configuration for the step.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-test_report-key"` Ulimits UlimitSlice `yaml:"ulimits,omitempty" json:"ulimits,omitempty" jsonschema:"description=Set the user limits for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ulimits-key"` Volumes VolumeSlice `yaml:"volumes,omitempty" json:"volumes,omitempty" jsonschema:"description=Mount volumes for the container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-volume-key"` Image string `yaml:"image,omitempty" json:"image,omitempty" jsonschema:"oneof_required=image,minLength=1,description=Docker image to use to create the ephemeral container.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-image-key"` @@ -59,6 +60,7 @@ func (s *StepSlice) ToPipeline() *pipeline.ContainerSlice { Pull: step.Pull, Ruleset: *step.Ruleset.ToPipeline(), Secrets: *step.Secrets.ToPipeline(), + TestReport: *step.TestReport.ToPipeline(), Ulimits: *step.Ulimits.ToPipeline(), Volumes: *step.Volumes.ToPipeline(), User: step.User, diff --git a/compiler/types/yaml/yaml/step_test.go b/compiler/types/yaml/yaml/step_test.go index 343daf230..dabbc7c3f 100644 --- a/compiler/types/yaml/yaml/step_test.go +++ b/compiler/types/yaml/yaml/step_test.go @@ -76,6 +76,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, want: &pipeline.ContainerSlice{ @@ -134,6 +138,10 @@ func TestYaml_StepSlice_ToPipeline(t *testing.T) { AccessMode: "ro", }, }, + TestReport: pipeline.TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, }, }, }, @@ -213,6 +221,15 @@ func TestYaml_StepSlice_UnmarshalYAML(t *testing.T) { }, }, }, + { + Name: "test-reports", + Image: "golang:1.20", + Pull: "always", + TestReport: TestReport{ + Results: []string{"test-results/*.xml"}, + Attachments: []string{"screenshots/**/*.png", " video/*.mp4"}, + }, + }, }, }, { diff --git a/compiler/types/yaml/yaml/test_report.go b/compiler/types/yaml/yaml/test_report.go new file mode 100644 index 000000000..038db367c --- /dev/null +++ b/compiler/types/yaml/yaml/test_report.go @@ -0,0 +1,40 @@ +package yaml + +import "github.com/go-vela/server/compiler/types/pipeline" + +// TestReport represents the structure for test report configuration. +type TestReport struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` +} + +// ToPipeline converts the TestReport type +// to a pipeline TestReport type. +func (t *TestReport) ToPipeline() *pipeline.TestReport { + return &pipeline.TestReport{ + Results: t.Results, + Attachments: t.Attachments, + } +} + +// UnmarshalYAML implements the Unmarshaler interface for the TestReport type. +func (t *TestReport) UnmarshalYAML(unmarshal func(interface{}) error) error { + // test report we try unmarshalling to + testReport := new(struct { + Results []string `yaml:"results,omitempty" json:"results,omitempty"` + Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"` + }) + + // attempt to unmarshal test report type + err := unmarshal(testReport) + if err != nil { + return err + } + + // set the results field + t.Results = testReport.Results + // set the attachments field + t.Attachments = testReport.Attachments + + return nil +} diff --git a/compiler/types/yaml/yaml/testdata/step.yml b/compiler/types/yaml/yaml/testdata/step.yml index 1d6d9cc93..c0091f398 100644 --- a/compiler/types/yaml/yaml/testdata/step.yml +++ b/compiler/types/yaml/yaml/testdata/step.yml @@ -44,3 +44,10 @@ registry: index.docker.io repo: github/octocat tags: [ latest, dev ] + +- name: test-reports + image: golang:1.20 + pull: true + test_report: + results: ["test-results/*.xml"] + attachments: ["screenshots/**/*.png", " video/*.mp4"] diff --git a/constants/driver.go b/constants/driver.go index e42b924c5..fb716e435 100644 --- a/constants/driver.go +++ b/constants/driver.go @@ -62,3 +62,11 @@ const ( // DriverGitLab defines the driver type when integrating with a Gitlab source code system. DriverGitlab = "gitlab" ) + +// Server storage drivers. +const ( + // DriverMinio defines the driver type when integrating with a local storage system. + DriverMinio = "minio" + // DriverAws defines the driver type when integrating with an AWS S3 storage system. + DriverAws = "aws" +) diff --git a/docker-compose.yml b/docker-compose.yml index a3ab66ba1..8ca4fe35c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,13 @@ services: VELA_OTEL_TRACING_ENABLE: true VELA_OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318 VELA_OTEL_TRACING_SAMPLER_RATELIMIT_PER_SECOND: 100 + VELA_STORAGE_ENABLE: 'true' + VELA_STORAGE_DRIVER: minio + VELA_STORAGE_ADDRESS: "http://minio:9000" + VELA_STORAGE_ACCESS_KEY: minio_access_user + VELA_STORAGE_SECRET_KEY: minio_secret_key + VELA_STORAGE_USE_SSL: 'false' + VELA_STORAGE_BUCKET: vela env_file: - .env restart: always @@ -65,8 +72,12 @@ services: # # https://go-vela.github.io/docs/administration/worker/ worker: +# container_name: worker +# image: target/vela-worker:latest + build: + context: ../worker + dockerfile: Dockerfile container_name: worker - image: target/vela-worker:latest networks: - vela environment: @@ -83,6 +94,11 @@ services: VELA_SERVER_SECRET: 'zB7mrKDTZqNeNTD8z47yG4DHywspAh' WORKER_ADDR: 'http://worker:8080' WORKER_CHECK_IN: 2m + VELA_EXECUTOR_OUTPUTS_IMAGE: 'alpine:latest' + VELA_STORAGE_ENABLE: 'true' + VELA_STORAGE_DRIVER: minio + VELA_STORAGE_ADDRESS: "http://minio:9000" + VELA_STORAGE_BUCKET: vela restart: always ports: - '8081:8080' @@ -177,5 +193,21 @@ services: - '16686:16686' - '4318:4318' + minio: + container_name: minio + image: minio/minio:latest + ports: + - "9000:9000" + - "9001:9001" + networks: + - vela +# volumes: +# - ~./minio:/data + environment: + - MINIO_ROOT_USER=minio_access_user + - MINIO_ROOT_PASSWORD=minio_secret_key + command: minio server --address ":9000" --console-address ":9001" /data + + networks: vela: diff --git a/go.mod b/go.mod index cdf9974c5..7e1f21ecd 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,11 @@ require ( github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 github.com/distribution/reference v0.6.0 github.com/drone/envsubst v1.0.3 + github.com/dustin/go-humanize v1.0.1 github.com/ghodss/yaml v1.0.0 github.com/gin-gonic/gin v1.10.0 github.com/go-playground/assert/v2 v2.2.0 + github.com/go-vela/archiver/v3 v3.4.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v68 v68.0.0 @@ -30,11 +32,13 @@ require ( github.com/lestrrat-go/jwx/v2 v2.1.3 github.com/lib/pq v1.10.9 github.com/microcosm-cc/bluemonday v1.0.27 + github.com/minio/minio-go/v7 v7.0.83 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.5 github.com/redis/go-redis/v9 v9.7.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.12.0 + github.com/stretchr/testify v1.10.0 github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 github.com/urfave/cli/v2 v2.27.5 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.58.0 @@ -63,6 +67,7 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/andybalholm/brotli v1.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -74,12 +79,15 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -88,6 +96,7 @@ require ( github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.2 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -108,8 +117,9 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -119,6 +129,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -126,11 +137,15 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect @@ -138,9 +153,11 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.9 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index 45851ff9c..7e0381a92 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= @@ -60,6 +62,7 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -72,11 +75,17 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -89,6 +98,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -106,6 +117,10 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-vela/archiver v3.1.1+incompatible h1:xTTMMwKyHwDgNFn+c1XsKHrHFg6UyWmK4oSceSduH7A= +github.com/go-vela/archiver v3.1.1+incompatible/go.mod h1:pYn/vJ47tuVltHdnA8YrDWOZ6yix4k0ZSKUBbWmkAFM= +github.com/go-vela/archiver/v3 v3.4.0 h1:c6GQRNTzr4Pn8HaxjzslIEiN89+kgZB4hLkXuBCOczI= +github.com/go-vela/archiver/v3 v3.4.0/go.mod h1:1HbXVOHBXsfzwSog3x7T6ZU9BUv+VEWnuaPLZS/v0/8= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -114,11 +129,15 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= @@ -190,14 +209,24 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -232,6 +261,10 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.83 h1:W4Kokksvlz3OKf3OqIlzDNKd4MERlC2oN8YptwJ0+GA= +github.com/minio/minio-go/v7 v7.0.83/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -250,10 +283,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -270,8 +309,11 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -302,6 +344,9 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 h1:Jjn3zoRz13f8b1bR6LrXWglx93Sbh4kYfwgmPju3E2k= github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2/go.mod h1:wocb5pNrj/sjhWB9J5jctnC0K2eisSdz/nJJBNFHo+A= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= @@ -312,6 +357,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -399,8 +446,10 @@ google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe0 google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/metadata.go b/internal/metadata.go index e87868d4e..5b34159d2 100644 --- a/internal/metadata.go +++ b/internal/metadata.go @@ -24,6 +24,12 @@ type ( Host string `json:"host"` } + // Storage is the extra set of Storage data passed to the compiler. + Storage struct { + Driver string `json:"driver"` + Host string `json:"host"` + } + // Vela is the extra set of Vela data passed to the compiler. Vela struct { Address string `json:"address"` @@ -41,5 +47,6 @@ type ( Queue *Queue `json:"queue"` Source *Source `json:"source"` Vela *Vela `json:"vela"` + Storage *Storage `json:"storage"` } ) diff --git a/mock/server/server.go b/mock/server/server.go index 62cdc47fc..cd775288d 100644 --- a/mock/server/server.go +++ b/mock/server/server.go @@ -157,6 +157,9 @@ func FakeHandler() http.Handler { // mock endpoint for queue credentials e.GET("/api/v1/queue/info", getQueueCreds) + + // mock endpoint for storage credentials + e.GET("/api/v1/storage/info", getStorageCreds) return e } diff --git a/mock/server/worker.go b/mock/server/worker.go index c92b508ec..4dc4c53e9 100644 --- a/mock/server/worker.go +++ b/mock/server/worker.go @@ -188,6 +188,15 @@ const ( "queue_public_key": "DXeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ98zmko=", "queue_address": "redis://redis:6000" }` + + // StorageInfoResp represents a JSON return for an admin requesting a storage registration info. + // + //not actual credentials. + StorageInfoResp = `{ + "storage_access_key": "DXeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ98zmko=", + "storage_secret_key": "DXeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ98zmko=", + "storage_address": "http://storage:9000" + }` ) // getWorkers returns mock JSON for a http GET. @@ -334,3 +343,24 @@ func getQueueCreds(c *gin.Context) { c.JSON(http.StatusCreated, body) } + +// getStorageCreds returns mock JSON for a http GET. +// Pass "" to Authorization header to test receiving a http 401 response. +func getStorageCreds(c *gin.Context) { + token := c.Request.Header.Get("Authorization") + // verify token if empty + if token == "" { + msg := "unable get storage credentials; invalid registration token" + + c.AbortWithStatusJSON(http.StatusUnauthorized, api.Error{Message: &msg}) + + return + } + + data := []byte(StorageInfoResp) + + var body api.StorageInfo + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusCreated, body) +} diff --git a/router/admin.go b/router/admin.go index d2eb667ac..d24e967ca 100644 --- a/router/admin.go +++ b/router/admin.go @@ -60,6 +60,22 @@ func AdminHandlers(base *gin.RouterGroup) { // Admin step endpoint _admin.PUT("/step", admin.UpdateStep) + // Admin storage bucket endpoints + //_admin.GET("/storage/bucket", admin.ListBuckets) + _admin.PUT("/storage/bucket", admin.CreateBucket) + _admin.DELETE("/storage/bucket", admin.DeleteBucket) + + // Admin storage bucket lifecycle endpoint + _admin.GET("/storage/bucket/lifecycle", admin.GetBucketLifecycle) + _admin.PUT("/storage/bucket/lifecycle", admin.SetBucketLifecycle) + + // Admin storage object endpoints + _admin.POST("/storage/object/download", admin.DownloadObject) + //_admin.POST("/storage/object", admin.UploadObject) + + // Admin storage presign endpoints + _admin.GET("/storage/presign", admin.GetPresignedURL) + // Admin user endpoint _admin.PUT("/user", admin.UpdateUser) diff --git a/router/build.go b/router/build.go index 924576763..12455e464 100644 --- a/router/build.go +++ b/router/build.go @@ -4,7 +4,6 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api/build" "github.com/go-vela/server/api/log" "github.com/go-vela/server/router/middleware" @@ -44,7 +43,8 @@ import ( // POST /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs // GET /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs // PUT /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs -// DELETE /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs . +// DELETE /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs +// POST /api/v1/repos/:org/:repo/builds/:build/storage/upload func BuildHandlers(base *gin.RouterGroup) { // Builds endpoints builds := base.Group("/builds") @@ -68,6 +68,8 @@ func BuildHandlers(base *gin.RouterGroup) { b.GET("/graph", perm.MustRead(), build.GetBuildGraph) b.GET("/executable", perm.MustBuildAccess(), build.GetBuildExecutable) + //b.POST("/storage/upload", perm.MustBuildAccess(), build.UploadObject) + // Service endpoints // * Log endpoints ServiceHandlers(b) diff --git a/router/middleware/signing.go b/router/middleware/signing.go index 6b9dd4c1f..796c7c8a3 100644 --- a/router/middleware/signing.go +++ b/router/middleware/signing.go @@ -32,3 +32,30 @@ func QueueAddress(address string) gin.HandlerFunc { c.Next() } } + +// StorageAccessKey is a middleware function that attaches the access key used +// to open the connection to the storage. +func StorageAccessKey(key string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("access-key", key) + c.Next() + } +} + +// StorageSecretKey is a middleware function that attaches the secret key used +// to open the connection to the storage. +func StorageSecretKey(key string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("secret-key", key) + c.Next() + } +} + +// StorageAddress is a middleware function that attaches the storage address used +// to open the connection to the queue. +func StorageAddress(address string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("storage-address", address) + c.Next() + } +} diff --git a/router/middleware/signing_test.go b/router/middleware/signing_test.go index 3f152a3c9..839448ca6 100644 --- a/router/middleware/signing_test.go +++ b/router/middleware/signing_test.go @@ -106,3 +106,99 @@ func TestMiddleware_QueueAddress(t *testing.T) { t.Errorf("QueueAddress is %v, want %v", got, want) } } + +func TestMiddleware_StorageAddress(t *testing.T) { + // setup types + got := "" + want := "foobar" + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(StorageAddress(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("storage-address").(string) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("StorageAddress returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("StorageAddress is %v, want %v", got, want) + } +} + +func TestMiddleware_StorageAccessKey(t *testing.T) { + // setup types + got := "" + want := "foobar" + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(StorageAccessKey(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("access-key").(string) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("StorageAccessKey returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("StorageAccessKey is %v, want %v", got, want) + } +} + +func TestMiddleware_StorageSecretKey(t *testing.T) { + // setup types + got := "" + want := "foobar" + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(StorageSecretKey(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("secret-key").(string) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("StorageSecretKey returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("StorageSecretKey is %v, want %v", got, want) + } +} diff --git a/router/middleware/storage.go b/router/middleware/storage.go new file mode 100644 index 000000000..11bc1dc2c --- /dev/null +++ b/router/middleware/storage.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/go-vela/server/storage" +) + +// Storage is a middleware function that initializes the object storage and +// attaches to the context of every http.Request. +func Storage(q storage.Storage) gin.HandlerFunc { + return func(c *gin.Context) { + // attach the object storage to the context + storage.ToContext(c, q) + + c.Next() + } +} diff --git a/router/middleware/storage_test.go b/router/middleware/storage_test.go new file mode 100644 index 000000000..0652ec589 --- /dev/null +++ b/router/middleware/storage_test.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/go-vela/server/storage" + "github.com/go-vela/server/storage/minio" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestMiddleware_Storage(t *testing.T) { + // setup types + var got storage.Storage + want, _ := minio.NewTest("", "", "", false) + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(Storage(want)) + engine.GET("/health", func(c *gin.Context) { + got = storage.FromGinContext(c) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Storage returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Storage is %v, want %v", got, want) + } +} diff --git a/router/router.go b/router/router.go index ca131ae63..1b1fb57d4 100644 --- a/router/router.go +++ b/router/router.go @@ -155,6 +155,9 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // Queue endpoints QueueHandlers(baseAPI) + + // Storage endpoints + StorageHandlers(baseAPI) } // end of api return r diff --git a/router/storage.go b/router/storage.go new file mode 100644 index 000000000..c969caea2 --- /dev/null +++ b/router/storage.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/storage" + + "github.com/go-vela/server/router/middleware/perm" +) + +// StorageHandlers is a function that extends the provided base router group +// with the API handlers for storage functionality. +// +// GET /api/v1/storage/info . +func StorageHandlers(base *gin.RouterGroup) { + // Storage endpoints + _storage := base.Group("/storage") + { + _storage.GET("/info", perm.MustWorkerRegisterToken(), storage.Info) + _storage.POST("/upload", storage.UploadObject) + } // end of storage endpoints +} diff --git a/storage/context.go b/storage/context.go new file mode 100644 index 000000000..82bad6627 --- /dev/null +++ b/storage/context.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 + +package storage + +import ( + "context" + "github.com/gin-gonic/gin" +) + +// key is the key used to store minio service in context +const key = "minio" + +// Setter defines a context that enables setting values. +type Setter interface { + Set(string, interface{}) +} + +// FromContext retrieves minio service from the context +func FromContext(ctx context.Context) Storage { + // get minio value from context.Context + v := ctx.Value(key) + if v == nil { + return nil + } + + // cast minio value to expected Storage type + s, ok := v.(Storage) + if !ok { + return nil + } + return s +} + +// FromGinContext retrieves the S3 Service from the gin.Context. +func FromGinContext(c *gin.Context) Storage { + // get minio value from gin.Context + // + // https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc#Context.Get + v, ok := c.Get(key) + if !ok { + return nil + } + + // cast minio value to expected Service type + s, ok := v.(Storage) + if !ok { + return nil + } + + return s +} + +// ToContext adds the secret Service to this +// context if it supports the Setter interface. +func ToContext(c Setter, s Storage) { + c.Set(key, s) +} + +// WithContext adds the minio Storage to the context +func WithContext(ctx context.Context, storage Storage) context.Context { + return context.WithValue(ctx, key, storage) +} + +// WithGinContext inserts the minio Storage into the gin.Context. +func WithGinContext(c *gin.Context, s Storage) { + // set the minio Storage in the gin.Context + // + // https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc#Context.Set + c.Set(key, s) +} diff --git a/storage/context_test.go b/storage/context_test.go new file mode 100644 index 000000000..9b96d83b9 --- /dev/null +++ b/storage/context_test.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +package storage + +import ( + "context" + "reflect" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestExecutor_FromContext(t *testing.T) { + // setup types + _service, _ := New(&Setup{}) + + // setup tests + tests := []struct { + context context.Context + want Storage + }{ + { + //nolint:staticcheck,revive // ignore using string with context value + context: context.WithValue(context.Background(), key, _service), + want: _service, + }, + { + context: context.Background(), + want: nil, + }, + { + //nolint:staticcheck,revive // ignore using string with context value + context: context.WithValue(context.Background(), key, "foo"), + want: nil, + }, + } + + // run tests + for _, test := range tests { + got := FromContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext is %v, want %v", got, test.want) + } + } +} + +func TestExecutor_FromGinContext(t *testing.T) { + // setup types + _service, _ := New(&Setup{}) + + // setup tests + tests := []struct { + context *gin.Context + value interface{} + want Storage + }{ + { + context: new(gin.Context), + value: _service, + want: _service, + }, + { + context: new(gin.Context), + value: nil, + want: nil, + }, + { + context: new(gin.Context), + value: "foo", + want: nil, + }, + } + + // run tests + for _, test := range tests { + if test.value != nil { + test.context.Set(key, test.value) + } + + got := FromGinContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromGinContext is %v, want %v", got, test.want) + } + } +} + +func TestExecutor_WithContext(t *testing.T) { + // setup types + _service, _ := New(&Setup{}) + + //nolint:staticcheck,revive // ignore using string with context value + want := context.WithValue(context.Background(), key, _service) + + // run test + got := WithContext(context.Background(), _service) + + if !reflect.DeepEqual(got, want) { + t.Errorf("WithContext is %v, want %v", got, want) + } +} + +func TestExecutor_WithGinContext(t *testing.T) { + // setup types + _service, _ := New(&Setup{}) + + want := new(gin.Context) + want.Set(key, _service) + + // run test + got := new(gin.Context) + WithGinContext(got, _service) + + if !reflect.DeepEqual(got, want) { + t.Errorf("WithGinContext is %v, want %v", got, want) + } +} diff --git a/storage/flags.go b/storage/flags.go new file mode 100644 index 000000000..12567f8f7 --- /dev/null +++ b/storage/flags.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +package storage + +import ( + "github.com/urfave/cli/v2" +) + +var Flags = []cli.Flag{ + // STORAGE Flags + + &cli.BoolFlag{ + EnvVars: []string{"VELA_STORAGE_ENABLE"}, + Name: "storage.enable", + Usage: "enable object storage", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_STORAGE_DRIVER", "STORAGE_DRIVER"}, + Name: "storage.driver", + Usage: "object storage driver", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_STORAGE_ADDRESS", "STORAGE_ADDRESS"}, + Name: "storage.addr", + Usage: "set the storage endpoint (ex. scheme://host:port)", + }, + + &cli.StringFlag{ + EnvVars: []string{"VELA_STORAGE_ACCESS_KEY", "STORAGE_ACCESS_KEY"}, + Name: "storage.access.key", + Usage: "set storage access key", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_STORAGE_SECRET_KEY", "STORAGE_SECRET_KEY"}, + Name: "storage.secret.key", + Usage: "set storage secret key", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_STORAGE_BUCKET"}, + Name: "storage.bucket.name", + Usage: "set storage bucket name", + }, + &cli.BoolFlag{ + EnvVars: []string{"VELA_STORAGE_USE_SSL"}, + Name: "storage.use.ssl", + Usage: "enable storage to use SSL", + Value: false, + }, +} diff --git a/storage/minio/bucket_exists.go b/storage/minio/bucket_exists.go new file mode 100644 index 000000000..488f34191 --- /dev/null +++ b/storage/minio/bucket_exists.go @@ -0,0 +1,17 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" +) + +// BucketExists checks if a bucket exists in MinIO. +func (c *MinioClient) BucketExists(ctx context.Context, bucket *api.Bucket) (bool, error) { + c.Logger.Tracef("checking if bucket %s exists", bucket.BucketName) + + exists, err := c.client.BucketExists(ctx, bucket.BucketName) + if err != nil { + return false, err + } + return exists, nil +} diff --git a/storage/minio/bucket_exists_test.go b/storage/minio/bucket_exists_test.go new file mode 100644 index 000000000..cb1b3e9a5 --- /dev/null +++ b/storage/minio/bucket_exists_test.go @@ -0,0 +1,88 @@ +package minio + +import ( + "context" + "github.com/gin-gonic/gin" + api "github.com/go-vela/server/api/types" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMinioClient_BucketExists(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + // mock create bucket call + engine.PUT("/foo/", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + }) + // mock bucket exists call + engine.HEAD("/foo/", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // create bucket + err := client.CreateBucket(ctx, &api.Bucket{BucketName: "foo"}) + if err != nil { + t.Errorf("CreateBucket returned err: %v", err) + } + + // run test + exists, err := client.BucketExists(ctx, &api.Bucket{BucketName: "foo"}) + if resp.Code != http.StatusOK { + t.Errorf("BucketExists returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("BucketExists returned err: %v", err) + } + + if !exists { + t.Errorf("BucketExists returned %v, want %v", exists, true) + } +} + +func TestMinioClient_BucketExists_Failure(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.HEAD("/foo/", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // run test + exists, err := client.BucketExists(ctx, &api.Bucket{BucketName: "bar"}) + if resp.Code != http.StatusOK { + t.Errorf("BucketExists returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("BucketExists returned err: %v", err) + } + + if exists { + t.Errorf("BucketExists returned %v, want %v", exists, false) + } +} diff --git a/storage/minio/create_bucket.go b/storage/minio/create_bucket.go new file mode 100644 index 000000000..8b7bb9a31 --- /dev/null +++ b/storage/minio/create_bucket.go @@ -0,0 +1,38 @@ +package minio + +import ( + "context" + "fmt" + api "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" +) + +// CreateBucket creates a new bucket in MinIO. +func (c *MinioClient) CreateBucket(ctx context.Context, bucket *api.Bucket) error { + c.Logger.Tracef("create new bucket: %s", bucket.BucketName) + var opts minio.MakeBucketOptions + if &bucket.Options == nil { + c.Logger.Trace("Using US Standard Region as location default") + opts = minio.MakeBucketOptions{} + } else { + opts = minio.MakeBucketOptions{ + Region: bucket.Options.Region, + ObjectLocking: bucket.Options.ObjectLocking, + } + } + + exists, errBucketExists := c.BucketExists(ctx, bucket) + if errBucketExists != nil && exists { + c.Logger.Tracef("Bucket %s already exists", bucket.BucketName) + + return fmt.Errorf("bucket %s already exists", bucket.BucketName) + } + + err := c.client.MakeBucket(ctx, bucket.BucketName, opts) + if err != nil { + + c.Logger.Errorf("unable to create bucket %s: %v", bucket.BucketName, err) + return err + } + return nil +} diff --git a/storage/minio/create_bucket_test.go b/storage/minio/create_bucket_test.go new file mode 100644 index 000000000..4e9d8a6aa --- /dev/null +++ b/storage/minio/create_bucket_test.go @@ -0,0 +1,42 @@ +package minio + +import ( + "context" + "github.com/gin-gonic/gin" + api "github.com/go-vela/server/api/types" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMinioClient_CreateBucket(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.PUT("/foo/", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + }) + + fake := httptest.NewServer(engine) + defer fake.Close() + + b := new(api.Bucket) + b.BucketName = "foo" + + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // run test + err := client.CreateBucket(context.TODO(), b) + if resp.Code != http.StatusOK { + t.Errorf("CreateBucket returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("CreateBucket returned err: %v", err) + } +} diff --git a/storage/minio/delete.go b/storage/minio/delete.go new file mode 100644 index 000000000..d6a62441f --- /dev/null +++ b/storage/minio/delete.go @@ -0,0 +1,18 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" +) + +// Delete deletes an object in a bucket in MinIO. +func (c *MinioClient) Delete(ctx context.Context, object *api.Object) error { + c.Logger.Tracef("deleting objectName: %s from bucketName: %s", object.ObjectName, object.Bucket.BucketName) + + err := c.client.RemoveObject(ctx, object.Bucket.BucketName, object.ObjectName, minio.RemoveObjectOptions{}) + if err != nil { + return err + } + return nil +} diff --git a/storage/minio/delete_bucket.go b/storage/minio/delete_bucket.go new file mode 100644 index 000000000..01aa02a66 --- /dev/null +++ b/storage/minio/delete_bucket.go @@ -0,0 +1,17 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" +) + +// DeleteBucket deletes a bucket in MinIO. +func (c *MinioClient) DeleteBucket(ctx context.Context, bucket *api.Bucket) error { + c.Logger.Tracef("deleting bucketName: %s", bucket.BucketName) + + err := c.client.RemoveBucket(ctx, bucket.BucketName) + if err != nil { + return err + } + return nil +} diff --git a/storage/minio/delete_bucket_test.go b/storage/minio/delete_bucket_test.go new file mode 100644 index 000000000..eb08ec591 --- /dev/null +++ b/storage/minio/delete_bucket_test.go @@ -0,0 +1,86 @@ +package minio + +import ( + "context" + "github.com/gin-gonic/gin" + api "github.com/go-vela/server/api/types" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMinioClient_Bucket_Delete_Success(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + // mock create bucket call + engine.PUT("/foo/", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + }) + + // mock delete bucket call + engine.DELETE("/foo/", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + b := new(api.Bucket) + b.BucketName = "foo" + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // create bucket + err := client.CreateBucket(ctx, b) + if err != nil { + t.Errorf("CreateBucket returned err: %v", err) + } + + // run test + err = client.DeleteBucket(ctx, b) + if resp.Code != http.StatusOK { + t.Errorf("DeleteBucket returned %v, want %v", resp.Code, http.StatusOK) + } + + // in Minio SDK, removeBucket returns status code 200 OK as error if a bucket is deleted successfully + if err != nil && err.Error() != "200 OK" { + t.Errorf("DeleteBucket returned err: %v", err) + } + +} + +func TestMinioClient_Bucket_Delete_BucketNotFound(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // mock delete bucket call + engine.DELETE("/foo/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "The specified bucket does not exist"}) + }) + + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + b := new(api.Bucket) + b.BucketName = "foo" + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // run test + err := client.DeleteBucket(ctx, b) + if resp.Code != http.StatusOK { + t.Errorf("DeleteBucket returned %v, want %v", resp.Code, http.StatusOK) + } + + if err == nil { + t.Errorf("DeleteBucket expected error, got nil") + } + +} diff --git a/storage/minio/doc.go b/storage/minio/doc.go new file mode 100644 index 000000000..c5576b1fd --- /dev/null +++ b/storage/minio/doc.go @@ -0,0 +1 @@ +package minio diff --git a/storage/minio/download.go b/storage/minio/download.go new file mode 100644 index 000000000..db167ada5 --- /dev/null +++ b/storage/minio/download.go @@ -0,0 +1,95 @@ +package minio + +import ( + "context" + "github.com/dustin/go-humanize" + "github.com/go-vela/archiver/v3" + api "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" + "github.com/sirupsen/logrus" + "os" +) + +func (c *MinioClient) Download(ctx context.Context, object *api.Object) error { + // Temporary file to store the object + filename := "/" + // Check if the directory exists + //_, err := os.Stat(object.FilePath) + //if os.IsNotExist(err) { + // // Create the directory if it does not exist + // err = os.MkdirAll(object.FilePath, 0755) + // if err != nil { + // return fmt.Errorf("failed to create directory: %w", err) + // } + //} else if err != nil { + // return fmt.Errorf("failed to check directory: %w", err) + //} + //err := c.client.FGetObject(ctx, object.BucketName, object.ObjectName, object.FilePath, minio.GetObjectOptions{}) + //if err != nil { + // c.Logger.Errorf("unable to retrive object %s", object.ObjectName) + // return err + //} + // + //c.Logger.Tracef("successfully downloaded object %s to %s", object.ObjectName, object.FilePath) + //return nil + logrus.Debugf("getting object info on bucket %s from path: %s", object.Bucket.BucketName, object.ObjectName) + + // set a timeout on the request to the cache provider + //ctx, cancel := context.WithTimeout(context.Background(), r.Timeout) + //defer cancel() + + // collect metadata on the object + objInfo, err := c.client.StatObject(ctx, object.Bucket.BucketName, object.ObjectName, minio.StatObjectOptions{}) + if objInfo.Key == "" { + logrus.Error(err) + return nil + } + + logrus.Debugf("getting object in bucket %s from path: %s", object.Bucket.BucketName, object.ObjectName) + + logrus.Infof("%s to download", humanize.Bytes(uint64(objInfo.Size))) + + // retrieve the object in specified path of the bucket + err = c.client.FGetObject(ctx, object.Bucket.BucketName, object.ObjectName, filename, minio.GetObjectOptions{}) + if err != nil { + return err + } + + stat, err := os.Stat(object.FilePath) + if err != nil { + return err + } + + logrus.Infof("downloaded %s to %s on local filesystem", humanize.Bytes(uint64(stat.Size())), filename) + + logrus.Debug("getting current working directory") + + // grab the current working directory for unpacking the object + pwd, err := os.Getwd() + if err != nil { + return err + } + + logrus.Debugf("unarchiving file %s into directory %s", filename, pwd) + + // expand the object back onto the filesystem + err = archiver.Unarchive(object.FilePath, pwd) + if err != nil { + return err + } + + logrus.Infof("successfully unpacked file %s", object.FilePath) + + // delete the temporary archive file + err = os.Remove(filename) + if err != nil { + logrus.Infof("delete of file %s unsuccessful", filename) + } else { + logrus.Infof("file archive %s successfully deleted", filename) + } + + logrus.Infof("object downloaded successfully") + + return nil + +} diff --git a/storage/minio/download_test.go b/storage/minio/download_test.go new file mode 100644 index 000000000..c5576b1fd --- /dev/null +++ b/storage/minio/download_test.go @@ -0,0 +1 @@ +package minio diff --git a/storage/minio/get_bucket_lifecycle.go b/storage/minio/get_bucket_lifecycle.go new file mode 100644 index 000000000..8c9027cc8 --- /dev/null +++ b/storage/minio/get_bucket_lifecycle.go @@ -0,0 +1,21 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" +) + +// GetBucketLifecycle retrieves the lifecycle configuration for a bucket. +func (c *MinioClient) GetBucketLifecycle(ctx context.Context, bucket *api.Bucket) (*api.Bucket, error) { + c.Logger.Tracef("getting lifecycle configuration for bucket %s", bucket.BucketName) + + var lifecycleConfig *api.Bucket + lifeCycle, err := c.client.GetBucketLifecycle(ctx, bucket.BucketName) + if err != nil { + return lifecycleConfig, err + } + + lifecycleConfig = &api.Bucket{BucketName: bucket.BucketName, LifecycleConfig: *lifeCycle} + + return lifecycleConfig, nil +} diff --git a/storage/minio/list_bucket.go b/storage/minio/list_bucket.go new file mode 100644 index 000000000..a619e7705 --- /dev/null +++ b/storage/minio/list_bucket.go @@ -0,0 +1,21 @@ +package minio + +import ( + "context" +) + +// ListBuckets lists all buckets in MinIO. +func (c *MinioClient) ListBuckets(ctx context.Context) ([]string, error) { + c.Logger.Trace("listing all buckets") + + buckets, err := c.client.ListBuckets(ctx) + if err != nil { + return nil, err + } + + bucketNames := make([]string, len(buckets)) + for i, bucket := range buckets { + bucketNames[i] = bucket.Name + } + return bucketNames, nil +} diff --git a/storage/minio/list_objects.go b/storage/minio/list_objects.go new file mode 100644 index 000000000..31d8b5b8f --- /dev/null +++ b/storage/minio/list_objects.go @@ -0,0 +1,23 @@ +package minio + +import ( + "context" + "github.com/minio/minio-go/v7" +) + +// ListObjects lists the objects in a bucket. +func (c *MinioClient) ListObjects(ctx context.Context, bucketName string) ([]string, error) { + c.Logger.Tracef("listing objects in bucket %s", bucketName) + + objectCh := c.client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{}) + + var objects []string + for object := range objectCh { + if object.Err != nil { + return nil, object.Err + } + objects = append(objects, object.Key) + } + + return objects, nil +} diff --git a/storage/minio/minio.go b/storage/minio/minio.go new file mode 100644 index 000000000..49c3f2164 --- /dev/null +++ b/storage/minio/minio.go @@ -0,0 +1,174 @@ +package minio + +import ( + "context" + "fmt" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/sirupsen/logrus" + "strings" + "time" +) + +// config holds the configuration for the MinIO client. +type config struct { + Endpoint string + AccessKey string + SecretKey string + Secure bool +} + +// MinioClient implements the Storage interface using MinIO. +type MinioClient struct { + config *config + client *minio.Client + Options *minio.Options + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + Logger *logrus.Entry +} + +// New creates a new MinIO client. +func New(endpoint string, opts ...ClientOpt) (*MinioClient, error) { + // create new Minio client + c := new(MinioClient) + + // default to secure connection + var urlEndpoint string + useSSL := true + + // create new fields + c.config = new(config) + c.Options = new(minio.Options) + + // create new logger for the client + logger := logrus.StandardLogger() + c.Logger = logrus.NewEntry(logger).WithField("minio", "minio") + + // apply all provided configuration options + for _, opt := range opts { + err := opt(c) + if err != nil { + return nil, err + } + } + c.Options.Creds = credentials.NewStaticV4(c.config.AccessKey, c.config.SecretKey, "") + c.Options.Secure = c.config.Secure + logrus.Debugf("secure: %v", c.config.Secure) + + if len(endpoint) > 0 { + useSSL = strings.HasPrefix(endpoint, "https://") + + if !useSSL { + if !strings.HasPrefix(endpoint, "http://") { + return nil, fmt.Errorf("invalid server %s: must to be a HTTP URI", endpoint) + } + + urlEndpoint = endpoint[7:] + } else { + urlEndpoint = endpoint[8:] + } + } + + // create the Minio client from the provided endpoint and options + minioClient, err := minio.New(urlEndpoint, c.Options) + if err != nil { + return nil, err + } + + c.client = minioClient + + return c, nil + //minioClient, err := minio.New(endpoint, &minio.Options{ + // Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + // Secure: useSSL, + //}) + //if err != nil { + // return nil, err + //} + //return &MinioClient{client: minioClient}, nil +} + +// pingBucket checks if the specified bucket exists. +func pingBucket(c *MinioClient, bucket string) error { + for i := 0; i < 10; i++ { + _, err := c.client.BucketExists(context.Background(), bucket) + if err != nil { + c.Logger.Debugf("unable to ping %s. Retrying in %v", bucket, time.Duration(i)*time.Second) + time.Sleep(1 * time.Second) + + continue + } + } + + return nil +} + +// NewTest returns a Storage implementation that +// integrates with a local MinIO instance. +// +// This function is intended for running tests only. +// +//nolint:revive // ignore returning unexported client +func NewTest(endpoint, accessKey, secretKey string, secure bool) (*MinioClient, error) { + //var cleanup func() error + //var err error + //endpoint, cleanup, err = miniotest.StartEmbedded() + // + //if err != nil { + // fmt.Fprintf(os.Stderr, "while starting embedded server: %s", err) + // os.Exit(1) + //} + // + //err = cleanup() + //if err != nil { + // fmt.Fprintf(os.Stderr, "while stopping embedded server: %s", err) + //} + + // create a local fake MinIO instance + // + // https://pkg.go.dev/github.com/minio/minio-go/v7#New + //minioClient, err := minio.New(endpoint, &minio.Options{ + // Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + // Secure: false, + //}) + //if err != nil { + // return nil, err + //} + + return New(endpoint, WithAccessKey(accessKey), WithSecretKey(secretKey), WithSecure(secure)) +} + +//// UploadArtifact uploads an artifact to storage. +//func (c *MinioClient) UploadArtifact(ctx context.Context, workflowID, artifactName string, data []byte) error { +// key := path.Join("artifacts", workflowID, artifactName) +// bucket := "vela-artifacts" +// return c.upload(ctx, bucket, key, data) +//} +// +//// DownloadArtifact downloads an artifact from storage. +//func (c *MinioClient) DownloadArtifact(ctx context.Context, workflowID, artifactName string) ([]byte, error) { +// key := path.Join("artifacts", workflowID, artifactName) +// bucket := "vela-artifacts" +// return c.download(ctx, bucket, key) +//} +// +//// UploadCache uploads cache data to storage. +//func (c *MinioClient) UploadCache(ctx context.Context, key string, data []byte) error { +// cacheKey := path.Join("cache", key) +// bucket := "vela-cache" +// return c.upload(ctx, bucket, cacheKey, data) +//} +// +//// DownloadCache downloads cache data from storage. +//func (c *MinioClient) DownloadCache(ctx context.Context, key string) ([]byte, error) { +// cacheKey := path.Join("cache", key) +// bucket := "vela-cache" +// return c.download(ctx, bucket, cacheKey) +//} +// +//// DeleteCache deletes cache data from storage. +//func (c *MinioClient) DeleteCache(ctx context.Context, key string) error { +// cacheKey := path.Join("cache", key) +// bucket := "vela-cache" +// return c.client.RemoveObject(ctx, bucket, cacheKey, minio.RemoveObjectOptions{}) +//} diff --git a/storage/minio/minio_test.go b/storage/minio/minio_test.go new file mode 100644 index 000000000..201410e20 --- /dev/null +++ b/storage/minio/minio_test.go @@ -0,0 +1,55 @@ +package minio + +import ( + "testing" +) + +var ( + endpoint = "http://localhost:9000" + _accessKey = "minio_access_user" + _secretKey = "minio_secret_key" + _useSSL = false +) + +func TestMinio_New(t *testing.T) { + // setup types + // create a local fake MinIO instance + // + // https://pkg.go.dev/github.com/minio/minio-go/v7#New + // setup tests + tests := []struct { + failure bool + endpoint string + }{ + { + failure: false, + endpoint: endpoint, + }, + { + failure: true, + endpoint: "", + }, + } + + // run tests + for _, test := range tests { + _, err := New( + test.endpoint, + WithAccessKey(_accessKey), + WithSecretKey(_secretKey), + WithSecure(_useSSL), + ) + + if test.failure { + if err == nil { + t.Errorf("New should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("New returned err: %v", err) + } + } +} diff --git a/storage/minio/opts.go b/storage/minio/opts.go new file mode 100644 index 000000000..a4ab86c5c --- /dev/null +++ b/storage/minio/opts.go @@ -0,0 +1,54 @@ +package minio + +import ( + "fmt" +) + +// ClientOpt represents a configuration option to initialize the MinIO client. +type ClientOpt func(client *MinioClient) error + +// WithAccessKey sets the access key in the MinIO client. +func WithAccessKey(accessKey string) ClientOpt { + return func(c *MinioClient) error { + c.Logger.Trace("configuring access key in minio client") + + // check if the access key provided is empty + if len(accessKey) == 0 { + return fmt.Errorf("no MinIO access key provided") + } + + // set the access key in the minio client + c.config.AccessKey = accessKey + + return nil + } +} + +// WithSecretKey sets the secret key in the MinIO client. +func WithSecretKey(secretKey string) ClientOpt { + return func(c *MinioClient) error { + c.Logger.Trace("configuring secret key in minio client") + + // check if the secret key provided is empty + if len(secretKey) == 0 { + return fmt.Errorf("no MinIO secret key provided") + } + + // set the secret key in the minio client + c.config.SecretKey = secretKey + + return nil + } +} + +// WithSecure sets the secure connection mode in the MinIO client. +func WithSecure(secure bool) ClientOpt { + return func(c *MinioClient) error { + c.Logger.Trace("configuring secure connection mode in minio client") + + // set the secure connection mode in the minio client + c.config.Secure = secure + + return nil + } +} diff --git a/storage/minio/presigned_get_object.go b/storage/minio/presigned_get_object.go new file mode 100644 index 000000000..f2add5ef8 --- /dev/null +++ b/storage/minio/presigned_get_object.go @@ -0,0 +1,36 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" + "github.com/sirupsen/logrus" + "time" +) + +// TODO hide URL behind a different name +// PresignedGetObject generates a presigned URL for downloading an object. +func (c *MinioClient) PresignedGetObject(ctx context.Context, object *api.Object) (string, error) { + c.Logger.Tracef("generating presigned URL for object %s in bucket %s", object.ObjectName, object.Bucket.BucketName) + + // collect metadata on the object + objInfo, err := c.client.StatObject(ctx, object.Bucket.BucketName, object.ObjectName, minio.StatObjectOptions{}) + if objInfo.Key == "" { + logrus.Errorf("unable to get object info %s from bucket %s: %v", object.ObjectName, object.Bucket.BucketName, err) + return "", err + } + + _, err = c.client.BucketExists(ctx, object.Bucket.BucketName) + if err != nil { + logrus.Errorf("unable to check if bucket %s exists: %v", object.Bucket.BucketName, err) + return "", err + } + // Generate presigned URL for downloading the object. + // The URL is valid for 7 days. + presignedURL, err := c.client.PresignedGetObject(ctx, object.Bucket.BucketName, object.ObjectName, 7*24*time.Hour, nil) + if err != nil { + return "", err + } + //presignedURL.RequestURI() + return presignedURL.String(), nil +} diff --git a/storage/minio/set_bucket_lifecycle.go b/storage/minio/set_bucket_lifecycle.go new file mode 100644 index 000000000..1b98c36da --- /dev/null +++ b/storage/minio/set_bucket_lifecycle.go @@ -0,0 +1,12 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" +) + +// SetBucketLifecycle sets the lifecycle configuration for a bucket. +func (c *MinioClient) SetBucketLifecycle(ctx context.Context, bucket *api.Bucket) error { + c.Logger.Tracef("setting lifecycle configuration for bucket %s", bucket.BucketName) + return c.client.SetBucketLifecycle(ctx, bucket.BucketName, &bucket.LifecycleConfig) +} diff --git a/storage/minio/stat_object.go b/storage/minio/stat_object.go new file mode 100644 index 000000000..4e4d8d2a9 --- /dev/null +++ b/storage/minio/stat_object.go @@ -0,0 +1,24 @@ +package minio + +import ( + "context" + "fmt" + "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" +) + +// StatObject retrieves the metadata of an object from the MinIO storage. +func (c *MinioClient) StatObject(ctx context.Context, object *types.Object) (*types.Object, error) { + c.Logger.Tracef("retrieving metadata for object %s from bucket %s", object.ObjectName, object.Bucket.BucketName) + + // Get object info + info, err := c.client.StatObject(ctx, object.Bucket.BucketName, object.ObjectName, minio.StatObjectOptions{}) + if err != nil { + return nil, fmt.Errorf("unable to get object info %s from bucket %s: %v", object.ObjectName, object.Bucket.BucketName, err) + } + + // Map MinIO object info to API object + return &types.Object{ + ObjectName: info.Key, + }, nil +} diff --git a/storage/minio/test_data/create_bucket.json b/storage/minio/test_data/create_bucket.json new file mode 100644 index 000000000..b1e77da2f --- /dev/null +++ b/storage/minio/test_data/create_bucket.json @@ -0,0 +1,3 @@ +{ + "bucket_name": "foo" +} \ No newline at end of file diff --git a/storage/minio/upload.go b/storage/minio/upload.go new file mode 100644 index 000000000..e325070b6 --- /dev/null +++ b/storage/minio/upload.go @@ -0,0 +1,35 @@ +package minio + +import ( + "context" + api "github.com/go-vela/server/api/types" + "github.com/minio/minio-go/v7" + "io" + "mime" + "path/filepath" +) + +// Upload uploads an object to a bucket in MinIO.ts +func (c *MinioClient) Upload(ctx context.Context, object *api.Object) error { + c.Logger.Tracef("uploading data to bucket %s", object.Bucket.BucketName) + _, err := c.client.FPutObject(ctx, object.Bucket.BucketName, object.ObjectName, object.FilePath, minio.PutObjectOptions{}) + + return err +} + +// UploadObject uploads an object to a bucket in MinIO.ts +func (c *MinioClient) UploadObject(ctx context.Context, object *api.Object, reader io.Reader, size int64) error { + c.Logger.Infof("uploading data to bucket %s", object.Bucket.BucketName) + ext := filepath.Ext(object.FilePath) + contentType := mime.TypeByExtension(ext) + + c.Logger.Infof("uploading object %s with content type %s", object.ObjectName, contentType) + info, err := c.client.PutObject(ctx, object.Bucket.BucketName, object.ObjectName, reader, size, + minio.PutObjectOptions{ContentType: contentType}) + if err != nil { + c.Logger.Errorf("unable to upload object %s: %v", object.ObjectName, err) + return err + } + c.Logger.Infof("uploaded object %v with size %d", info, info.Size) + return nil +} diff --git a/storage/minio/upload_test.go b/storage/minio/upload_test.go new file mode 100644 index 000000000..fa9492e7e --- /dev/null +++ b/storage/minio/upload_test.go @@ -0,0 +1,102 @@ +package minio + +import ( + "context" + "github.com/gin-gonic/gin" + api "github.com/go-vela/server/api/types" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestMinioClient_Upload_Success(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + // mock create bucket call + engine.PUT("/foo/", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + }) + // mock bucket exists call + engine.PUT("/foo/test.xml", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("test_data/test.xml") + }) + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + obj := new(api.Object) + obj.Bucket.BucketName = "foo" + obj.ObjectName = "test.xml" + obj.FilePath = "test_data/test.xml" + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // create bucket + err := client.CreateBucket(ctx, &api.Bucket{BucketName: "foo"}) + if err != nil { + t.Errorf("CreateBucket returned err: %v", err) + } + + // run test + err = client.Upload(ctx, obj) + if resp.Code != http.StatusOK { + t.Errorf("Upload returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("Upload returned err: %v", err) + } + +} + +func TestMinioClient_Upload_Failure(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + // mock create bucket call + engine.PUT("/foo/", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + }) + // mock bucket exists call + engine.PUT("/foo/test.xml", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("test_data/test.xml") + }) + fake := httptest.NewServer(engine) + defer fake.Close() + ctx := context.TODO() + obj := new(api.Object) + obj.Bucket.BucketName = "foo" + obj.ObjectName = "test.xml" + obj.FilePath = "nonexist/test.xml" + client, _ := NewTest(fake.URL, "miniokey", "miniosecret", false) + + // create bucket + err := client.CreateBucket(ctx, &api.Bucket{BucketName: "foo"}) + if err != nil { + t.Errorf("CreateBucket returned err: %v", err) + } + + // run test + err = client.Upload(ctx, obj) + if resp.Code != http.StatusOK { + t.Errorf("Upload returned %v, want %v", resp.Code, http.StatusOK) + } + + if !os.IsNotExist(err) { + t.Errorf("Upload returned err: %v", err) + } +} diff --git a/storage/service.go b/storage/service.go new file mode 100644 index 000000000..cdc225823 --- /dev/null +++ b/storage/service.go @@ -0,0 +1,35 @@ +package storage + +import ( + "context" + api "github.com/go-vela/server/api/types" + "io" +) + +// Storage defines the service interface for object storage operations. +type Storage interface { + // Bucket Management + CreateBucket(ctx context.Context, bucket *api.Bucket) error + DeleteBucket(ctx context.Context, bucket *api.Bucket) error + BucketExists(ctx context.Context, bucket *api.Bucket) (bool, error) + ListBuckets(ctx context.Context) ([]string, error) + // Object Operations + StatObject(ctx context.Context, object *api.Object) (*api.Object, error) + Upload(ctx context.Context, object *api.Object) error + UploadObject(ctx context.Context, object *api.Object, reader io.Reader, size int64) error + Download(ctx context.Context, object *api.Object) error + Delete(ctx context.Context, object *api.Object) error + ListObjects(ctx context.Context, bucketName string) ([]string, error) + // Presigned URLs + //GeneratePresignedURL(ctx context.Context, bucket string, key string, expiry int64) (string, error) + PresignedGetObject(ctx context.Context, object *api.Object) (string, error) + // Object Lifecycle + SetBucketLifecycle(ctx context.Context, bucketName *api.Bucket) error + GetBucketLifecycle(ctx context.Context, bucket *api.Bucket) (*api.Bucket, error) + //// Workflow-Specific Operations + //UploadArtifact(ctx context.Context, workflowID, artifactName string, data []byte) error + //DownloadArtifact(ctx context.Context, workflowID, artifactName string) ([]byte, error) + //UploadCache(ctx context.Context, key string, data []byte) error + //DownloadCache(ctx context.Context, key string) ([]byte, error) + //DeleteCache(ctx context.Context, key string) error +} diff --git a/storage/setup.go b/storage/setup.go new file mode 100644 index 000000000..740b5b824 --- /dev/null +++ b/storage/setup.go @@ -0,0 +1,73 @@ +package storage + +import ( + "fmt" + "github.com/go-vela/server/storage/minio" + "github.com/sirupsen/logrus" +) + +// Setup represents the configuration necessary for +// creating a Vela service capable of integrating +// with a configured S3 environment. +type Setup struct { + Enable bool + Driver string + Endpoint string + AccessKey string + SecretKey string + Bucket string + Region string + Secure bool +} + +// Minio creates and returns a Vela service capable +// of integrating with an S3 environment. +func (s *Setup) Minio() (Storage, error) { + //client, err := minio.New(s.Endpoint, &minio.Options{ + // Creds: credentials.NewStaticV4(s.AccessKey, s.SecretKey, ""), + // Secure: s.Secure, + //}) + //if err != nil { + // return nil, err + //} + return minio.New( + s.Endpoint, + minio.WithAccessKey(s.AccessKey), + minio.WithSecretKey(s.SecretKey), + minio.WithSecure(s.Secure), + ) +} + +// Validate verifies the necessary fields for the +// provided configuration are populated correctly. +func (s *Setup) Validate() error { + logrus.Trace("validating Storage setup for client") + + // verify storage is enabled + //if !s.Enable { + // return fmt.Errorf("Storage is not enabled") + //} + + // verify an endpoint was provided + if len(s.Endpoint) == 0 { + return fmt.Errorf("no storage endpoint provided") + } + + // verify an access key was provided + if len(s.AccessKey) == 0 { + return fmt.Errorf("no storage access key provided") + } + + // verify a secret key was provided + if len(s.SecretKey) == 0 { + return fmt.Errorf("no storage secret key provided") + } + + // verify a bucket was provided + //if len(s.Bucket) == 0 { + // return fmt.Errorf("no storage bucket provided") + //} + + // setup is valid + return nil +} diff --git a/storage/setup_test.go b/storage/setup_test.go new file mode 100644 index 000000000..21ea256c8 --- /dev/null +++ b/storage/setup_test.go @@ -0,0 +1,99 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetup_Minio(t *testing.T) { + setup := &Setup{ + Enable: true, + Driver: "minio", + Endpoint: "http://minio.example.com", + AccessKey: "access-key", + SecretKey: "secret-key", + Bucket: "bucket-name", + Secure: true, + } + + storage, err := setup.Minio() + assert.NoError(t, err) + assert.NotNil(t, storage) +} + +func TestSetup_Validate(t *testing.T) { + tests := []struct { + failure bool + setup *Setup + }{ + { + failure: false, + setup: &Setup{ + Enable: true, + Endpoint: "example.com", + AccessKey: "access-key", + SecretKey: "secret-key", + Bucket: "bucket-name", + }, + }, + { + failure: true, + setup: &Setup{ + Enable: false, + }, + }, + { + failure: true, + setup: &Setup{ + Enable: true, + AccessKey: "access-key", + SecretKey: "secret-key", + Bucket: "bucket-name", + }, + }, + { + failure: true, + setup: &Setup{ + Enable: true, + Endpoint: "example.com", + SecretKey: "secret-key", + Bucket: "bucket-name", + }, + }, + { + failure: true, + setup: &Setup{ + Enable: true, + Endpoint: "example.com", + AccessKey: "access-key", + Bucket: "bucket-name", + }, + }, + { + failure: true, + setup: &Setup{ + Enable: true, + Endpoint: "example.com", + AccessKey: "access-key", + SecretKey: "secret-key", + }, + }, + } + + for _, test := range tests { + err := test.setup.Validate() + + if test.failure { + if err == nil { + t.Errorf("Validate should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("Validate returned err: %v", err) + } + } +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 000000000..5c57be0df --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,56 @@ +package storage + +import ( + "fmt" + "github.com/go-vela/server/constants" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +// FromCLIContext helper function to setup Minio Client from the CLI arguments. +func FromCLIContext(c *cli.Context) (Storage, error) { + logrus.Debug("creating Minio client from CLI configuration") + logrus.Debugf("STORAGE Key: %s", c.String("storage.access.key")) + logrus.Debugf("STORAGE Secret: %s", c.String("storage.secret.key")) + // S3 configuration + _setup := &Setup{ + Enable: c.Bool("storage.enable"), + Driver: c.String("storage.driver"), + Endpoint: c.String("storage.addr"), + AccessKey: c.String("storage.access.key"), + SecretKey: c.String("storage.secret.key"), + Bucket: c.String("storage.bucket.name"), + Secure: c.Bool("storage.use.ssl"), + } + + return New(_setup) + +} + +// New creates and returns a Vela service capable of +// integrating with the configured storage environment. +// Currently, the following storages are supported: +// +// * minio +// . +func New(s *Setup) (Storage, error) { + // validate the setup being provided + // + err := s.Validate() + if err != nil { + return nil, fmt.Errorf("unable to validate storage setup: %w", err) + } + logrus.Debug("creating storage client from setup") + // process the storage driver being provided + switch s.Driver { + case constants.DriverMinio: + // handle the Kafka queue driver being provided + // + // https://pkg.go.dev/github.com/go-vela/server/queue?tab=doc#Setup.Kafka + return s.Minio() + default: + // handle an invalid queue driver being provided + return nil, fmt.Errorf("invalid storage driver provided: %s", s.Driver) + } + +} diff --git a/storage/storage_test.go b/storage/storage_test.go new file mode 100644 index 000000000..a5ad55254 --- /dev/null +++ b/storage/storage_test.go @@ -0,0 +1,53 @@ +package storage + +import ( + "github.com/go-vela/server/constants" + "testing" +) + +func TestStorage_New(t *testing.T) { + tests := []struct { + failure bool + setup *Setup + }{ + { + failure: false, + setup: &Setup{ + Driver: constants.DriverMinio, + Enable: true, + Endpoint: "http://minio.example.com", + AccessKey: "access-key", + SecretKey: "secret-key", + Bucket: "bucket-name", + Secure: true, + }, + }, + { + failure: true, + setup: &Setup{ + Driver: "invalid-driver", + Enable: false, + Endpoint: "http://invalid.example.com", + AccessKey: "access-key", + SecretKey: "secret-key", + Bucket: "bucket-name", + Secure: true, + }, + }, + } + + for _, test := range tests { + _, err := New(test.setup) + + if test.failure { + if err == nil { + t.Errorf("New should have returned err") + } + continue + } + + if err != nil { + t.Errorf("New returned err: %v", err) + } + } +}