From ecf111332f76b24e3e893d41863f63f3961cfb73 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 27 Jan 2022 14:59:54 +0000 Subject: [PATCH 1/3] [image-builder, et. al] Return the public workspace URL for all imagebuilds (incl. extra headers to access said URL) and store it in workspace --- .../typeorm/entity/db-workspace-instance.ts | 4 +- .../migration/1643724132624-ImageBuildInfo.ts | 21 ++ .../gitpod-protocol/src/workspace-instance.ts | 20 ++ .../image-builder-api/go/imgbuilder.pb.go | 135 ++++++++-- .../go/imgbuilder_grpc.pb.go | 2 +- components/image-builder-api/go/mock/mock.go | 2 +- components/image-builder-api/imgbuilder.proto | 6 + .../typescript/src/imgbuilder_grpc_pb.d.ts | 2 +- .../typescript/src/imgbuilder_grpc_pb.js | 2 +- .../typescript/src/imgbuilder_pb.d.ts | 33 ++- .../typescript/src/imgbuilder_pb.js | 240 +++++++++++++++++- .../image-builder-api/typescript/src/sugar.ts | 25 +- components/image-builder-mk3/go.mod | 11 + components/image-builder-mk3/go.sum | 38 +++ .../pkg/orchestrator/monitor.go | 6 + .../pkg/orchestrator/monitor_test.go | 24 +- .../server/src/workspace/workspace-starter.ts | 16 +- 17 files changed, 549 insertions(+), 38 deletions(-) create mode 100644 components/gitpod-db/src/typeorm/migration/1643724132624-ImageBuildInfo.ts diff --git a/components/gitpod-db/src/typeorm/entity/db-workspace-instance.ts b/components/gitpod-db/src/typeorm/entity/db-workspace-instance.ts index a049ef1a4efc07..bf88da13a2ab35 100644 --- a/components/gitpod-db/src/typeorm/entity/db-workspace-instance.ts +++ b/components/gitpod-db/src/typeorm/entity/db-workspace-instance.ts @@ -6,7 +6,7 @@ import { PrimaryColumn, Column, Index, Entity } from "typeorm"; -import { WorkspaceInstance, WorkspaceInstanceStatus, WorkspaceInstancePhase, WorkspaceInstanceConfiguration } from "@gitpod/gitpod-protocol"; +import { WorkspaceInstance, WorkspaceInstanceStatus, WorkspaceInstancePhase, WorkspaceInstanceConfiguration, ImageBuildInfo } from "@gitpod/gitpod-protocol"; import { TypeORM } from "../typeorm"; import { Transformer } from "../transformer"; @@ -87,4 +87,6 @@ export class DBWorkspaceInstance implements WorkspaceInstance { }) configuration?: WorkspaceInstanceConfiguration; + @Column("simple-json", { nullable: true }) + imageBuildInfo?: ImageBuildInfo; } \ No newline at end of file diff --git a/components/gitpod-db/src/typeorm/migration/1643724132624-ImageBuildInfo.ts b/components/gitpod-db/src/typeorm/migration/1643724132624-ImageBuildInfo.ts new file mode 100644 index 00000000000000..7edcef68b230b8 --- /dev/null +++ b/components/gitpod-db/src/typeorm/migration/1643724132624-ImageBuildInfo.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import {MigrationInterface, QueryRunner} from "typeorm"; +import { columnExists } from "./helper/helper"; + +export class ImageBuildInfo1643724132624 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + if (!(await columnExists(queryRunner, "d_b_workspace_instance", "imageBuildInfo"))) { + await queryRunner.query("ALTER TABLE d_b_workspace_instance ADD COLUMN `imageBuildInfo` text NULL"); + } + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/components/gitpod-protocol/src/workspace-instance.ts b/components/gitpod-protocol/src/workspace-instance.ts index e935e0cf7b18d4..8305dd84d273e9 100644 --- a/components/gitpod-protocol/src/workspace-instance.ts +++ b/components/gitpod-protocol/src/workspace-instance.ts @@ -51,6 +51,11 @@ export interface WorkspaceInstance { // instance is hard-deleted on the database and about to be collected by db-sync readonly deleted?: boolean; + + /** + * Contains information about the image build, if there was any + */ + imageBuildInfo?: ImageBuildInfo; } // WorkspaceInstanceStatus describes the current state of a workspace instance @@ -213,3 +218,18 @@ export interface WorkspaceInstanceConfiguration { // supervisorImage is the ref of the supervisor image this instance uses. supervisorImage?: string; } + +/** + * Holds information about the image build (if there was one) for this WorkspaceInstance + */ +export interface ImageBuildInfo { + log?: ImageBuildLogInfo, +} + +/** + * Holds information about how to access logs for this an image build + */ +export interface ImageBuildLogInfo { + url: string, + headers: { [key: string]: string }, +} diff --git a/components/image-builder-api/go/imgbuilder.pb.go b/components/image-builder-api/go/imgbuilder.pb.go index 56e1f4e2ee631c..5f28b4f2fcae64 100644 --- a/components/image-builder-api/go/imgbuilder.pb.go +++ b/components/image-builder-api/go/imgbuilder.pb.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. @@ -1036,6 +1036,7 @@ type BuildInfo struct { Status BuildStatus `protobuf:"varint,2,opt,name=status,proto3,enum=builder.BuildStatus" json:"status,omitempty"` StartedAt int64 `protobuf:"varint,3,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"` BuildId string `protobuf:"bytes,5,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` + LogInfo *LogInfo `protobuf:"bytes,6,opt,name=log_info,json=logInfo,proto3" json:"log_info,omitempty"` } func (x *BuildInfo) Reset() { @@ -1105,6 +1106,68 @@ func (x *BuildInfo) GetBuildId() string { return "" } +func (x *BuildInfo) GetLogInfo() *LogInfo { + if x != nil { + return x.LogInfo + } + return nil +} + +type LogInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Headers map[string]string `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *LogInfo) Reset() { + *x = LogInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_imgbuilder_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LogInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogInfo) ProtoMessage() {} + +func (x *LogInfo) ProtoReflect() protoreflect.Message { + mi := &file_imgbuilder_proto_msgTypes[17] + 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 LogInfo.ProtoReflect.Descriptor instead. +func (*LogInfo) Descriptor() ([]byte, []int) { + return file_imgbuilder_proto_rawDescGZIP(), []int{17} +} + +func (x *LogInfo) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *LogInfo) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + var File_imgbuilder_proto protoreflect.FileDescriptor var file_imgbuilder_proto_rawDesc = []byte{ @@ -1216,7 +1279,7 @@ var file_imgbuilder_proto_rawDesc = []byte{ 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x22, 0xa0, 0x01, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, + 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x22, 0xcd, 0x01, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x52, @@ -1226,7 +1289,19 @@ var file_imgbuilder_proto_rawDesc = []byte{ 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x2a, 0x4b, 0x0a, 0x0b, 0x42, 0x75, + 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x08, 0x6c, 0x6f, + 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, + 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x37, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, + 0x2e, 0x4c, 0x6f, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x3a, + 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x4b, 0x0a, 0x0b, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x64, 0x6f, 0x6e, 0x65, 0x5f, 0x73, 0x75, 0x63, 0x63, @@ -1276,7 +1351,7 @@ func file_imgbuilder_proto_rawDescGZIP() []byte { } var file_imgbuilder_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_imgbuilder_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_imgbuilder_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_imgbuilder_proto_goTypes = []interface{}{ (BuildStatus)(0), // 0: builder.BuildStatus (*BuildSource)(nil), // 1: builder.BuildSource @@ -1296,12 +1371,14 @@ var file_imgbuilder_proto_goTypes = []interface{}{ (*ListBuildsRequest)(nil), // 15: builder.ListBuildsRequest (*ListBuildsResponse)(nil), // 16: builder.ListBuildsResponse (*BuildInfo)(nil), // 17: builder.BuildInfo - (*api.WorkspaceInitializer)(nil), // 18: contentservice.WorkspaceInitializer + (*LogInfo)(nil), // 18: builder.LogInfo + nil, // 19: builder.LogInfo.HeadersEntry + (*api.WorkspaceInitializer)(nil), // 20: contentservice.WorkspaceInitializer } var file_imgbuilder_proto_depIdxs = []int32{ 2, // 0: builder.BuildSource.ref:type_name -> builder.BuildSourceReference 3, // 1: builder.BuildSource.file:type_name -> builder.BuildSourceDockerfile - 18, // 2: builder.BuildSourceDockerfile.source:type_name -> contentservice.WorkspaceInitializer + 20, // 2: builder.BuildSourceDockerfile.source:type_name -> contentservice.WorkspaceInitializer 9, // 3: builder.ResolveBaseImageRequest.auth:type_name -> builder.BuildRegistryAuth 1, // 4: builder.ResolveWorkspaceImageRequest.source:type_name -> builder.BuildSource 9, // 5: builder.ResolveWorkspaceImageRequest.auth:type_name -> builder.BuildRegistryAuth @@ -1314,21 +1391,23 @@ var file_imgbuilder_proto_depIdxs = []int32{ 17, // 12: builder.BuildResponse.info:type_name -> builder.BuildInfo 17, // 13: builder.ListBuildsResponse.builds:type_name -> builder.BuildInfo 0, // 14: builder.BuildInfo.status:type_name -> builder.BuildStatus - 4, // 15: builder.ImageBuilder.ResolveBaseImage:input_type -> builder.ResolveBaseImageRequest - 6, // 16: builder.ImageBuilder.ResolveWorkspaceImage:input_type -> builder.ResolveWorkspaceImageRequest - 8, // 17: builder.ImageBuilder.Build:input_type -> builder.BuildRequest - 13, // 18: builder.ImageBuilder.Logs:input_type -> builder.LogsRequest - 15, // 19: builder.ImageBuilder.ListBuilds:input_type -> builder.ListBuildsRequest - 5, // 20: builder.ImageBuilder.ResolveBaseImage:output_type -> builder.ResolveBaseImageResponse - 7, // 21: builder.ImageBuilder.ResolveWorkspaceImage:output_type -> builder.ResolveWorkspaceImageResponse - 12, // 22: builder.ImageBuilder.Build:output_type -> builder.BuildResponse - 14, // 23: builder.ImageBuilder.Logs:output_type -> builder.LogsResponse - 16, // 24: builder.ImageBuilder.ListBuilds:output_type -> builder.ListBuildsResponse - 20, // [20:25] is the sub-list for method output_type - 15, // [15:20] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 18, // 15: builder.BuildInfo.log_info:type_name -> builder.LogInfo + 19, // 16: builder.LogInfo.headers:type_name -> builder.LogInfo.HeadersEntry + 4, // 17: builder.ImageBuilder.ResolveBaseImage:input_type -> builder.ResolveBaseImageRequest + 6, // 18: builder.ImageBuilder.ResolveWorkspaceImage:input_type -> builder.ResolveWorkspaceImageRequest + 8, // 19: builder.ImageBuilder.Build:input_type -> builder.BuildRequest + 13, // 20: builder.ImageBuilder.Logs:input_type -> builder.LogsRequest + 15, // 21: builder.ImageBuilder.ListBuilds:input_type -> builder.ListBuildsRequest + 5, // 22: builder.ImageBuilder.ResolveBaseImage:output_type -> builder.ResolveBaseImageResponse + 7, // 23: builder.ImageBuilder.ResolveWorkspaceImage:output_type -> builder.ResolveWorkspaceImageResponse + 12, // 24: builder.ImageBuilder.Build:output_type -> builder.BuildResponse + 14, // 25: builder.ImageBuilder.Logs:output_type -> builder.LogsResponse + 16, // 26: builder.ImageBuilder.ListBuilds:output_type -> builder.ListBuildsResponse + 22, // [22:27] is the sub-list for method output_type + 17, // [17:22] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_imgbuilder_proto_init() } @@ -1541,6 +1620,18 @@ func file_imgbuilder_proto_init() { return nil } } + file_imgbuilder_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LogInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_imgbuilder_proto_msgTypes[0].OneofWrappers = []interface{}{ (*BuildSource_Ref)(nil), @@ -1556,7 +1647,7 @@ func file_imgbuilder_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_imgbuilder_proto_rawDesc, NumEnums: 1, - NumMessages: 17, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/components/image-builder-api/go/imgbuilder_grpc.pb.go b/components/image-builder-api/go/imgbuilder_grpc.pb.go index e898db39a39bb1..aa369bc3f3fb1b 100644 --- a/components/image-builder-api/go/imgbuilder_grpc.pb.go +++ b/components/image-builder-api/go/imgbuilder_grpc.pb.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. diff --git a/components/image-builder-api/go/mock/mock.go b/components/image-builder-api/go/mock/mock.go index baf0988418c386..d9a3f7a5f8d018 100644 --- a/components/image-builder-api/go/mock/mock.go +++ b/components/image-builder-api/go/mock/mock.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. diff --git a/components/image-builder-api/imgbuilder.proto b/components/image-builder-api/imgbuilder.proto index a9fe94d97341ba..a7ea091a51efd1 100644 --- a/components/image-builder-api/imgbuilder.proto +++ b/components/image-builder-api/imgbuilder.proto @@ -128,4 +128,10 @@ message BuildInfo { BuildStatus status = 2; int64 started_at = 3; string build_id = 5; + LogInfo log_info = 6; +} + +message LogInfo { + string url = 1; + map headers = 2; } diff --git a/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.d.ts b/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.d.ts index 65b5abb7a8491c..5da61588bf13a1 100644 --- a/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.d.ts +++ b/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ diff --git a/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.js b/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.js index ce48ec35040fa7..2c7303cd2c81d2 100644 --- a/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.js +++ b/components/image-builder-api/typescript/src/imgbuilder_grpc_pb.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ diff --git a/components/image-builder-api/typescript/src/imgbuilder_pb.d.ts b/components/image-builder-api/typescript/src/imgbuilder_pb.d.ts index c23741e0bb87db..c1a17cac6dc666 100644 --- a/components/image-builder-api/typescript/src/imgbuilder_pb.d.ts +++ b/components/image-builder-api/typescript/src/imgbuilder_pb.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ @@ -456,6 +456,11 @@ export class BuildInfo extends jspb.Message { getBuildId(): string; setBuildId(value: string): BuildInfo; + hasLogInfo(): boolean; + clearLogInfo(): void; + getLogInfo(): LogInfo | undefined; + setLogInfo(value?: LogInfo): BuildInfo; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): BuildInfo.AsObject; static toObject(includeInstance: boolean, msg: BuildInfo): BuildInfo.AsObject; @@ -473,6 +478,32 @@ export namespace BuildInfo { status: BuildStatus, startedAt: number, buildId: string, + logInfo?: LogInfo.AsObject, + } +} + +export class LogInfo extends jspb.Message { + getUrl(): string; + setUrl(value: string): LogInfo; + + getHeadersMap(): jspb.Map; + clearHeadersMap(): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): LogInfo.AsObject; + static toObject(includeInstance: boolean, msg: LogInfo): LogInfo.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: LogInfo, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): LogInfo; + static deserializeBinaryFromReader(message: LogInfo, reader: jspb.BinaryReader): LogInfo; +} + +export namespace LogInfo { + export type AsObject = { + url: string, + + headersMap: Array<[string, string]>, } } diff --git a/components/image-builder-api/typescript/src/imgbuilder_pb.js b/components/image-builder-api/typescript/src/imgbuilder_pb.js index 7a30031017d145..cdd94a9affa125 100644 --- a/components/image-builder-api/typescript/src/imgbuilder_pb.js +++ b/components/image-builder-api/typescript/src/imgbuilder_pb.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ @@ -43,6 +43,7 @@ goog.exportSymbol('proto.builder.BuildSourceReference', null, global); goog.exportSymbol('proto.builder.BuildStatus', null, global); goog.exportSymbol('proto.builder.ListBuildsRequest', null, global); goog.exportSymbol('proto.builder.ListBuildsResponse', null, global); +goog.exportSymbol('proto.builder.LogInfo', null, global); goog.exportSymbol('proto.builder.LogsRequest', null, global); goog.exportSymbol('proto.builder.LogsResponse', null, global); goog.exportSymbol('proto.builder.ResolveBaseImageRequest', null, global); @@ -406,6 +407,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.builder.BuildInfo.displayName = 'proto.builder.BuildInfo'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.builder.LogInfo = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.builder.LogInfo, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.builder.LogInfo.displayName = 'proto.builder.LogInfo'; +} /** * Oneof group definitions for this message. Each group defines the field @@ -3426,7 +3448,8 @@ proto.builder.BuildInfo.toObject = function(includeInstance, msg) { baseRef: jspb.Message.getFieldWithDefault(msg, 4, ""), status: jspb.Message.getFieldWithDefault(msg, 2, 0), startedAt: jspb.Message.getFieldWithDefault(msg, 3, 0), - buildId: jspb.Message.getFieldWithDefault(msg, 5, "") + buildId: jspb.Message.getFieldWithDefault(msg, 5, ""), + logInfo: (f = msg.getLogInfo()) && proto.builder.LogInfo.toObject(includeInstance, f) }; if (includeInstance) { @@ -3483,6 +3506,11 @@ proto.builder.BuildInfo.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readString()); msg.setBuildId(value); break; + case 6: + var value = new proto.builder.LogInfo; + reader.readMessage(value,proto.builder.LogInfo.deserializeBinaryFromReader); + msg.setLogInfo(value); + break; default: reader.skipField(); break; @@ -3547,6 +3575,14 @@ proto.builder.BuildInfo.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getLogInfo(); + if (f != null) { + writer.writeMessage( + 6, + f, + proto.builder.LogInfo.serializeBinaryToWriter + ); + } }; @@ -3640,6 +3676,206 @@ proto.builder.BuildInfo.prototype.setBuildId = function(value) { }; +/** + * optional LogInfo log_info = 6; + * @return {?proto.builder.LogInfo} + */ +proto.builder.BuildInfo.prototype.getLogInfo = function() { + return /** @type{?proto.builder.LogInfo} */ ( + jspb.Message.getWrapperField(this, proto.builder.LogInfo, 6)); +}; + + +/** + * @param {?proto.builder.LogInfo|undefined} value + * @return {!proto.builder.BuildInfo} returns this +*/ +proto.builder.BuildInfo.prototype.setLogInfo = function(value) { + return jspb.Message.setWrapperField(this, 6, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.builder.BuildInfo} returns this + */ +proto.builder.BuildInfo.prototype.clearLogInfo = function() { + return this.setLogInfo(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.builder.BuildInfo.prototype.hasLogInfo = function() { + return jspb.Message.getField(this, 6) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.builder.LogInfo.prototype.toObject = function(opt_includeInstance) { + return proto.builder.LogInfo.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.builder.LogInfo} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.builder.LogInfo.toObject = function(includeInstance, msg) { + var f, obj = { + url: jspb.Message.getFieldWithDefault(msg, 1, ""), + headersMap: (f = msg.getHeadersMap()) ? f.toObject(includeInstance, undefined) : [] + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.builder.LogInfo} + */ +proto.builder.LogInfo.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.builder.LogInfo; + return proto.builder.LogInfo.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.builder.LogInfo} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.builder.LogInfo} + */ +proto.builder.LogInfo.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setUrl(value); + break; + case 2: + var value = msg.getHeadersMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString, null, "", ""); + }); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.builder.LogInfo.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.builder.LogInfo.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.builder.LogInfo} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.builder.LogInfo.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUrl(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getHeadersMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(2, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); + } +}; + + +/** + * optional string url = 1; + * @return {string} + */ +proto.builder.LogInfo.prototype.getUrl = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.builder.LogInfo} returns this + */ +proto.builder.LogInfo.prototype.setUrl = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * map headers = 2; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.builder.LogInfo.prototype.getHeadersMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 2, opt_noLazyCreate, + null)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.builder.LogInfo} returns this + */ +proto.builder.LogInfo.prototype.clearHeadersMap = function() { + this.getHeadersMap().clear(); + return this;}; + + /** * @enum {number} */ diff --git a/components/image-builder-api/typescript/src/sugar.ts b/components/image-builder-api/typescript/src/sugar.ts index ca70d8dddb188b..679fbfad85ff41 100644 --- a/components/image-builder-api/typescript/src/sugar.ts +++ b/components/image-builder-api/typescript/src/sugar.ts @@ -15,6 +15,7 @@ import { BuildRequest, BuildResponse, BuildStatus, LogsRequest, LogsResponse, Re import { injectable, inject, optional } from 'inversify'; import * as grpc from "@grpc/grpc-js"; import { TextDecoder } from "util"; +import { ImageBuildLogInfo } from "@gitpod/gitpod-protocol"; export const ImageBuilderClientProvider = Symbol("ImageBuilderClientProvider"); @@ -82,6 +83,7 @@ export class CachingImageBuilderClientProvider implements ImageBuilderClientProv // StagedBuildResponse captures the multi-stage nature (starting, running, done) of image builds. export interface StagedBuildResponse { buildPromise: Promise; + logPromise: Promise; actuallyNeedsBuild: boolean; ref: string; @@ -129,13 +131,14 @@ export class PromisifiedImageBuilderClient { // build returns a nested promise. The outer one resolves/rejects with the build start, // the inner one resolves/rejects when the build is done. - public build(ctx: TraceContext, request: BuildRequest): Promise { + public build(ctx: TraceContext, request: BuildRequest, logInfoDeferred: Deferred = new Deferred()): Promise { const span = TraceContext.startSpan(`/image-builder/build`, ctx); const buildResult = new Deferred(); const result = new Deferred(); const resultResp: StagedBuildResponse = { + logPromise: logInfoDeferred.promise, buildPromise: buildResult.promise, actuallyNeedsBuild: true, ref: "unknown", @@ -151,6 +154,7 @@ export class PromisifiedImageBuilderClient { result.reject(err); } else { buildResult.reject(err); + logInfoDeferred.reject(err); } TraceContext.setError({ span }, err); @@ -166,6 +170,22 @@ export class PromisifiedImageBuilderClient { resultResp.baseRef = resp.getBaseRef(); } + if (resp.hasInfo()) { + // assumes that log info stays stable for instance lifetime + const info = resp.getInfo() + if (info && info.hasLogInfo() && !logInfoDeferred.isResolved) { + const logInfo = info.getLogInfo()!; + const headers: { [key: string]: string } = {}; + for (const [k, v] of logInfo.getHeadersMap().entries()) { + headers[k] = v; + } + logInfoDeferred.resolve({ + url: logInfo.getUrl(), + headers, + }); + } + } + if (resp.getStatus() == BuildStatus.RUNNING) { resultResp.actuallyNeedsBuild = true; result.resolve(resultResp); @@ -177,6 +197,9 @@ export class PromisifiedImageBuilderClient { } else { buildResult.resolve(resp); } + if (!logInfoDeferred.isResolved) { + logInfoDeferred.reject(new Error("no log stream for this image build")); + } span.finish(); } diff --git a/components/image-builder-mk3/go.mod b/components/image-builder-mk3/go.mod index 3c6f769b60f9f5..52a99bbd1f1ee6 100644 --- a/components/image-builder-mk3/go.mod +++ b/components/image-builder-mk3/go.mod @@ -34,16 +34,21 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/go-logr/logr v0.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gofuzz v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect github.com/klauspost/compress v1.11.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/moby/locker v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect @@ -58,6 +63,12 @@ require ( golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.22.2 // indirect + k8s.io/apimachinery v0.22.2 // indirect + k8s.io/klog/v2 v2.9.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect ) replace github.com/gitpod-io/gitpod/common-go => ../common-go // leeway diff --git a/components/image-builder-mk3/go.sum b/components/image-builder-mk3/go.sum index be717375f7d779..33a477d21335e4 100644 --- a/components/image-builder-mk3/go.sum +++ b/components/image-builder-mk3/go.sum @@ -52,6 +52,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I= +github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -76,6 +77,7 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc h1:mT8qSzuyEAkxbv4GBln7yeuQZpBnfikr3PTuiPs6Z3k= github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -113,6 +115,7 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3k github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -286,6 +289,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= @@ -307,6 +311,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -314,7 +319,9 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9 github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -328,6 +335,7 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -379,6 +387,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -464,12 +473,14 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -487,6 +498,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -524,8 +536,10 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -534,6 +548,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -636,6 +651,7 @@ github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiB github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -716,6 +732,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -779,7 +796,10 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= @@ -789,6 +809,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -988,13 +1009,16 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1052,6 +1076,10 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -1176,15 +1204,18 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -1197,6 +1228,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1214,7 +1246,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= @@ -1223,15 +1257,19 @@ k8s.io/cri-api v0.22.2/go.mod h1:mj5DGUtElRyErU5AZ8EM0ahxbElYsaLAMTPhLPQ40Eg= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/components/image-builder-mk3/pkg/orchestrator/monitor.go b/components/image-builder-mk3/pkg/orchestrator/monitor.go index 9e87646f358c16..993f9b40a32c55 100644 --- a/components/image-builder-mk3/pkg/orchestrator/monitor.go +++ b/components/image-builder-mk3/pkg/orchestrator/monitor.go @@ -202,6 +202,12 @@ func extractBuildStatus(status *wsmanapi.WorkspaceStatus) *api.BuildInfo { BaseRef: status.Metadata.Annotations[annotationBaseRef], Status: s, StartedAt: status.Metadata.StartedAt.Seconds, + LogInfo: &api.LogInfo{ + Url: status.Spec.Url, + Headers: map[string]string{ + "x-gitpod-owner-token": status.Auth.OwnerToken, + }, + }, } } diff --git a/components/image-builder-mk3/pkg/orchestrator/monitor_test.go b/components/image-builder-mk3/pkg/orchestrator/monitor_test.go index 76a224b3c0863c..8b01de7785d917 100644 --- a/components/image-builder-mk3/pkg/orchestrator/monitor_test.go +++ b/components/image-builder-mk3/pkg/orchestrator/monitor_test.go @@ -18,10 +18,12 @@ import ( func TestExtractBuildResponse(t *testing.T) { const ( - buildID = "build-id" - ref = "ref" - baseref = "base-ref" - startedAt int64 = 12345 + buildID = "build-id" + ref = "ref" + baseref = "base-ref" + startedAt int64 = 12345 + url = "https://some-url.some-domain.com" + ownerToken = "super-secret-owner-token" ) tests := []struct { Name string @@ -93,6 +95,12 @@ func TestExtractBuildResponse(t *testing.T) { }, Conditions: &wsmanapi.WorkspaceConditions{}, Phase: wsmanapi.WorkspacePhase_RUNNING, + Auth: &wsmanapi.WorkspaceAuthentication{ + OwnerToken: ownerToken, + }, + Spec: &wsmanapi.WorkspaceSpec{ + Url: url, + }, } test.Mod(status) act := extractBuildResponse(status) @@ -107,11 +115,17 @@ func TestExtractBuildResponse(t *testing.T) { BaseRef: baseref, Status: api.BuildStatus_running, StartedAt: startedAt, + LogInfo: &api.LogInfo{ + Url: url, + Headers: map[string]string{ + "x-gitpod-owner-token": status.Auth.OwnerToken, + }, + }, }, } test.Expectation(exp) - if diff := cmp.Diff(exp, act, cmpopts.IgnoreUnexported(api.BuildResponse{}, api.BuildInfo{})); diff != "" { + if diff := cmp.Diff(exp, act, cmpopts.IgnoreUnexported(api.BuildResponse{}, api.BuildInfo{}, api.LogInfo{})); diff != "" { t.Errorf("extractBuildResponse() mismatch (-want +got):\n%s", diff) } }) diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 24b7c5bcf20154..f27fc06ec5f65a 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -7,7 +7,7 @@ import { CloneTargetMode, FileDownloadInitializer, GitAuthMethod, GitConfig, GitInitializer, PrebuildInitializer, SnapshotInitializer, WorkspaceInitializer } from "@gitpod/content-service/lib"; import { CompositeInitializer, FromBackupInitializer } from "@gitpod/content-service/lib/initializer_pb"; import { DBUser, DBWithTracing, ProjectDB, TracedUserDB, TracedWorkspaceDB, UserDB, WorkspaceDB } from '@gitpod/gitpod-db/lib'; -import { CommitContext, Disposable, GitpodToken, GitpodTokenType, IssueContext, NamedWorkspaceFeatureFlag, PullRequestContext, RefType, SnapshotContext, StartWorkspaceResult, User, UserEnvVar, UserEnvVarValue, WithEnvvarsContext, WithPrebuild, Workspace, WorkspaceContext, WorkspaceImageSource, WorkspaceImageSourceDocker, WorkspaceImageSourceReference, WorkspaceInstance, WorkspaceInstanceConfiguration, WorkspaceInstanceStatus, WorkspaceProbeContext, Permission, HeadlessWorkspaceEvent, HeadlessWorkspaceEventType, DisposableCollection, AdditionalContentContext, ImageConfigFile, ProjectEnvVar } from "@gitpod/gitpod-protocol"; +import { CommitContext, Disposable, GitpodToken, GitpodTokenType, IssueContext, NamedWorkspaceFeatureFlag, PullRequestContext, RefType, SnapshotContext, StartWorkspaceResult, User, UserEnvVar, UserEnvVarValue, WithEnvvarsContext, WithPrebuild, Workspace, WorkspaceContext, WorkspaceImageSource, WorkspaceImageSourceDocker, WorkspaceImageSourceReference, WorkspaceInstance, WorkspaceInstanceConfiguration, WorkspaceInstanceStatus, WorkspaceProbeContext, Permission, HeadlessWorkspaceEvent, HeadlessWorkspaceEventType, DisposableCollection, AdditionalContentContext, ImageConfigFile, ProjectEnvVar, ImageBuildLogInfo } from "@gitpod/gitpod-protocol"; import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; @@ -34,6 +34,7 @@ import { IDEConfig, IDEConfigService } from "../ide-config"; import { EnvVarWithValue } from "@gitpod/gitpod-protocol/src/protocol"; import { WithReferrerContext } from "@gitpod/gitpod-protocol/lib/protocol"; import { IDEOption } from "@gitpod/gitpod-protocol/lib/ide-protocol"; +import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; export interface StartWorkspaceOptions { rethrow?: boolean; @@ -568,7 +569,18 @@ export class WorkspaceStarter { req.setAuth(auth); req.setForcerebuild(forceRebuild); - const result = await client.build({ span }, req); + // Make sure we persist logInfo as soon as we retrieve it + const imageBuildLogInfo = new Deferred(); + imageBuildLogInfo.promise.then(async logInfo => { + const imageBuildInfo = { + ...(instance.imageBuildInfo || {}), + log: logInfo, + }; + instance.imageBuildInfo = imageBuildInfo; // make sure we're not overriding ourselves again + await this.workspaceDb.trace({span}).updateInstancePartial(instance.id, { imageBuildInfo }).catch(err => log.error("error writing image build log info to the DB", err)); + }).catch(err => log.warn("image build: never received log info")); + + const result = await client.build({ span }, req, imageBuildLogInfo); // Update the workspace now that we know what the name of the workspace image will be (which doubles as buildID) workspace.imageNameResolved = result.ref; From 09be181d8d260d77bae6da4041ae5fd5e802849c Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Mon, 31 Jan 2022 08:12:55 +0000 Subject: [PATCH 2/3] [server] Generalize HeadlessLogService --- .../src/workspace/gitpod-server-impl.ts | 5 +- .../src/workspace/headless-log-controller.ts | 9 +- .../src/workspace/headless-log-service.ts | 146 +++++++++++------- 3 files changed, 102 insertions(+), 58 deletions(-) diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 714b72b0215242..0fa8d2c5e646ac 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1162,7 +1162,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async getHeadlessLog(ctx: TraceContext, instanceId: string): Promise { traceAPIParams(ctx, { instanceId }); - const user = this.checkAndBlockUser('getHeadlessLog', { instanceId }); + this.checkAndBlockUser('getHeadlessLog', { instanceId }); + const logCtx: LogContext = { instanceId }; const ws = await this.workspaceDb.trace(ctx).findByInstanceId(instanceId); if (!ws) { @@ -1179,7 +1180,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace instance for ${instanceId} not found`); } - const urls = await this.headlessLogService.getHeadlessLogURLs(user.id, wsi, ws.ownerId); + const urls = await this.headlessLogService.getHeadlessLogURLs(logCtx, wsi, ws.ownerId); if (!urls || (typeof urls.streams === "object" && Object.keys(urls.streams).length === 0)) { throw new ResponseError(ErrorCodes.NOT_FOUND, `Headless logs for ${instanceId} not found`); } diff --git a/components/server/src/workspace/headless-log-controller.ts b/components/server/src/workspace/headless-log-controller.ts index 2a0c11b96fbb3c..669ab18c4c2d50 100644 --- a/components/server/src/workspace/headless-log-controller.ts +++ b/components/server/src/workspace/headless-log-controller.ts @@ -12,7 +12,7 @@ import { CompositeResourceAccessGuard, OwnerResourceGuard, TeamMemberResourceGua import { DBWithTracing, TracedWorkspaceDB } from "@gitpod/gitpod-db/lib/traced-db"; import { WorkspaceDB } from "@gitpod/gitpod-db/lib/workspace-db"; import { TeamDB } from "@gitpod/gitpod-db/lib/team-db"; -import { HeadlessLogService } from "./headless-log-service"; +import { HeadlessLogService, HeadlessLogEndpoint } from "./headless-log-service"; import * as opentracing from 'opentracing'; import { asyncHandler } from "../express-util"; import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; @@ -54,6 +54,7 @@ export class HeadlessLogController { const logCtx = { userId: user.id, instanceId, workspaceId: workspace!.id }; log.debug(logCtx, HEADLESS_LOGS_PATH_PREFIX); + const aborted = new Deferred(); try { const head = { 'Content-Type': 'text/html; charset=utf-8', // is text/plain, but with that node.js won't stream... @@ -62,7 +63,6 @@ export class HeadlessLogController { }; res.writeHead(200, head) - const aborted = new Deferred(); const abort = (err: any) => { aborted.resolve(true); log.debug(logCtx, "headless-log: aborted"); @@ -88,7 +88,8 @@ export class HeadlessLogController { process.nextTick(resolve); } })); - await this.headlessLogService.streamWorkspaceLog(instance, params.terminalId, writeToResponse, aborted); + const logEndpoint = HeadlessLogEndpoint.fromWithOwnerToken(instance); + await this.headlessLogService.streamWorkspaceLogWhileRunning(logCtx, logEndpoint, instanceId, params.terminalId, writeToResponse, aborted); // In an ideal world, we'd use res.addTrailers()/response.trailer here. But despite being introduced with HTTP/1.1 in 1999, trailers are not supported by popular proxies (nginx, for example). // So we resort to this hand-written solution @@ -100,6 +101,8 @@ export class HeadlessLogController { res.write(`\n${HEADLESS_LOG_STREAM_STATUS_CODE}: 500`); res.end(); + } finally { + aborted.resolve(true); // ensure that the promise gets resolved eventually! } })]); router.get("/", malformedRequestHandler); diff --git a/components/server/src/workspace/headless-log-service.ts b/components/server/src/workspace/headless-log-service.ts index 5cd6e11444638a..95919c9ebb91ad 100644 --- a/components/server/src/workspace/headless-log-service.ts +++ b/components/server/src/workspace/headless-log-service.ts @@ -16,7 +16,7 @@ import { WorkspaceInstance } from "@gitpod/gitpod-protocol"; import * as grpc from '@grpc/grpc-js'; import { Config } from "../config"; import * as browserHeaders from "browser-headers"; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { log, LogContext } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TextDecoder } from "util"; import { WebsocketTransport } from "../util/grpc-web-ws-transport"; import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; @@ -24,6 +24,33 @@ import { ListLogsRequest, ListLogsResponse, LogDownloadURLRequest, LogDownloadUR import { HEADLESS_LOG_DOWNLOAD_PATH_PREFIX } from "./headless-log-controller"; import { CachingHeadlessLogServiceClientProvider } from "@gitpod/content-service/lib/sugar"; +export type HeadlessLogEndpoint = { + url: string, + ownerToken?: string, + headers?: { [key: string]: string }, +}; +export namespace HeadlessLogEndpoint { + export function authHeaders(logCtx: LogContext, logEndpoint: HeadlessLogEndpoint): browserHeaders.BrowserHeaders | undefined { + const headers = new browserHeaders.BrowserHeaders(logEndpoint.headers); + if (logEndpoint.ownerToken) { + headers.set("x-gitpod-owner-token", logEndpoint.ownerToken); + } + + if (Object.keys(headers.headersMap).length === 0) { + log.warn(logCtx, "workspace logs: no ownerToken nor headers!"); + return undefined; + } + + return headers; + } + export function fromWithOwnerToken(wsi: WorkspaceInstance): HeadlessLogEndpoint { + return { + url: wsi.ideUrl, + ownerToken: wsi.status.ownerToken, + } + } +} + @injectable() export class HeadlessLogService { static readonly SUPERVISOR_API_PATH = "/_supervisor/v1"; @@ -32,21 +59,22 @@ export class HeadlessLogService { @inject(Config) protected readonly config: Config; @inject(CachingHeadlessLogServiceClientProvider) protected readonly headlessLogClientProvider: CachingHeadlessLogServiceClientProvider; - public async getHeadlessLogURLs(userId: string, wsi: WorkspaceInstance, ownerId: string, maxTimeoutSecs: number = 30): Promise { + public async getHeadlessLogURLs(logCtx: LogContext, wsi: WorkspaceInstance, ownerId: string, maxTimeoutSecs: number = 30): Promise { if (isSupervisorAvailableSoon(wsi)) { + const logEndpoint = HeadlessLogEndpoint.fromWithOwnerToken(wsi); const aborted = new Deferred(); setTimeout(() => aborted.resolve(true), maxTimeoutSecs * 1000); - const streamIds = await this.retryWhileInstanceIsRunning(wsi, () => this.supervisorListHeadlessLogs(wsi), "list headless log streams", aborted); + const streamIds = await this.retryOnError(() => this.supervisorListHeadlessLogs(logCtx, wsi.id, logEndpoint), "list headless log streams", this.continueWhileRunning(wsi.id), aborted); if (streamIds !== undefined) { return streamIds; } } // we were unable to get a repsonse from supervisor - let's try content service next - return await this.contentServiceListLogs(userId, wsi, ownerId); + return await this.contentServiceListLogs(wsi, ownerId); } - protected async contentServiceListLogs(userId: string, wsi: WorkspaceInstance, ownerId: string): Promise { + protected async contentServiceListLogs(wsi: WorkspaceInstance, ownerId: string): Promise { const req = new ListLogsRequest(); req.setOwnerId(ownerId); req.setWorkspaceId(wsi.workspaceId); @@ -74,19 +102,24 @@ export class HeadlessLogService { }; } - protected async supervisorListHeadlessLogs(wsi: WorkspaceInstance): Promise { - if (wsi.ideUrl === "") { + protected async supervisorListHeadlessLogs(logCtx: LogContext, instanceId: string, logEndpoint: HeadlessLogEndpoint): Promise { + const tasks = await this.supervisorListTasks(logCtx, logEndpoint); + return this.renderTasksHeadlessLogUrls(logCtx, instanceId, tasks); + } + + protected async supervisorListTasks(logCtx: LogContext, logEndpoint: HeadlessLogEndpoint): Promise { + if (logEndpoint.url === "") { // if ideUrl is not yet set we're too early and we deem the workspace not ready yet: retry later! - throw new Error(`instance's ${wsi.id} has no ideUrl, yet`); + throw new Error(`instance's ${logCtx.instanceId} has no ideUrl, yet`); } const tasks = await new Promise((resolve, reject) => { - const client = new StatusServiceClient(toSupervisorURL(wsi.ideUrl), { + const client = new StatusServiceClient(toSupervisorURL(logEndpoint.url), { transport: WebsocketTransport(), }); const req = new TasksStatusRequest(); // Note: Don't set observe here at all, else it won't work! - const stream = client.tasksStatus(req, authHeaders(wsi)); + const stream = client.tasksStatus(req, HeadlessLogEndpoint.authHeaders(logCtx, logEndpoint)); stream.on('data', (resp: TasksStatusResponse) => { resolve(resp.getTasksList()); stream.cancel(); @@ -99,7 +132,10 @@ export class HeadlessLogService { } }); }); + return tasks; + } + protected renderTasksHeadlessLogUrls(logCtx: LogContext, instanceId: string, tasks: TaskStatus[]): HeadlessLogUrls { // render URLs that point to server's /headless-logs/ endpoint which forwards calls to the running workspaces's supervisor const streams: { [id: string]: string } = {}; for (const task of tasks) { @@ -109,14 +145,14 @@ export class HeadlessLogService { // this might be the case when there is no terminal for this task, yet. // if we find any such case, we deem the workspace not ready yet, and try to reconnect later, // to be sure to get hold of all terminals created. - throw new Error(`instance's ${wsi.id} task ${task.getId()} has no terminal yet`); + throw new Error(`instance's ${instanceId} task ${task.getId()} has no terminal yet`); } if (task.getState() === TaskState.CLOSED) { // if a task has already been closed we can no longer access it's terminal, and have to skip it. continue; } streams[taskId] = this.config.hostUrl.with({ - pathname: `/headless-logs/${wsi.id}/${terminalId}`, + pathname: `/headless-logs/${instanceId}/${terminalId}`, }).toString(); } return { @@ -158,12 +194,29 @@ export class HeadlessLogService { /** * For now, simply stream the supervisor data - * - * @param workspace + * @param logCtx + * @param logEndpoint + * @param instanceId + * @param terminalID + * @param sink + * @param doContinue + * @param aborted + */ + async streamWorkspaceLogWhileRunning(logCtx: LogContext, logEndpoint: HeadlessLogEndpoint, instanceId: string, terminalID: string, sink: (chunk: string) => Promise, aborted: Deferred): Promise { + await this.streamWorkspaceLog(logCtx, logEndpoint, terminalID, sink, this.continueWhileRunning(instanceId), aborted); + } + + /** + * For now, simply stream the supervisor data + * @param logCtx + * @param logEndpoint * @param terminalID + * @param sink + * @param doContinue + * @param aborted */ - async streamWorkspaceLog(wsi: WorkspaceInstance, terminalID: string, sink: (chunk: string) => Promise, aborted: Deferred): Promise { - const client = new TerminalServiceClient(toSupervisorURL(wsi.ideUrl), { + protected async streamWorkspaceLog(logCtx: LogContext, logEndpoint: HeadlessLogEndpoint, terminalID: string, sink: (chunk: string) => Promise, doContinue: () => Promise, aborted: Deferred): Promise { + const client = new TerminalServiceClient(toSupervisorURL(logEndpoint.url), { transport: WebsocketTransport(), // necessary because HTTPTransport causes caching issues }); const req = new ListenTerminalRequest(); @@ -172,10 +225,10 @@ export class HeadlessLogService { let receivedDataYet = false; let stream: ResponseStream | undefined = undefined; aborted.promise.then(() => stream?.cancel()); - const doStream = (cancelRetry: () => void) => new Promise((resolve, reject) => { + const doStream = (retry: (doRetry?: boolean) => void) => new Promise((resolve, reject) => { // [gpl] this is the very reason we cannot redirect the frontend to the supervisor URL: currently we only have ownerTokens for authentication const decoder = new TextDecoder('utf-8') - stream = client.listen(req, authHeaders(wsi)); + stream = client.listen(req, HeadlessLogEndpoint.authHeaders(logCtx, logEndpoint)); stream.on('data', (resp: ListenTerminalResponse) => { receivedDataYet = true; @@ -184,7 +237,7 @@ export class HeadlessLogService { sink(data) .catch((err) => { stream?.cancel(); // If downstream reports an error: cancel connection to upstream - log.debug({ instanceId: wsi.id }, "stream cancelled", err); + log.debug(logCtx, "stream cancelled", err); }); }); stream.on('end', (status?: Status) => { @@ -201,48 +254,42 @@ export class HeadlessLogService { return; } - cancelRetry(); + retry(false); reject(err); }); }); - await this.retryWhileInstanceIsRunning(wsi, doStream, "stream workspace logs", aborted); + await this.retryOnError(doStream, "stream workspace logs", doContinue, aborted); } /** * Retries op while the passed WorkspaceInstance is still starting. Retries are stopped if either: - * - `op` calls `cancel()` and an err is thrown, it is re-thrown by this method + * - `op` calls `retry(false)` and an err is thrown, it is re-thrown by this method * - `aborted` resolves to `true`: `undefined` is returned - * - if the instance enters the either STOPPING/STOPPED phases, we stop retrying, and return `undefined` - * @param wsi + * - `(await while()) === true`: `undefined` is returned * @param op * @param description + * @param doContinue * @param aborted * @returns */ - protected async retryWhileInstanceIsRunning(wsi: WorkspaceInstance, op: (cancel: () => void) => Promise, description: string, aborted: Deferred): Promise { - let cancelled = false; - const cancel = () => { cancelled = true; }; + protected async retryOnError(op: (cancel: () => void) => Promise, description: string, doContinue: () => Promise, aborted: Deferred): Promise { + let retry = true; + const retryFunction = (doRetry: boolean = true) => { retry = doRetry }; - let instance = wsi; - while (!cancelled && !(aborted.isResolved && (await aborted.promise)) ) { + while (retry && !(aborted.isResolved && (await aborted.promise)) ) { try { - return await op(cancel); + return await op(retryFunction); } catch (err) { - if (cancelled) { + if (!retry) { throw err; } - log.debug(`unable to ${description}`, err); - const maybeInstance = await this.db.findInstanceById(instance.id); - if (!maybeInstance) { + const shouldContinue = await doContinue(); + if (!shouldContinue) { return undefined; } - instance = maybeInstance; - if (!this.shouldRetry(instance)) { - return undefined; - } - log.debug(`re-trying ${description}...`); + log.debug(`unable to ${description}, retrying...`, err); await new Promise((resolve) => setTimeout(resolve, 2000)); continue; } @@ -250,9 +297,13 @@ export class HeadlessLogService { return undefined; } - protected shouldRetry(wsi: WorkspaceInstance): boolean { - return isSupervisorAvailableSoon(wsi); - } + protected continueWhileRunning(instanceId: string): () => Promise { + const db = this.db; + return async () => { + const maybeInstance = await db.findInstanceById(instanceId); + return !!maybeInstance && isSupervisorAvailableSoon(maybeInstance); + } + }; } function isSupervisorAvailableSoon(wsi: WorkspaceInstance): boolean { @@ -273,14 +324,3 @@ function toSupervisorURL(ideUrl: string): string { u.pathname = HeadlessLogService.SUPERVISOR_API_PATH; return u.toString(); } - -function authHeaders(wsi: WorkspaceInstance): browserHeaders.BrowserHeaders | undefined { - const ownerToken = wsi.status.ownerToken; - if (!ownerToken) { - log.warn({ instanceId: wsi.id }, "workspace logs: owner token not found"); - return undefined; - } - const headers = new browserHeaders.BrowserHeaders(); - headers.set("x-gitpod-owner-token", ownerToken); - return headers; -} From 1129ace4356acb9dcb19748bbcc00f9ba9e33839 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Mon, 31 Jan 2022 12:41:45 +0000 Subject: [PATCH 3/3] [server] Stream imagebuild logs from headless workspace directly --- .../dashboard/src/start/StartWorkspace.tsx | 22 ++++- .../gitpod-protocol/src/messaging/error.ts | 3 + .../src/workspace/gitpod-server-impl.ts | 84 ++++++++++++++++--- .../src/workspace/headless-log-service.ts | 18 ++++ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index f517d431b5b29d..384eff1434fbcb 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -418,11 +418,29 @@ function ImageBuildView(props: ImageBuildViewProps) { const logsEmitter = new EventEmitter(); useEffect(() => { - const watchBuild = () => getGitpodService().server.watchWorkspaceImageBuildLogs(props.workspaceId); + let registered = false; + const watchBuild = () => { + if (registered) { + return; + } + + getGitpodService().server.watchWorkspaceImageBuildLogs(props.workspaceId) + .then(() => registered = true) + .catch(err => { + + if (err?.code === ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE) { + // wait, and then retry + setTimeout(watchBuild, 5000); + } + }) + } watchBuild(); const toDispose = getGitpodService().registerClient({ - notifyDidOpenConnection: () => watchBuild(), + notifyDidOpenConnection: () => { + registered = false; // new connection, we're not registered anymore + watchBuild(); + }, onWorkspaceImageBuildLogs: (info: WorkspaceImageBuild.StateInfo, content?: WorkspaceImageBuild.LogContent) => { if (!content) { return; diff --git a/components/gitpod-protocol/src/messaging/error.ts b/components/gitpod-protocol/src/messaging/error.ts index 65da76c95be0e4..b825a012eabeb7 100644 --- a/components/gitpod-protocol/src/messaging/error.ts +++ b/components/gitpod-protocol/src/messaging/error.ts @@ -78,4 +78,7 @@ export namespace ErrorCodes { // 630 Snapshot Error export const SNAPSHOT_ERROR = 630; + + // 640 Headless logs are not available (yet) + export const HEADLESS_LOG_NOT_YET_AVAILABLE = 640; } \ No newline at end of file diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 0fa8d2c5e646ac..d13c2fbd1e968d 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -46,7 +46,7 @@ import { WorkspaceDeletionService } from './workspace-deletion-service'; import { WorkspaceFactory } from './workspace-factory'; import { WorkspaceStarter } from './workspace-starter'; import { HeadlessLogUrls } from "@gitpod/gitpod-protocol/lib/headless-workspace-log"; -import { HeadlessLogService } from "./headless-log-service"; +import { HeadlessLogService, HeadlessLogEndpoint } from "./headless-log-service"; import { InvalidGitpodYMLError } from "./config-provider"; import { ProjectsService } from "../projects/projects-service"; import { LocalMessageBroker } from "../messaging/local-message-broker"; @@ -58,6 +58,7 @@ import { ClientMetadata } from '../websocket/websocket-connection-manager'; import { ConfigurationService } from '../config/configuration-service'; import { ProjectEnvVar } from '@gitpod/gitpod-protocol/src/protocol'; import { InstallationAdminSettings } from '@gitpod/gitpod-protocol'; +import { Deferred } from '@gitpod/gitpod-protocol/lib/util/deferred'; // shortcut export const traceWI = (ctx: TraceContext, wi: Omit) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager @@ -1112,24 +1113,87 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { traceWI(ctx, { workspaceId }); const user = this.checkAndBlockUser("watchWorkspaceImageBuildLogs", undefined, { workspaceId }); - const logCtx: LogContext = { userId: user.id, workspaceId }; - - const { instance, workspace } = await this.internGetCurrentWorkspaceInstance(ctx, workspaceId); - if (!this.client) { + const client = this.client; + if (!client) { return; } + + const logCtx: LogContext = { userId: user.id, workspaceId }; + let { instance, workspace } = await this.internGetCurrentWorkspaceInstance(ctx, workspaceId); if (!instance) { log.debug(logCtx, `No running instance for workspaceId.`); return; } traceWI(ctx, { instanceId: instance.id }); - if (!workspace.imageNameResolved) { - log.debug(logCtx, `No imageNameResolved set for workspaceId, cannot watch logs.`); - return; - } const teamMembers = await this.getTeamMembersByProject(workspace.projectId); await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace, teamMembers }, "get"); - if (!this.client) { + + // wait for up to 20s for imageBuildLogInfo to appear due to: + // - db-sync round-trip times + // - but also: wait until the image build actually started (image pull!), and log info is available! + for (let i = 0; i < 10; i++) { + if (instance.imageBuildInfo?.log) { + break; + } + await new Promise(resolve => setTimeout(resolve, 2000)); + + const wsi = await this.workspaceDb.trace(ctx).findInstanceById(instance.id); + if (!wsi || wsi.status.phase !== 'preparing') { + log.debug(logCtx, `imagebuild logs: instance is not/no longer in 'preparing' state`, { phase: wsi?.status.phase }); + return; + } + instance = wsi as WorkspaceInstance; // help the compiler a bit + } + + const logInfo = instance.imageBuildInfo?.log; + if (!logInfo) { + // during roll-out this is our fall-back case. + // Afterwards we might want to do some spinning-lock and re-check for a certain period (30s?) to give db-sync + // a change to move the imageBuildLogInfo across the globe. + + log.warn(logCtx, "imageBuild logs: fallback!"); + ctx.span?.setTag("workspace.imageBuild.logs.fallback", true); + await this.deprecatedDoWatchWorkspaceImageBuildLogs(ctx, logCtx, workspace); + return; + } + + const aborted = new Deferred(); + try { + const logEndpoint: HeadlessLogEndpoint = { + url: logInfo.url, + headers: logInfo.headers, + }; + let lineCount = 0; + await this.headlessLogService.streamImageBuildLog(logCtx, logEndpoint, async (chunk) => { + if (aborted.isResolved) { + return; + } + + try { + chunk = chunk.replace("\n", WorkspaceImageBuild.LogLine.DELIMITER); + lineCount += chunk.split(WorkspaceImageBuild.LogLine.DELIMITER_REGEX).length; + + client.onWorkspaceImageBuildLogs(undefined as any, { + text: chunk, + isDiff: true, + upToLine: lineCount + }); + } catch (err) { + log.error("error while streaming imagebuild logs", err); + aborted.resolve(true); + } + }, aborted); + } catch (err) { + log.error(logCtx, "cannot watch imagebuild logs for workspaceId", err); + throw new ResponseError(ErrorCodes.HEADLESS_LOG_NOT_YET_AVAILABLE, "cannot watch imagebuild logs for workspaceId"); + } finally { + aborted.resolve(false); + } + } + + protected async deprecatedDoWatchWorkspaceImageBuildLogs(ctx: TraceContext, logCtx: LogContext, workspace: Workspace) { + if (!workspace.imageNameResolved) { + log.debug(logCtx, `No imageNameResolved set for workspaceId, cannot watch logs.`); return; } diff --git a/components/server/src/workspace/headless-log-service.ts b/components/server/src/workspace/headless-log-service.ts index 95919c9ebb91ad..d52000b597693a 100644 --- a/components/server/src/workspace/headless-log-service.ts +++ b/components/server/src/workspace/headless-log-service.ts @@ -261,6 +261,24 @@ export class HeadlessLogService { await this.retryOnError(doStream, "stream workspace logs", doContinue, aborted); } + /** + * Streaming imagebuild logs is different to other headless workspaces (prebuilds) because we do not store them as "workspace" (incl. status, etc.), but have a special field "workspace.imageBuildInfo". + * @param logCtx + * @param logEndpoint + * @param sink + * @param aborted + */ + async streamImageBuildLog(logCtx: LogContext, logEndpoint: HeadlessLogEndpoint, sink: (chunk: string) => Promise, aborted: Deferred): Promise { + const tasks = await this.supervisorListTasks(logCtx, logEndpoint); + if (tasks.length === 0) { + throw new Error(`imagebuild logs: not tasks found for endpoint ${logEndpoint.url}!`); + } + + // we're just looking at the first stream; image builds just have one stream atm + const task = tasks[0]; + await this.streamWorkspaceLog(logCtx, logEndpoint, task.getTerminal(), sink, () => Promise.resolve(true), aborted); + } + /** * Retries op while the passed WorkspaceInstance is still starting. Retries are stopped if either: * - `op` calls `retry(false)` and an err is thrown, it is re-thrown by this method