From 5df46652b30c506028bbe76fb2b32ebce8fba594 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 Feb 2024 17:34:14 +0100 Subject: [PATCH 01/15] Actions Artifacts v4 backend --- models/fixtures/action_run.yml | 19 + models/fixtures/action_run_job.yml | 14 + models/fixtures/action_task.yml | 20 + routers/api/actions/artifact.pb.go | 899 ++++++++++++++++++ routers/api/actions/artifact.proto | 62 ++ routers/api/actions/artifacts_chunks.go | 50 +- routers/api/actions/artifacts_utils.go | 11 + routers/api/actions/artifactsv4.go | 523 ++++++++++ routers/init.go | 2 + routers/web/repo/actions/view.go | 11 + .../api_actions_artifact_v4_test.go | 162 ++++ 11 files changed, 1771 insertions(+), 2 deletions(-) create mode 100644 routers/api/actions/artifact.pb.go create mode 100644 routers/api/actions/artifact.proto create mode 100644 routers/api/actions/artifactsv4.go create mode 100644 tests/integration/api_actions_artifact_v4_test.go diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 2c2151f354af6..a42ab77ca5b09 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -17,3 +17,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 792 + title: "update actions" + repo_id: 4 + owner_id: 1 + workflow_id: "artifact.yaml" + index: 188 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 071998b9796f9..fd90f4fd5d2ea 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -12,3 +12,17 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 193 + run_id: 792 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c78fb3c5d6377..443effe08c92a 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -18,3 +18,23 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 48 + job_id: 193 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running", running task can upload artifacts + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go new file mode 100644 index 0000000000000..7390527542b45 --- /dev/null +++ b/routers/api/actions/artifact.pb.go @@ -0,0 +1,899 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.2 +// source: artifact.proto + +package actions + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *CreateArtifactRequest) Reset() { + *x = CreateArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateArtifactRequest) ProtoMessage() {} + +func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead. +func (*CreateArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *CreateArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *CreateArtifactRequest) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +type CreateArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` +} + +func (x *CreateArtifactResponse) Reset() { + *x = CreateArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateArtifactResponse) ProtoMessage() {} + +func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead. +func (*CreateArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *CreateArtifactResponse) GetSignedUploadUrl() string { + if x != nil { + return x.SignedUploadUrl + } + return "" +} + +type FinalizeArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (x *FinalizeArtifactRequest) Reset() { + *x = FinalizeArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FinalizeArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeArtifactRequest) ProtoMessage() {} + +func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead. +func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{2} +} + +func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *FinalizeArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *FinalizeArtifactRequest) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue { + if x != nil { + return x.Hash + } + return nil +} + +type FinalizeArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *FinalizeArtifactResponse) Reset() { + *x = FinalizeArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FinalizeArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeArtifactResponse) ProtoMessage() {} + +func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead. +func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{3} +} + +func (x *FinalizeArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *FinalizeArtifactResponse) GetArtifactId() int64 { + if x != nil { + return x.ArtifactId + } + return 0 +} + +type ListArtifactsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"` + IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *ListArtifactsRequest) Reset() { + *x = ListArtifactsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsRequest) ProtoMessage() {} + +func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead. +func (*ListArtifactsRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{4} +} + +func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue { + if x != nil { + return x.NameFilter + } + return nil +} + +func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value { + if x != nil { + return x.IdFilter + } + return nil +} + +type ListArtifactsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` +} + +func (x *ListArtifactsResponse) Reset() { + *x = ListArtifactsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsResponse) ProtoMessage() {} + +func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead. +func (*ListArtifactsResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{5} +} + +func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact { + if x != nil { + return x.Artifacts + } + return nil +} + +type ListArtifactsResponse_MonolithArtifact struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *ListArtifactsResponse_MonolithArtifact) Reset() { + *x = ListArtifactsResponse_MonolithArtifact{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListArtifactsResponse_MonolithArtifact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {} + +func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead. +func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{6} +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 { + if x != nil { + return x.DatabaseId + } + return 0 +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type GetSignedArtifactURLRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *GetSignedArtifactURLRequest) Reset() { + *x = GetSignedArtifactURLRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSignedArtifactURLRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignedArtifactURLRequest) ProtoMessage() {} + +func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead. +func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{7} +} + +func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *GetSignedArtifactURLRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetSignedArtifactURLResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` +} + +func (x *GetSignedArtifactURLResponse) Reset() { + *x = GetSignedArtifactURLResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSignedArtifactURLResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignedArtifactURLResponse) ProtoMessage() {} + +func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead. +func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{8} +} + +func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { + if x != nil { + return x.SignedUrl + } + return "" +} + +var File_artifact_proto protoreflect.FileDescriptor + +var file_artifact_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, + 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, + 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, + 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, + 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, + 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, + 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8, + 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, + 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, + 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, + 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, + 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, + 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a, + 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, + 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, + 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, + 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, + 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, + 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, + 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_artifact_proto_rawDescOnce sync.Once + file_artifact_proto_rawDescData = file_artifact_proto_rawDesc +) + +func file_artifact_proto_rawDescGZIP() []byte { + file_artifact_proto_rawDescOnce.Do(func() { + file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData) + }) + return file_artifact_proto_rawDescData +} + +var ( + file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 9) + file_artifact_proto_goTypes = []interface{}{ + (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest + (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse + (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest + (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse + (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest + (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse + (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest + (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 10: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 11: google.protobuf.Int64Value + } +) + +var file_artifact_proto_depIdxs = []int32{ + 9, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp + 10, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 10, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 11, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + 9, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_artifact_proto_init() } +func file_artifact_proto_init() { + if File_artifact_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FinalizeArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FinalizeArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListArtifactsResponse_MonolithArtifact); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSignedArtifactURLRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSignedArtifactURLResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_artifact_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_artifact_proto_goTypes, + DependencyIndexes: file_artifact_proto_depIdxs, + MessageInfos: file_artifact_proto_msgTypes, + }.Build() + File_artifact_proto = out.File + file_artifact_proto_rawDesc = nil + file_artifact_proto_goTypes = nil + file_artifact_proto_depIdxs = nil +} diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto new file mode 100644 index 0000000000000..25fbc03a778fb --- /dev/null +++ b/routers/api/actions/artifact.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +package github.actions.results.api.v1; + +message CreateArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; + google.protobuf.Timestamp expires_at = 4; + int32 version = 5; +} + +message CreateArtifactResponse { + bool ok = 1; + string signed_upload_url = 2; +} + +message FinalizeArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; + int64 size = 4; + google.protobuf.StringValue hash = 5; +} + +message FinalizeArtifactResponse { + bool ok = 1; + int64 artifact_id = 2; +} + +message ListArtifactsRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + google.protobuf.StringValue name_filter = 3; + google.protobuf.Int64Value id_filter = 4; +} + +message ListArtifactsResponse { + repeated ListArtifactsResponse_MonolithArtifact artifacts = 1; +} + +message ListArtifactsResponse_MonolithArtifact { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + int64 database_id = 3; + string name = 4; + int64 size = 5; + google.protobuf.Timestamp created_at = 6; +} + +message GetSignedArtifactURLRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; +} + +message GetSignedArtifactURLResponse { + string signed_url = 1; +} diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 0713c8bba8cea..9c677f5617fe0 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -5,11 +5,15 @@ package actions import ( "crypto/md5" + "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" + "hash" "io" "path/filepath" "sort" + "strings" "time" "code.gitea.io/gitea/models/actions" @@ -58,6 +62,32 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, return length, nil } +func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, + artifact *actions.ActionArtifact, + start, contentSize, runID int64, +) (int64, error) { + end := start + contentSize - 1 + // build chunk store path + storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end) + r := ctx.Req.Body + // save chunk to storage + writtenSize, err := st.Save(storagePath, r, -1) + if err != nil { + return -1, fmt.Errorf("save chunk to storage error: %v", err) + } + // if len not match, delete the chunk + if writtenSize != contentSize { + if err := st.Delete(storagePath); err != nil { + log.Error("Error deleting chunk: %s, %v", storagePath, err) + } + return -1, fmt.Errorf("len not match") + } + log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d", + storagePath, contentSize, artifact.ID, start, end) + // return chunk total size + return -1, nil +} + type chunkFileItem struct { RunID int64 ArtifactID int64 @@ -111,14 +141,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int log.Debug("artifact %d chunks not found", art.ID) continue } - if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil { + if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil { return err } } return nil } -func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error { +func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error { sort.Slice(chunks, func(i, j int) bool { return chunks[i].Start < chunks[j].Start }) @@ -157,6 +187,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st readers = append(readers, readCloser) } mergedReader := io.MultiReader(readers...) + shaPrefix := "sha256:" + var hash hash.Hash + if strings.HasPrefix(checksum, shaPrefix) { + hash = sha256.New() + } + if hash != nil { + mergedReader = io.TeeReader(mergedReader, hash) + } // if chunk is gzip, use gz as extension // download-artifact action will use content-encoding header to decide if it should decompress the file @@ -185,6 +223,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st } }() + if hash != nil { + rawChecksum := hash.Sum(nil) + actualChecksum := hex.EncodeToString(rawChecksum) + if !strings.HasSuffix(checksum, actualChecksum) { + return fmt.Errorf("update artifact error checksum is invalid") + } + } + // save storage path to artifact log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath) // if artifact is already uploaded, delete the old file diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go index 381e7eb16ee95..aaf89ef40e6d3 100644 --- a/routers/api/actions/artifacts_utils.go +++ b/routers/api/actions/artifacts_utils.go @@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) { return task, runID, true } +func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { + task := ctx.ActionTask + runID, err := strconv.ParseInt(rawRunID, 10, 64) + if err != nil || task.Job.RunID != runID { + log.Error("Error runID not match") + ctx.Error(http.StatusBadRequest, "run-id does not match") + return nil, 0, false + } + return task, runID, true +} + func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool { paramHash := ctx.Params("artifact_hash") // use artifact name to create upload url diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go new file mode 100644 index 0000000000000..ca249714d38d2 --- /dev/null +++ b/routers/api/actions/artifactsv4.go @@ -0,0 +1,523 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +// GitHub Actions Artifacts V4 API Simple Description +// +// 1. Upload artifact +// 1.1. CreateArtifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact +// Request: +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test", +// "version": 4 +// } +// Response: +// { +// "ok": true, +// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75" +// } +// 1.2. Upload Zip Content to Blobstorage (unauthenticated request) +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block +// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock +// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now +// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList +// 1.5. FinalizeArtifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test", +// "size": "2097", +// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4" +// } +// Response +// { +// "ok": true, +// "artifactId": "4" +// } +// 2. Download artifact +// 2.1. ListArtifacts and optionally filter by artifact exact name or id +// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name_filter": "test" +// } +// Response +// { +// "artifacts": [ +// { +// "workflowRunBackendId": "21", +// "workflowJobRunBackendId": "49", +// "databaseId": "4", +// "name": "test", +// "size": "2093", +// "createdAt": "2024-01-23T00:13:28Z" +// } +// ] +// } +// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact +// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL +// Request +// { +// "workflow_run_backend_id": "21", +// "workflow_job_run_backend_id": "49", +// "name": "test" +// } +// Response +// { +// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76" +// } +// 2.3. Download Zip from Blobstorage (unauthenticated request) +// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76 + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/web" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" + +type artifactV4Routes struct { + prefix string + fs storage.ObjectStorage +} + +func ArtifactV4Contexter() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + base, baseCleanUp := context.NewBaseContext(resp, req) + defer baseCleanUp() + + ctx := &ArtifactContext{Base: base} + ctx.AppendContextValue(artifactContextKey, ctx) + + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } +} + +func ArtifactsV4Routes(prefix string) *web.Route { + m := web.NewRoute() + + r := artifactV4Routes{ + prefix: prefix, + fs: storage.ActionsArtifacts, + } + + m.Group("", func() { + m.Post("CreateArtifact", r.createArtifact) + m.Post("FinalizeArtifact", r.finalizeArtifact) + m.Post("ListArtifacts", r.listArtifacts) + m.Post("GetSignedArtifactURL", r.getSignedArtifactURL) + }, ArtifactContexter()) + m.Group("", func() { + m.Put("UploadArtifact", r.uploadArtifact) + m.Get("DownloadArtifact", r.downloadArtifact) + }, ArtifactV4Contexter()) + + return m +} + +func (r artifactV4Routes) buildArtifactURL(endp, sig, expires, artifactName string, taskID int64) string { + uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + + "/" + endp + "?sig=" + sig + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + return uploadURL +} + +func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { + var req CreateArtifactRequest + + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + err = protojson.Unmarshal(body, &req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifactName := req.Name + + rententionDays := setting.Actions.ArtifactRetentionDays + if req.ExpiresAt != nil { + rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) + } + // create or get artifact with name and path + artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays) + if err != nil { + log.Error("Error create or get artifact: %v", err) + ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + return + } + artifact.ContentEncoding = "application/zip" + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + } + + expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") + mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac.Write([]byte("UploadArtifact")) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(fmt.Sprint(ctx.ActionTask.ID))) + expecedsig := mac.Sum(nil) + sig := base64.URLEncoding.EncodeToString(expecedsig) + respData := CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: r.buildArtifactURL("UploadArtifact", sig, expires, artifactName, ctx.ActionTask.ID), + } + resp, err := protojson.Marshal(&respData) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { + rawTaskID := ctx.Req.URL.Query().Get("taskID") + sig := ctx.Req.URL.Query().Get("sig") + expires := ctx.Req.URL.Query().Get("expires") + artifactName := ctx.Req.URL.Query().Get("artifactName") + dsig, _ := base64.URLEncoding.DecodeString(sig) + + mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac.Write([]byte("UploadArtifact")) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(rawTaskID)) + expecedsig := mac.Sum(nil) + if !hmac.Equal(dsig, expecedsig) { + log.Error("Error unauthorized") + ctx.Error(http.StatusUnauthorized, "Error unauthorized") + return + } + t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) + if err != nil || t.Before(time.Now()) { + log.Error("Error link expired") + ctx.Error(http.StatusUnauthorized, "Error link expired") + return + } + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + task, err := actions.GetTaskByID(ctx, taskID) + if err != nil { + log.Error("Error runner api getting task by ID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + return + } + if task.Status != actions.StatusRunning { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + if err := task.LoadJob(ctx); err != nil { + log.Error("Error runner api getting job: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + return + } + + comp := ctx.Req.URL.Query().Get("comp") + switch comp { + case "block", "appendBlock": + // create or get artifact with name and path + artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactName+".zip", 90) + if err != nil { + log.Error("Error create or get artifact: %v", err) + ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + return + } + + if comp == "block" { + artifact.FileSize = 0 + artifact.FileCompressedSize = 0 + } + + _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + } + artifact.FileCompressedSize += ctx.Req.ContentLength + artifact.FileSize += ctx.Req.ContentLength + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + } + ctx.JSON(201, "appended") + case "blocklist": + ctx.JSON(201, "created") + } +} + +func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { + var req FinalizeArtifactRequest + + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + err = protojson.Unmarshal(body, &req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + // create or get artifact with name and path + artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, req.Name, req.Name+".zip", 90) + if err != nil { + log.Error("Error create or get artifact: %v", err) + ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + return + } + chunkMap, err := listChunksByRunID(r.fs, runID) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + chunks, ok := chunkMap[artifact.ID] + if !ok { + log.Error("Error merge chunks") + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + checksum := "" + if req.Hash != nil { + checksum = req.Hash.Value + } + if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + + respData := FinalizeArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + resp, err := protojson.Marshal(&respData) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { + var req ListArtifactsRequest + + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + err = protojson.Unmarshal(body, &req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + if err != nil { + log.Error("Error getting artifacts: %v", err) + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + if len(artifacts) == 0 { + log.Debug("[artifact] handleListArtifacts, no artifacts") + ctx.Error(http.StatusNotFound) + return + } + + list := []*ListArtifactsResponse_MonolithArtifact{} + + table := map[string]*ListArtifactsResponse_MonolithArtifact{} + for _, artifact := range artifacts { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != "application/zip" { + table[artifact.ArtifactName] = nil + continue + } + + table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{ + Name: artifact.ArtifactName, + CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()), + DatabaseId: artifact.ID, + WorkflowRunBackendId: req.WorkflowRunBackendId, + WorkflowJobRunBackendId: req.WorkflowJobRunBackendId, + Size: artifact.FileSize, + } + } + for _, artifact := range table { + if artifact != nil { + list = append(list, artifact) + } + } + + respData := ListArtifactsResponse{ + Artifacts: list, + } + resp, err := protojson.Marshal(&respData) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { + var req GetSignedArtifactURLRequest + + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + err = protojson.Unmarshal(body, &req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifactName := req.Name + + expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") + mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac.Write([]byte("DownloadArtifact")) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(fmt.Sprint(ctx.ActionTask.ID))) + expecedsig := mac.Sum(nil) + sig := base64.URLEncoding.EncodeToString(expecedsig) + + respData := GetSignedArtifactURLResponse{ + SignedUrl: r.buildArtifactURL("DownloadArtifact", sig, expires, artifactName, ctx.ActionTask.ID), + } + resp, err := protojson.Marshal(&respData) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { + rawTaskID := ctx.Req.URL.Query().Get("taskID") + sig := ctx.Req.URL.Query().Get("sig") + expires := ctx.Req.URL.Query().Get("expires") + artifactName := ctx.Req.URL.Query().Get("artifactName") + dsig, _ := base64.URLEncoding.DecodeString(sig) + + mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac.Write([]byte("DownloadArtifact")) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(rawTaskID)) + expecedsig := mac.Sum(nil) + if !hmac.Equal(dsig, expecedsig) { + log.Error("Error unauthorized") + ctx.Error(http.StatusUnauthorized, "Error unauthorized") + return + } + t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) + if err != nil || t.Before(time.Now()) { + log.Error("Error link expired") + ctx.Error(http.StatusUnauthorized, "Error link expired") + return + } + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + task, err := actions.GetTaskByID(ctx, taskID) + if err != nil { + log.Error("Error runner api getting task by ID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + return + } + if task.Status != actions.StatusRunning { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + if err := task.LoadJob(ctx); err != nil { + log.Error("Error runner api getting job: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + return + } + + // create or get artifact with name and path + artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactName+".zip", 90) + if err != nil { + log.Error("Error create or get artifact: %v", err) + ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + return + } + + file, _ := r.fs.Open(artifact.StoragePath) + + _, _ = io.Copy(ctx.Resp, file) +} diff --git a/routers/init.go b/routers/init.go index e0a7150ba3182..1dedbebeb5c47 100644 --- a/routers/init.go +++ b/routers/init.go @@ -198,6 +198,8 @@ func NormalRoutes() *web.Route { // TODO: this prefix should be generated with a token string with runner ? prefix = "/api/actions_pipeline" r.Mount(prefix, actions_router.ArtifactsRoutes(prefix)) + prefix = actions_router.ArtifactV4RouteBase + r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix)) } return r diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 9cda30d23dc70..2a6f46df0d294 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -564,6 +564,17 @@ func ArtifactsDownloadView(ctx *context_module.Context) { ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) + if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { + art := artifacts[0] + f, err := storage.ActionsArtifacts.Open(art.StoragePath) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + _, _ = io.Copy(ctx.Resp, f) + return + } + writer := zip.NewWriter(ctx.Resp) defer writer.Close() for _, art := range artifacts { diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go new file mode 100644 index 0000000000000..252622fe711d8 --- /dev/null +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -0,0 +1,162 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "io" + "net/http" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/routers/api/actions" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func toProtoJSON(m protoreflect.ProtoMessage) io.Reader { + resp, _ := protojson.Marshal(m) + buf := bytes.Buffer{} + buf.Write(resp) + return &buf +} + +func TestActionsArtifactV4UploadSingleFile(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifact", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + ExpiresAt: timestamppb.New(time.Now().Add(5 * 24 * time.Hour)), + Name: "artifactWithRetentionDays", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithRetentionDays", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4DownloadSingle(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + NameFilter: wrapperspb.String("artifact"), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var listResp actions.ListArtifactsResponse + protojson.Unmarshal(resp.Body.Bytes(), &listResp) + assert.Len(t, listResp.Artifacts, 1) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.GetSignedArtifactURLResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.NotEmpty(t, finalizeResp.SignedUrl) + + req = NewRequest(t, "GET", finalizeResp.SignedUrl) + resp = MakeRequest(t, req, http.StatusOK) + body := strings.Repeat("A", 1024) + assert.Equal(t, resp.Body.String(), body) +} From d3b4226fbff6c1065870d0897f05ce92e8a43536 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 8 Feb 2024 16:18:06 +0100 Subject: [PATCH 02/15] fix all other places use http.StatusXxx constants add missing return on errors --- routers/api/actions/artifactsv4.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index ca249714d38d2..7cc9af7ff030a 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -188,6 +188,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return } expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") @@ -275,16 +276,18 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { if err != nil { log.Error("Error runner api getting task: task is not running") ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return } artifact.FileCompressedSize += ctx.Req.ContentLength artifact.FileSize += ctx.Req.ContentLength if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return } - ctx.JSON(201, "appended") + ctx.JSON(http.StatusCreated, "appended") case "blocklist": - ctx.JSON(201, "created") + ctx.JSON(http.StatusCreated, "created") } } From f689531d5b84db7489315857557f1d6be1dd82b4 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 8 Feb 2024 16:57:19 +0100 Subject: [PATCH 03/15] refactor chunk code to reduce duplication --- routers/api/actions/artifacts_chunks.go | 81 +++++++++++++------------ 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 9c677f5617fe0..5cf41ff591105 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "hash" "io" @@ -22,39 +23,43 @@ import ( "code.gitea.io/gitea/modules/storage" ) -func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, +func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact, - contentSize, runID int64, + contentSize, runID, start, end, length int64, checkMd5 bool, ) (int64, error) { - // parse content-range header, format: bytes 0-1023/146515 - contentRange := ctx.Req.Header.Get("Content-Range") - start, end, length := int64(0), int64(0), int64(0) - if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil { - log.Warn("parse content range error: %v, content-range: %s", err, contentRange) - return -1, fmt.Errorf("parse content range error: %v", err) - } // build chunk store path storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end) - // use io.TeeReader to avoid reading all body to md5 sum. - // it writes data to hasher after reading end - // if hash is not matched, delete the read-end result - hasher := md5.New() - r := io.TeeReader(ctx.Req.Body, hasher) + var r io.Reader = ctx.Req.Body + var hasher hash.Hash + if checkMd5 { + // use io.TeeReader to avoid reading all body to md5 sum. + // it writes data to hasher after reading end + // if hash is not matched, delete the read-end result + hasher = md5.New() + r = io.TeeReader(r, hasher) + } // save chunk to storage writtenSize, err := st.Save(storagePath, r, -1) if err != nil { return -1, fmt.Errorf("save chunk to storage error: %v", err) } - // check md5 - reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header) - chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) - log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) - // if md5 not match, delete the chunk - if reqMd5String != chunkMd5String || writtenSize != contentSize { + var checkErr error + if checkMd5 { + // check md5 + reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header) + chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) + log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) + // if md5 not match, delete the chunk + checkErr = fmt.Errorf("md5 not match") + } + if writtenSize != contentSize { + checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size")) + } + if checkErr != nil { if err := st.Delete(storagePath); err != nil { log.Error("Error deleting chunk: %s, %v", storagePath, err) } - return -1, fmt.Errorf("md5 not match") + return -1, checkErr } log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d", storagePath, contentSize, artifact.ID, start, end) @@ -62,30 +67,26 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, return length, nil } +func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, + artifact *actions.ActionArtifact, + contentSize, runID int64, +) (int64, error) { + // parse content-range header, format: bytes 0-1023/146515 + contentRange := ctx.Req.Header.Get("Content-Range") + start, end, length := int64(0), int64(0), int64(0) + if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil { + log.Warn("parse content range error: %v, content-range: %s", err, contentRange) + return -1, fmt.Errorf("parse content range error: %v", err) + } + return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true) +} + func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext, artifact *actions.ActionArtifact, start, contentSize, runID int64, ) (int64, error) { end := start + contentSize - 1 - // build chunk store path - storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end) - r := ctx.Req.Body - // save chunk to storage - writtenSize, err := st.Save(storagePath, r, -1) - if err != nil { - return -1, fmt.Errorf("save chunk to storage error: %v", err) - } - // if len not match, delete the chunk - if writtenSize != contentSize { - if err := st.Delete(storagePath); err != nil { - log.Error("Error deleting chunk: %s, %v", storagePath, err) - } - return -1, fmt.Errorf("len not match") - } - log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d", - storagePath, contentSize, artifact.ID, start, end) - // return chunk total size - return -1, nil + return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false) } type chunkFileItem struct { From e7ccbea7d2324e66ae79088b79afd47df2ff697f Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 8 Feb 2024 17:17:13 +0100 Subject: [PATCH 04/15] fix md5 check --- routers/api/actions/artifacts_chunks.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 5cf41ff591105..3a81724b3adf3 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -50,7 +50,9 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext, chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) // if md5 not match, delete the chunk - checkErr = fmt.Errorf("md5 not match") + if reqMd5String != chunkMd5String { + checkErr = fmt.Errorf("md5 not match") + } } if writtenSize != contentSize { checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size")) From aedd382cb24d087bafea6f426320434aa7a7622d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 17 Feb 2024 15:59:00 +0100 Subject: [PATCH 05/15] add DeleteArtifact endpoint + test --- routers/api/actions/artifact.pb.go | 182 ++++++++++++++++-- routers/api/actions/artifact.proto | 11 ++ routers/api/actions/artifactsv4.go | 55 ++++++ .../api_actions_artifact_v4_test.go | 18 ++ 4 files changed, 254 insertions(+), 12 deletions(-) diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go index 7390527542b45..5bbc434a90516 100644 --- a/routers/api/actions/artifact.pb.go +++ b/routers/api/actions/artifact.pb.go @@ -609,6 +609,124 @@ func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { return "" } +type DeleteArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *DeleteArtifactRequest) Reset() { + *x = DeleteArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteArtifactRequest) ProtoMessage() {} + +func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead. +func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string { + if x != nil { + return x.WorkflowRunBackendId + } + return "" +} + +func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string { + if x != nil { + return x.WorkflowJobRunBackendId + } + return "" +} + +func (x *DeleteArtifactRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type DeleteArtifactResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` +} + +func (x *DeleteArtifactResponse) Reset() { + *x = DeleteArtifactResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_artifact_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteArtifactResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteArtifactResponse) ProtoMessage() {} + +func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message { + mi := &file_artifact_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead. +func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) { + return file_artifact_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteArtifactResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *DeleteArtifactResponse) GetArtifactId() int64 { + if x != nil { + return x.ArtifactId + } + return 0 +} + var File_artifact_proto protoreflect.FileDescriptor var file_artifact_proto_rawDesc = []byte{ @@ -716,7 +834,22 @@ var file_artifact_proto_rawDesc = []byte{ 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, + 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, + 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, + 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, + 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -732,7 +865,7 @@ func file_artifact_proto_rawDescGZIP() []byte { } var ( - file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 9) + file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) file_artifact_proto_goTypes = []interface{}{ (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse @@ -743,19 +876,20 @@ var ( (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp - (*wrapperspb.StringValue)(nil), // 10: google.protobuf.StringValue - (*wrapperspb.Int64Value)(nil), // 11: google.protobuf.Int64Value + (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest + (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value } ) - var file_artifact_proto_depIdxs = []int32{ - 9, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp - 10, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue - 10, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue - 11, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp + 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - 9, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name @@ -877,6 +1011,30 @@ func file_artifact_proto_init() { return nil } } + file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteArtifactResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -884,7 +1042,7 @@ func file_artifact_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_artifact_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto index 25fbc03a778fb..c68e5d030d09c 100644 --- a/routers/api/actions/artifact.proto +++ b/routers/api/actions/artifact.proto @@ -60,3 +60,14 @@ message GetSignedArtifactURLRequest { message GetSignedArtifactURLResponse { string signed_url = 1; } + +message DeleteArtifactRequest { + string workflow_run_backend_id = 1; + string workflow_job_run_backend_id = 2; + string name = 3; +} + +message DeleteArtifactResponse { + bool ok = 1; + int64 artifact_id = 2; +} diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 7cc9af7ff030a..7329274bc64cb 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -136,6 +136,7 @@ func ArtifactsV4Routes(prefix string) *web.Route { m.Post("FinalizeArtifact", r.finalizeArtifact) m.Post("ListArtifacts", r.listArtifacts) m.Post("GetSignedArtifactURL", r.getSignedArtifactURL) + m.Post("DeleteArtifact", r.deleteArtifact) }, ArtifactContexter()) m.Group("", func() { m.Put("UploadArtifact", r.uploadArtifact) @@ -524,3 +525,57 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { _, _ = io.Copy(ctx.Resp, file) } + +func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { + var req DeleteArtifactRequest + + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + err = protojson.Unmarshal(body, &req) + if err != nil { + log.Error("Error decode request body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error decode request body") + return + } + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + if !ok { + return + } + + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID, ArtifactName: req.Name, ListOptions: db.ListOptions{PageSize: 1}}) + if err != nil { + log.Error("Error getting artifacts: %v", err) + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + if len(artifacts) != 1 { + log.Debug("[artifact] handleListArtifacts, no artifacts") + ctx.Error(http.StatusNotFound) + return + } + + _, err = db.DeleteByID[actions.ActionArtifact](ctx, artifacts[0].ID) + if err != nil { + log.Error("Error getting artifacts: %v", err) + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + respData := DeleteArtifactResponse{ + Ok: true, + ArtifactId: artifacts[0].ID, + } + resp, err := protojson.Marshal(&respData) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 252622fe711d8..225c257b5fcef 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -160,3 +160,21 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) { body := strings.Repeat("A", 1024) assert.Equal(t, resp.Body.String(), body) } + +func TestActionsArtifactV4Delete(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // delete artifact by name + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{ + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var deleteResp actions.DeleteArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &deleteResp) + assert.True(t, deleteResp.Ok) +} From 01d82bf139adc4d8a38eaedbc28f23f41522e25c Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 17 Feb 2024 16:08:46 +0100 Subject: [PATCH 06/15] fix lint --- routers/api/actions/artifact.pb.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go index 5bbc434a90516..590eda9fb9a08 100644 --- a/routers/api/actions/artifact.pb.go +++ b/routers/api/actions/artifact.pb.go @@ -883,6 +883,7 @@ var ( (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value } ) + var file_artifact_proto_depIdxs = []int32{ 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue From 6f2e5505932c3028aa3116998e2dca1353c79e86 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 17 Feb 2024 16:18:43 +0100 Subject: [PATCH 07/15] add comment about v4 zipped artifact --- routers/web/repo/actions/view.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index b51abe6d73d49..15a1a2135396e 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -564,6 +564,8 @@ func ArtifactsDownloadView(ctx *context_module.Context) { ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) + // Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend + // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { art := artifacts[0] f, err := storage.ActionsArtifacts.Open(art.StoragePath) @@ -575,6 +577,8 @@ func ArtifactsDownloadView(ctx *context_module.Context) { return } + // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend + // Those need to be zipped for download writer := zip.NewWriter(ctx.Resp) defer writer.Close() for _, art := range artifacts { From fa92d6ad07cb4a2939f509d7377687170ab63abf Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 14:21:28 +0100 Subject: [PATCH 08/15] Handle MinioConfig.ServeDirect and cleanup dups --- routers/api/actions/artifactsv4.go | 241 ++++++++++++----------------- routers/web/repo/actions/view.go | 8 + 2 files changed, 108 insertions(+), 141 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 7329274bc64cb..b152e8050eca9 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -82,6 +82,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "errors" "fmt" "io" "net/http" @@ -96,9 +97,11 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "google.golang.org/protobuf/encoding/protojson" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -146,25 +149,64 @@ func ArtifactsV4Routes(prefix string) *web.Route { return m } -func (r artifactV4Routes) buildArtifactURL(endp, sig, expires, artifactName string, taskID int64) string { +func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte { + mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac.Write([]byte(endp)) + mac.Write([]byte(expires)) + mac.Write([]byte(artifactName)) + mac.Write([]byte(fmt.Sprint(taskID))) + return mac.Sum(nil) +} + +func (r artifactV4Routes) buildArtifactURL(endp, expires, artifactName string, taskID int64) string { uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + - "/" + endp + "?sig=" + sig + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) return uploadURL } -func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { - var req CreateArtifactRequest +func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { + var art actions.ActionArtifact + has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, name+".zip").Get(&art) + if err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } + return &art, nil +} +func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool { body, err := io.ReadAll(ctx.Req.Body) if err != nil { log.Error("Error decode request body: %v", err) ctx.Error(http.StatusInternalServerError, "Error decode request body") - return + return false } - err = protojson.Unmarshal(body, &req) + err = protojson.Unmarshal(body, req) if err != nil { log.Error("Error decode request body: %v", err) ctx.Error(http.StatusInternalServerError, "Error decode request body") + return false + } + return true +} + +func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) { + resp, err := protojson.Marshal(req) + if err != nil { + log.Error("Error encode response body: %v", err) + ctx.Error(http.StatusInternalServerError, "Error encode response body") + return + } + ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(resp) +} + +func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { + var req CreateArtifactRequest + + if ok := r.parseProtbufBody(ctx, &req); !ok { return } _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) @@ -193,26 +235,11 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { } expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") - mac := hmac.New(sha256.New, []byte(setting.SecretKey)) - mac.Write([]byte("UploadArtifact")) - mac.Write([]byte(expires)) - mac.Write([]byte(artifactName)) - mac.Write([]byte(fmt.Sprint(ctx.ActionTask.ID))) - expecedsig := mac.Sum(nil) - sig := base64.URLEncoding.EncodeToString(expecedsig) respData := CreateArtifactResponse{ Ok: true, - SignedUploadUrl: r.buildArtifactURL("UploadArtifact", sig, expires, artifactName, ctx.ActionTask.ID), - } - resp, err := protojson.Marshal(&respData) - if err != nil { - log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") - return + SignedUploadUrl: r.buildArtifactURL("UploadArtifact", expires, artifactName, ctx.ActionTask.ID), } - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(resp) + r.sendProtbufBody(ctx, &respData) } func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { @@ -221,13 +248,9 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { expires := ctx.Req.URL.Query().Get("expires") artifactName := ctx.Req.URL.Query().Get("artifactName") dsig, _ := base64.URLEncoding.DecodeString(sig) + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) - mac := hmac.New(sha256.New, []byte(setting.SecretKey)) - mac.Write([]byte("UploadArtifact")) - mac.Write([]byte(expires)) - mac.Write([]byte(artifactName)) - mac.Write([]byte(rawTaskID)) - expecedsig := mac.Sum(nil) + expecedsig := r.buildSignature("UploadArtifact", expires, artifactName, taskID) if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") ctx.Error(http.StatusUnauthorized, "Error unauthorized") @@ -239,7 +262,6 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { ctx.Error(http.StatusUnauthorized, "Error link expired") return } - taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) task, err := actions.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) @@ -260,11 +282,11 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { comp := ctx.Req.URL.Query().Get("comp") switch comp { case "block", "appendBlock": - // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactName+".zip", 90) + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) if err != nil { - log.Error("Error create or get artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") return } @@ -295,16 +317,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { var req FinalizeArtifactRequest - body, err := io.ReadAll(ctx.Req.Body) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") - return - } - err = protojson.Unmarshal(body, &req) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + if ok := r.parseProtbufBody(ctx, &req); !ok { return } _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) @@ -312,11 +325,11 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, req.Name, req.Name+".zip", 90) + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, req.Name) if err != nil { - log.Error("Error create or get artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") return } chunkMap, err := listChunksByRunID(r.fs, runID) @@ -345,30 +358,13 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { Ok: true, ArtifactId: artifact.ID, } - resp, err := protojson.Marshal(&respData) - if err != nil { - log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") - return - } - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(resp) + r.sendProtbufBody(ctx, &respData) } func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { var req ListArtifactsRequest - body, err := io.ReadAll(ctx.Req.Body) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") - return - } - err = protojson.Unmarshal(body, &req) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + if ok := r.parseProtbufBody(ctx, &req); !ok { return } _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) @@ -415,60 +411,43 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { respData := ListArtifactsResponse{ Artifacts: list, } - resp, err := protojson.Marshal(&respData) - if err != nil { - log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") - return - } - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(resp) + r.sendProtbufBody(ctx, &respData) } func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { var req GetSignedArtifactURLRequest - body, err := io.ReadAll(ctx.Req.Body) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + if ok := r.parseProtbufBody(ctx, &req); !ok { return } - err = protojson.Unmarshal(body, &req) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") - return - } - _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) + _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) if !ok { return } artifactName := req.Name - expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") - mac := hmac.New(sha256.New, []byte(setting.SecretKey)) - mac.Write([]byte("DownloadArtifact")) - mac.Write([]byte(expires)) - mac.Write([]byte(artifactName)) - mac.Write([]byte(fmt.Sprint(ctx.ActionTask.ID))) - expecedsig := mac.Sum(nil) - sig := base64.URLEncoding.EncodeToString(expecedsig) - - respData := GetSignedArtifactURLResponse{ - SignedUrl: r.buildArtifactURL("DownloadArtifact", sig, expires, artifactName, ctx.ActionTask.ID), - } - resp, err := protojson.Marshal(&respData) + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, artifactName) if err != nil { - log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") return } - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(resp) + + respData := GetSignedArtifactURLResponse{} + + if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { + u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath) + if u != nil && err == nil { + respData.SignedUrl = u.String() + } + } + if respData.SignedUrl == "" { + expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") + respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", expires, artifactName, ctx.ActionTask.ID) + } + r.sendProtbufBody(ctx, &respData) } func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { @@ -483,7 +462,8 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) mac.Write([]byte(rawTaskID)) - expecedsig := mac.Sum(nil) + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + expecedsig := r.buildSignature("DownloadArtifact", expires, artifactName, taskID) if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") ctx.Error(http.StatusUnauthorized, "Error unauthorized") @@ -495,7 +475,6 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { ctx.Error(http.StatusUnauthorized, "Error link expired") return } - taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) task, err := actions.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) @@ -513,11 +492,11 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { return } - // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactName+".zip", 90) + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) if err != nil { - log.Error("Error create or get artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") return } @@ -529,16 +508,7 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { var req DeleteArtifactRequest - body, err := io.ReadAll(ctx.Req.Body) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") - return - } - err = protojson.Unmarshal(body, &req) - if err != nil { - log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + if ok := r.parseProtbufBody(ctx, &req); !ok { return } _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) @@ -546,36 +516,25 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID, ArtifactName: req.Name, ListOptions: db.ListOptions{PageSize: 1}}) + // get artifact by name + artifact, err := r.getArtifactByName(ctx, runID, req.Name) if err != nil { - log.Error("Error getting artifacts: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - if len(artifacts) != 1 { - log.Debug("[artifact] handleListArtifacts, no artifacts") - ctx.Error(http.StatusNotFound) + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") return } - _, err = db.DeleteByID[actions.ActionArtifact](ctx, artifacts[0].ID) + _, err = db.DeleteByID[actions.ActionArtifact](ctx, artifact.ID) + err = errors.Join(err, r.fs.Delete(artifact.StoragePath)) if err != nil { - log.Error("Error getting artifacts: %v", err) + log.Error("Error deleting artifacts: %v", err) ctx.Error(http.StatusInternalServerError, err.Error()) return } respData := DeleteArtifactResponse{ Ok: true, - ArtifactId: artifacts[0].ID, - } - resp, err := protojson.Marshal(&respData) - if err != nil { - log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") - return + ArtifactId: artifact.ID, } - ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(resp) + r.sendProtbufBody(ctx, &respData) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 15a1a2135396e..832475a004007 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" context_module "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -568,6 +569,13 @@ func ArtifactsDownloadView(ctx *context_module.Context) { // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { art := artifacts[0] + if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { + u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath) + if u != nil && err == nil { + ctx.Redirect(u.String()) + return + } + } f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) From 374df494fae92851368365c6a4a9b5d2843e8bf1 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 16:36:27 +0100 Subject: [PATCH 09/15] remove old code --- routers/api/actions/artifactsv4.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index b152e8050eca9..fba1a2226bafb 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -457,11 +457,6 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { artifactName := ctx.Req.URL.Query().Get("artifactName") dsig, _ := base64.URLEncoding.DecodeString(sig) - mac := hmac.New(sha256.New, []byte(setting.SecretKey)) - mac.Write([]byte("DownloadArtifact")) - mac.Write([]byte(expires)) - mac.Write([]byte(artifactName)) - mac.Write([]byte(rawTaskID)) taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) expecedsig := r.buildSignature("DownloadArtifact", expires, artifactName, taskID) if !hmac.Equal(dsig, expecedsig) { From 68ca3948c1029d2ebf6322fc87efb8ab6c755be9 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 16:57:49 +0100 Subject: [PATCH 10/15] move verifySignature into it's own method --- routers/api/actions/artifactsv4.go | 110 ++++++++++++----------------- 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index fba1a2226bafb..ca5171dde3231 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -164,6 +164,46 @@ func (r artifactV4Routes) buildArtifactURL(endp, expires, artifactName string, t return uploadURL } +func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (task *actions.ActionTask, artifactName string, ok bool) { + rawTaskID := ctx.Req.URL.Query().Get("taskID") + sig := ctx.Req.URL.Query().Get("sig") + expires := ctx.Req.URL.Query().Get("expires") + artifactName = ctx.Req.URL.Query().Get("artifactName") + dsig, _ := base64.URLEncoding.DecodeString(sig) + taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + + expecedsig := r.buildSignature(endp, expires, artifactName, taskID) + if !hmac.Equal(dsig, expecedsig) { + log.Error("Error unauthorized") + ctx.Error(http.StatusUnauthorized, "Error unauthorized") + return + } + t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) + if err != nil || t.Before(time.Now()) { + log.Error("Error link expired") + ctx.Error(http.StatusUnauthorized, "Error link expired") + return + } + task, err = actions.GetTaskByID(ctx, taskID) + if err != nil { + log.Error("Error runner api getting task by ID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + return + } + if task.Status != actions.StatusRunning { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + if err := task.LoadJob(ctx); err != nil { + log.Error("Error runner api getting job: %v", err) + ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + return + } + ok = true + return +} + func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { var art actions.ActionArtifact has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, name+".zip").Get(&art) @@ -243,39 +283,8 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { } func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { - rawTaskID := ctx.Req.URL.Query().Get("taskID") - sig := ctx.Req.URL.Query().Get("sig") - expires := ctx.Req.URL.Query().Get("expires") - artifactName := ctx.Req.URL.Query().Get("artifactName") - dsig, _ := base64.URLEncoding.DecodeString(sig) - taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) - - expecedsig := r.buildSignature("UploadArtifact", expires, artifactName, taskID) - if !hmac.Equal(dsig, expecedsig) { - log.Error("Error unauthorized") - ctx.Error(http.StatusUnauthorized, "Error unauthorized") - return - } - t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) - if err != nil || t.Before(time.Now()) { - log.Error("Error link expired") - ctx.Error(http.StatusUnauthorized, "Error link expired") - return - } - task, err := actions.GetTaskByID(ctx, taskID) - if err != nil { - log.Error("Error runner api getting task by ID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") - return - } - if task.Status != actions.StatusRunning { - log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") - return - } - if err := task.LoadJob(ctx); err != nil { - log.Error("Error runner api getting job: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact") + if !ok { return } @@ -451,39 +460,8 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { } func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { - rawTaskID := ctx.Req.URL.Query().Get("taskID") - sig := ctx.Req.URL.Query().Get("sig") - expires := ctx.Req.URL.Query().Get("expires") - artifactName := ctx.Req.URL.Query().Get("artifactName") - dsig, _ := base64.URLEncoding.DecodeString(sig) - - taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) - expecedsig := r.buildSignature("DownloadArtifact", expires, artifactName, taskID) - if !hmac.Equal(dsig, expecedsig) { - log.Error("Error unauthorized") - ctx.Error(http.StatusUnauthorized, "Error unauthorized") - return - } - t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) - if err != nil || t.Before(time.Now()) { - log.Error("Error link expired") - ctx.Error(http.StatusUnauthorized, "Error link expired") - return - } - task, err := actions.GetTaskByID(ctx, taskID) - if err != nil { - log.Error("Error runner api getting task by ID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") - return - } - if task.Status != actions.StatusRunning { - log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") - return - } - if err := task.LoadJob(ctx); err != nil { - log.Error("Error runner api getting job: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact") + if !ok { return } From f47f679f5af62b4624ea67c7a2ccffa6f964b745 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 17:28:15 +0100 Subject: [PATCH 11/15] extract content encoding and unify expire time --- routers/api/actions/artifactsv4.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index ca5171dde3231..4ed98b5e8222d 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -106,6 +106,7 @@ import ( ) const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" +const ArtifactV4ContentEncoding = "application/zip" type artifactV4Routes struct { prefix string @@ -158,7 +159,8 @@ func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, tas return mac.Sum(nil) } -func (r artifactV4Routes) buildArtifactURL(endp, expires, artifactName string, taskID int64) string { +func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string { + expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) return uploadURL @@ -206,7 +208,7 @@ func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (ta func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { var art actions.ActionArtifact - has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, name+".zip").Get(&art) + has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) if err != nil { return nil, err } else if !has { @@ -267,17 +269,16 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { ctx.Error(http.StatusInternalServerError, "Error create or get artifact") return } - artifact.ContentEncoding = "application/zip" + artifact.ContentEncoding = ArtifactV4ContentEncoding if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") return } - expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") respData := CreateArtifactResponse{ Ok: true, - SignedUploadUrl: r.buildArtifactURL("UploadArtifact", expires, artifactName, ctx.ActionTask.ID), + SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID), } r.sendProtbufBody(ctx, &respData) } @@ -397,7 +398,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { table := map[string]*ListArtifactsResponse_MonolithArtifact{} for _, artifact := range artifacts { - if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != "application/zip" { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { table[artifact.ArtifactName] = nil continue } @@ -453,8 +454,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { } } if respData.SignedUrl == "" { - expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") - respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", expires, artifactName, ctx.ActionTask.ID) + respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID) } r.sendProtbufBody(ctx, &respData) } From d48b7c20c61c0e949b4abbb93006a29f12ebe81b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 17:39:16 +0100 Subject: [PATCH 12/15] Add invalid checksum test / fix fmt --- routers/api/actions/artifactsv4.go | 6 ++- .../api_actions_artifact_v4_test.go | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 4ed98b5e8222d..29783032827c3 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -105,8 +105,10 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" -const ArtifactV4ContentEncoding = "application/zip" +const ( + ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" + ArtifactV4ContentEncoding = "application/zip" +) type artifactV4Routes struct { prefix string diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 225c257b5fcef..f58f876849bb0 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -78,6 +78,50 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { assert.True(t, finalizeResp.Ok) } +func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifact-invalid-checksum", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(strings.Repeat("A", 1024))) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifact-invalid-checksum", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusInternalServerError) +} + func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { defer tests.PrepareTestEnv(t)() From fb733ad4f21a82924ad3f794e4427a242d991b55 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 17:50:00 +0100 Subject: [PATCH 13/15] fix style --- routers/api/actions/artifactsv4.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 29783032827c3..f5484e7fc6337 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -168,11 +168,11 @@ func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int return uploadURL } -func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (task *actions.ActionTask, artifactName string, ok bool) { +func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") sig := ctx.Req.URL.Query().Get("sig") expires := ctx.Req.URL.Query().Get("expires") - artifactName = ctx.Req.URL.Query().Get("artifactName") + artifactName := ctx.Req.URL.Query().Get("artifactName") dsig, _ := base64.URLEncoding.DecodeString(sig) taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) @@ -180,32 +180,31 @@ func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (ta if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") ctx.Error(http.StatusUnauthorized, "Error unauthorized") - return + return nil, "", false } t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) if err != nil || t.Before(time.Now()) { log.Error("Error link expired") ctx.Error(http.StatusUnauthorized, "Error link expired") - return + return nil, "", false } - task, err = actions.GetTaskByID(ctx, taskID) + task, err := actions.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") - return + return nil, "", false } if task.Status != actions.StatusRunning { log.Error("Error runner api getting task: task is not running") ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") - return + return nil, "", false } if err := task.LoadJob(ctx); err != nil { log.Error("Error runner api getting job: %v", err) ctx.Error(http.StatusInternalServerError, "Error runner api getting job") - return + return nil, "", false } - ok = true - return + return task, artifactName, true } func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { From 745f1007cec3af5fc3ae237c21a11ae3634443ff Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 20:51:03 +0100 Subject: [PATCH 14/15] prefer `setting.GetGeneralTokenSigningSecret()` --- routers/api/actions/artifactsv4.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index f5484e7fc6337..f5ea3b607bb0b 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -153,7 +153,7 @@ func ArtifactsV4Routes(prefix string) *web.Route { } func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte { - mac := hmac.New(sha256.New, []byte(setting.SecretKey)) + mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endp)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) From ff30004285ad35190768d1ce787df03a5d59e578 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 18 Feb 2024 21:17:55 +0100 Subject: [PATCH 15/15] actions.SetArtifactNeedDelete in deleteArtifact --- routers/api/actions/artifactsv4.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index f5ea3b607bb0b..724b3261ac8c6 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -82,7 +82,6 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" - "errors" "fmt" "io" "net/http" @@ -498,8 +497,7 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { return } - _, err = db.DeleteByID[actions.ActionArtifact](ctx, artifact.ID) - err = errors.Join(err, r.fs.Delete(artifact.StoragePath)) + err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) if err != nil { log.Error("Error deleting artifacts: %v", err) ctx.Error(http.StatusInternalServerError, err.Error())