From a5de528de8d8b9f4dc4370e31eb5d1f5e8e382fa Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Wed, 31 May 2023 16:15:27 -0600 Subject: [PATCH] =?UTF-8?q?feature:=20Tracetest=20Linter=20=F0=9F=90=99=20?= =?UTF-8?q?(#2547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: The Dream 💜 * feature(openapi): adding specs for linterns * updating types for lintern * updating types for lintern * adding lintern resource logic * feature(cli): fixing UI type * add runner structure * add rule to check if there are missing required attributes * add test to required attributes rule * registering lintern resource on app * registering lintern resource on app * feat(frontend): add linter settings page * adding lintern runner logic * registering lintern resource on app * feat(frontend): add LintResult component in test run page * first working version * fix(frontend): fix rule results * add not empty attribute rule * feat(frontend): add LinterResults component styles * add rule to validate attribute names * remove anonymous variables * rename files * add kind to span and add span naming rule * registering rules * finishing required attrs rule * feat(frontend): add dag lint icon and result click * feat(frontend): add linter loading component * adding common and security plugins * feat(frontend): add dag lint errors component * UI cleanup * UI cleanup * fix(frontend): fix styles and linter settings * feat(frontend): add octolint footer * fixing unit tests * fixing unit tests * fixing annoying typo * fixing e2e tests * cleanup changes * tracetest linter UX mvp improvements * tracetest linter UX mvp improvements * PR comments and beta badge --------- Co-authored-by: Matheus Nogueira Co-authored-by: Jorge Padilla --- api/linters.yaml | 102 ++++ api/openapi.yaml | 127 ++++ api/parameters.yaml | 8 + api/tests.yaml | 4 + api/trace.yaml | 2 + cli/openapi/api_resource_api.go | 556 ++++++++++++++++++ cli/openapi/model_linter_resource.go | 160 +++++ cli/openapi/model_linter_resource_list.go | 124 ++++ cli/openapi/model_linter_resource_plugin.go | 196 ++++++ cli/openapi/model_linter_resource_spec.go | 268 +++++++++ cli/openapi/model_linter_result.go | 196 ++++++ cli/openapi/model_linter_result_plugin.go | 268 +++++++++ .../model_linter_result_plugin_rule.go | 304 ++++++++++ .../model_linter_result_plugin_rule_result.go | 232 ++++++++ cli/openapi/model_span.go | 36 ++ cli/openapi/model_test_run.go | 36 ++ .../tracetest/docker-compose.yaml | 5 +- go.work.sum | 34 +- server/app/app.go | 17 + server/app/facade.go | 14 +- server/executor/linter_runner.go | 200 +++++++ server/executor/trace_poller.go | 10 +- server/http/mappings/linter.go | 59 ++ server/http/mappings/tests.go | 1 + server/http/mappings/traces.go | 6 + server/linter/linter.go | 108 ++++ server/linter/plugins/common/plugin.go | 49 ++ .../plugins/common/rules/enforce_dns.go | 63 ++ server/linter/plugins/security/plugin.go | 50 ++ .../security/rules/enforce_secure_protocol.go | 63 ++ .../security/rules/ensures_no_api_key_leak.go | 70 +++ server/linter/plugins/standards/plugin.go | 52 ++ .../rules/ensure_attribute_naming_rule.go | 71 +++ .../ensure_attribute_naming_rule_test.go | 64 ++ .../rules/ensure_span_naming_rule.go | 144 +++++ .../rules/not_empty_attribute_rule.go | 57 ++ .../rules/not_empty_attribute_rule_test.go | 38 ++ .../rules/required_attribute_rule.go | 42 ++ .../rules/required_attribute_rule_test.go | 81 +++ .../standards/rules/required_attributes.go | 137 +++++ server/linter/resource/main_test.go | 18 + server/linter/resource/manager.go | 195 ++++++ server/linter/resource/manager_test.go | 90 +++ server/linter/resource/model.go | 80 +++ server/linter/resource/resource.go | 14 + server/migrations/26_add_linter.down.sql | 5 + server/migrations/26_add_linter.up.sql | 14 + server/model/events/events.go | 60 ++ server/model/linter.go | 68 +++ server/model/run.go | 15 +- server/model/spans.go | 12 + server/model/tests.go | 3 + server/openapi/api.go | 10 + server/openapi/api_resource_api.go | 139 +++++ server/openapi/api_resource_api_service.go | 88 +++ server/openapi/model_linter_resource.go | 36 ++ server/openapi/model_linter_resource_list.go | 36 ++ .../openapi/model_linter_resource_plugin.go | 35 ++ server/openapi/model_linter_resource_spec.go | 44 ++ server/openapi/model_linter_result.go | 40 ++ server/openapi/model_linter_result_plugin.go | 44 ++ .../model_linter_result_plugin_rule.go | 46 ++ .../model_linter_result_plugin_rule_result.go | 37 ++ server/openapi/model_span.go | 2 + server/openapi/model_test_run.go | 5 + server/testdb/runs.go | 38 +- server/traces/otel_converter.go | 18 + .../e2e/Transactions/TransactionsRun.spec.ts | 4 +- .../components/BetaBadge/BetaBadge.styled.ts | 12 + web/src/components/BetaBadge/BetaBadge.tsx | 5 + web/src/components/BetaBadge/index.ts | 2 + .../components/LintScore/LintScore.styled.ts | 40 ++ web/src/components/LintScore/LintScore.tsx | 28 + web/src/components/LintScore/index.ts | 2 + .../RunDetailTrace/CollapseIcon.tsx | 13 + .../RunDetailTrace/LintResults.styled.ts | 135 +++++ .../components/RunDetailTrace/LintResults.tsx | 141 +++++ .../RunDetailTrace/RunDetailTrace.styled.ts | 12 + .../RunDetailTrace/RunDetailTrace.tsx | 63 +- web/src/components/Settings/Linter/Linter.tsx | 34 ++ .../components/Settings/Linter/LinterForm.tsx | 71 +++ web/src/components/Settings/Linter/Plugin.tsx | 25 + web/src/components/Settings/Linter/index.ts | 2 + .../Settings/common/Settings.styled.ts | 12 + web/src/components/SpanDetail/Header.tsx | 16 +- .../SpanDetail/SpanDetail.styled.ts | 7 + .../components/DAG/SpanNode.styled.ts | 49 ++ .../Visualization/components/DAG/SpanNode.tsx | 34 +- web/src/constants/Common.constants.ts | 1 + web/src/constants/TestRun.constants.ts | 10 + web/src/models/Linter.model.ts | 25 + web/src/models/LinterResult.model.ts | 58 ++ web/src/models/TestRun.model.ts | 9 + web/src/pages/Settings/Content.tsx | 14 + web/src/pages/Settings/Settings.styled.ts | 5 + .../SettingsValues.provider.tsx | 16 +- .../providers/TestRun/TestRun.provider.tsx | 11 +- web/src/redux/apis/TraceTest.api.ts | 1 + .../redux/apis/endpoints/Setting.endpoint.ts | 10 + web/src/services/Span.service.ts | 28 + web/src/services/TestRun.service.ts | 5 +- web/src/types/Common.types.ts | 1 + web/src/types/Generated.types.ts | 167 ++++++ web/src/types/Settings.types.ts | 8 +- 104 files changed, 6252 insertions(+), 70 deletions(-) create mode 100644 api/linters.yaml create mode 100644 cli/openapi/model_linter_resource.go create mode 100644 cli/openapi/model_linter_resource_list.go create mode 100644 cli/openapi/model_linter_resource_plugin.go create mode 100644 cli/openapi/model_linter_resource_spec.go create mode 100644 cli/openapi/model_linter_result.go create mode 100644 cli/openapi/model_linter_result_plugin.go create mode 100644 cli/openapi/model_linter_result_plugin_rule.go create mode 100644 cli/openapi/model_linter_result_plugin_rule_result.go create mode 100644 server/executor/linter_runner.go create mode 100644 server/http/mappings/linter.go create mode 100644 server/linter/linter.go create mode 100644 server/linter/plugins/common/plugin.go create mode 100644 server/linter/plugins/common/rules/enforce_dns.go create mode 100644 server/linter/plugins/security/plugin.go create mode 100644 server/linter/plugins/security/rules/enforce_secure_protocol.go create mode 100644 server/linter/plugins/security/rules/ensures_no_api_key_leak.go create mode 100644 server/linter/plugins/standards/plugin.go create mode 100644 server/linter/plugins/standards/rules/ensure_attribute_naming_rule.go create mode 100644 server/linter/plugins/standards/rules/ensure_attribute_naming_rule_test.go create mode 100644 server/linter/plugins/standards/rules/ensure_span_naming_rule.go create mode 100644 server/linter/plugins/standards/rules/not_empty_attribute_rule.go create mode 100644 server/linter/plugins/standards/rules/not_empty_attribute_rule_test.go create mode 100644 server/linter/plugins/standards/rules/required_attribute_rule.go create mode 100644 server/linter/plugins/standards/rules/required_attribute_rule_test.go create mode 100644 server/linter/plugins/standards/rules/required_attributes.go create mode 100644 server/linter/resource/main_test.go create mode 100644 server/linter/resource/manager.go create mode 100644 server/linter/resource/manager_test.go create mode 100644 server/linter/resource/model.go create mode 100644 server/linter/resource/resource.go create mode 100644 server/migrations/26_add_linter.down.sql create mode 100644 server/migrations/26_add_linter.up.sql create mode 100644 server/model/linter.go create mode 100644 server/openapi/model_linter_resource.go create mode 100644 server/openapi/model_linter_resource_list.go create mode 100644 server/openapi/model_linter_resource_plugin.go create mode 100644 server/openapi/model_linter_resource_spec.go create mode 100644 server/openapi/model_linter_result.go create mode 100644 server/openapi/model_linter_result_plugin.go create mode 100644 server/openapi/model_linter_result_plugin_rule.go create mode 100644 server/openapi/model_linter_result_plugin_rule_result.go create mode 100644 web/src/components/BetaBadge/BetaBadge.styled.ts create mode 100644 web/src/components/BetaBadge/BetaBadge.tsx create mode 100644 web/src/components/BetaBadge/index.ts create mode 100644 web/src/components/LintScore/LintScore.styled.ts create mode 100644 web/src/components/LintScore/LintScore.tsx create mode 100644 web/src/components/LintScore/index.ts create mode 100644 web/src/components/RunDetailTrace/CollapseIcon.tsx create mode 100644 web/src/components/RunDetailTrace/LintResults.styled.ts create mode 100644 web/src/components/RunDetailTrace/LintResults.tsx create mode 100644 web/src/components/Settings/Linter/Linter.tsx create mode 100644 web/src/components/Settings/Linter/LinterForm.tsx create mode 100644 web/src/components/Settings/Linter/Plugin.tsx create mode 100644 web/src/components/Settings/Linter/index.ts create mode 100644 web/src/models/Linter.model.ts create mode 100644 web/src/models/LinterResult.model.ts diff --git a/api/linters.yaml b/api/linters.yaml new file mode 100644 index 0000000000..d9c6fd7b82 --- /dev/null +++ b/api/linters.yaml @@ -0,0 +1,102 @@ +openapi: 3.0.0 +components: + schemas: + LinterResourceList: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/LinterResource" + LinterResource: + type: object + properties: + type: + type: string + enum: + - Linter + spec: + type: object + properties: + id: + type: string + name: + type: string + enabled: + type: boolean + minimumScore: + type: integer + plugins: + type: array + items: + $ref: "#/components/schemas/LinterResourcePlugin" + LinterResourcePlugin: + type: object + properties: + name: + type: string + enabled: + type: boolean + required: + type: boolean + LinterResult: + type: object + properties: + passed: + type: boolean + score: + type: integer + plugins: + type: array + items: + $ref: "#/components/schemas/LinterResultPlugin" + LinterResultPlugin: + type: object + properties: + name: + type: string + description: + type: string + passed: + type: boolean + score: + type: integer + rules: + type: array + items: + $ref: "#/components/schemas/LinterResultPluginRule" + LinterResultPluginRule: + type: object + properties: + name: + type: string + description: + type: string + passed: + type: boolean + weight: + type: integer + tips: + type: array + items: + type: string + results: + type: array + items: + $ref: "#/components/schemas/LinterResultPluginRuleResult" + LinterResultPluginRuleResult: + type: object + properties: + spanId: + type: string + errors: + type: array + items: + type: string + passed: + type: boolean + severity: + type: string + enum: + - error + - warning diff --git a/api/openapi.yaml b/api/openapi.yaml index 53c9da12c0..baaaf8c243 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1265,3 +1265,130 @@ paths: $ref: "./version.yaml#/components/schemas/Version" 500: description: "problem getting the version of the API" + + # Linters + /linters: + get: + tags: + - resource-api + summary: "List Linters" + description: "List Linters available in Tracetest." + operationId: listLinters + parameters: + - $ref: "./parameters.yaml#/components/parameters/take" + - $ref: "./parameters.yaml#/components/parameters/skip" + - $ref: "./parameters.yaml#/components/parameters/switchableResourceSortBy" + - $ref: "./parameters.yaml#/components/parameters/sortDirection" + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResourceList" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResourceList" + 400: + description: "invalid query for Linters, some data was sent in incorrect format." + 500: + description: "problem listing Linters" + post: + tags: + - resource-api + summary: "Create an Linter" + description: "Create an Linter that can be used by tests and Linters" + operationId: createLinter + requestBody: + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + responses: + 201: + description: successful operation + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + 500: + description: "problem creating an Linter" + + /linters/{LinterId}: + get: + tags: + - resource-api + parameters: + - $ref: "./parameters.yaml#/components/parameters/LinterId" + summary: "Get a specific Linter" + description: "Get one Linter by its id" + operationId: getLinter + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + 404: + description: "Linter not found" + 500: + description: "problem getting a Linter" + put: + tags: + - resource-api + parameters: + - $ref: "./parameters.yaml#/components/parameters/LinterId" + summary: "Update a Linter" + description: "Update a Linter used on Tracetest" + operationId: updateLinter + requestBody: + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + text/yaml: + schema: + $ref: "./linters.yaml#/components/schemas/LinterResource" + 400: + description: "invalid Linter, some data was sent in incorrect format." + 404: + description: "Linter not found" + 500: + description: "problem updating an Linter" + delete: + tags: + - resource-api + parameters: + - $ref: "./parameters.yaml#/components/parameters/LinterId" + summary: "Delete an Linter" + description: "Delete an Linter from Tracetest" + operationId: deleteLinter + responses: + 204: + description: successful operation + 400: + description: "invalid Linter, some data was sent in incorrect format." + 404: + description: "Linter not found" + 500: + description: "problem deleting an Linter" diff --git a/api/parameters.yaml b/api/parameters.yaml index f4e5a67783..202ab6e374 100644 --- a/api/parameters.yaml +++ b/api/parameters.yaml @@ -133,3 +133,11 @@ components: description: "ID of an environment used on Tracetest to inject values into tests and transactions" schema: type: string + + LinterId: + in: path + name: LinterId + required: true + description: "ID of an Linter" + schema: + type: string diff --git a/api/tests.yaml b/api/tests.yaml index 203b7d1065..094eedb876 100644 --- a/api/tests.yaml +++ b/api/tests.yaml @@ -136,6 +136,8 @@ components: EXECUTING, AWAITING_TRACE, AWAITING_TEST_RESULTS, + ANALYZING_TRACE, + ANALYZING_ERROR, FINISHED, STOPPED, TRIGGER_FAILED, @@ -175,6 +177,8 @@ components: $ref: "./trace.yaml#/components/schemas/Trace" result: $ref: "#/components/schemas/AssertionResults" + linter: + $ref: "linters.yaml#/components/schemas/LinterResult" outputs: type: array items: diff --git a/api/trace.yaml b/api/trace.yaml index 024ba0f811..bddd2df1a6 100644 --- a/api/trace.yaml +++ b/api/trace.yaml @@ -23,6 +23,8 @@ components: type: string name: type: string + kind: + type: string startTime: type: integer format: int64 diff --git a/cli/openapi/api_resource_api.go b/cli/openapi/api_resource_api.go index 76bc3cab1e..0e0fee22eb 100644 --- a/cli/openapi/api_resource_api.go +++ b/cli/openapi/api_resource_api.go @@ -238,6 +238,114 @@ func (a *ResourceApiApiService) CreateEnvironmentExecute(r ApiCreateEnvironmentR return localVarReturnValue, localVarHTTPResponse, nil } +type ApiCreateLinterRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + linterResource *LinterResource +} + +func (r ApiCreateLinterRequest) LinterResource(linterResource LinterResource) ApiCreateLinterRequest { + r.linterResource = &linterResource + return r +} + +func (r ApiCreateLinterRequest) Execute() (*LinterResource, *http.Response, error) { + return r.ApiService.CreateLinterExecute(r) +} + +/* +CreateLinter Create an Linter + +Create an Linter that can be used by tests and Linters + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiCreateLinterRequest +*/ +func (a *ResourceApiApiService) CreateLinter(ctx context.Context) ApiCreateLinterRequest { + return ApiCreateLinterRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return LinterResource +func (a *ResourceApiApiService) CreateLinterExecute(r ApiCreateLinterRequest) (*LinterResource, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *LinterResource + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.CreateLinter") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/linters" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json", "text/yaml"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/yaml"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.linterResource + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiCreateTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -622,6 +730,98 @@ func (a *ResourceApiApiService) DeleteEnvironmentExecute(r ApiDeleteEnvironmentR return localVarHTTPResponse, nil } +type ApiDeleteLinterRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + linterId string +} + +func (r ApiDeleteLinterRequest) Execute() (*http.Response, error) { + return r.ApiService.DeleteLinterExecute(r) +} + +/* +DeleteLinter Delete an Linter + +Delete an Linter from Tracetest + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param linterId ID of an Linter + @return ApiDeleteLinterRequest +*/ +func (a *ResourceApiApiService) DeleteLinter(ctx context.Context, linterId string) ApiDeleteLinterRequest { + return ApiDeleteLinterRequest{ + ApiService: a, + ctx: ctx, + linterId: linterId, + } +} + +// Execute executes the request +func (a *ResourceApiApiService) DeleteLinterExecute(r ApiDeleteLinterRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.DeleteLinter") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/linters/{LinterId}" + localVarPath = strings.Replace(localVarPath, "{"+"LinterId"+"}", url.PathEscape(parameterValueToString(r.linterId, "linterId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + type ApiDeleteTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -1130,6 +1330,110 @@ func (a *ResourceApiApiService) GetEnvironmentExecute(r ApiGetEnvironmentRequest return localVarReturnValue, localVarHTTPResponse, nil } +type ApiGetLinterRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + linterId string +} + +func (r ApiGetLinterRequest) Execute() (*LinterResource, *http.Response, error) { + return r.ApiService.GetLinterExecute(r) +} + +/* +GetLinter Get a specific Linter + +Get one Linter by its id + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param linterId ID of an Linter + @return ApiGetLinterRequest +*/ +func (a *ResourceApiApiService) GetLinter(ctx context.Context, linterId string) ApiGetLinterRequest { + return ApiGetLinterRequest{ + ApiService: a, + ctx: ctx, + linterId: linterId, + } +} + +// Execute executes the request +// +// @return LinterResource +func (a *ResourceApiApiService) GetLinterExecute(r ApiGetLinterRequest) (*LinterResource, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *LinterResource + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.GetLinter") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/linters/{LinterId}" + localVarPath = strings.Replace(localVarPath, "{"+"LinterId"+"}", url.PathEscape(parameterValueToString(r.linterId, "linterId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/yaml"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiGetPollingProfileRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -2048,6 +2352,146 @@ func (a *ResourceApiApiService) ListEnvironmentsExecute(r ApiListEnvironmentsReq return localVarReturnValue, localVarHTTPResponse, nil } +type ApiListLintersRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + take *int32 + skip *int32 + sortBy *string + sortDirection *string +} + +// indicates how many resources can be returned by each page +func (r ApiListLintersRequest) Take(take int32) ApiListLintersRequest { + r.take = &take + return r +} + +// indicates how many resources will be skipped when paginating +func (r ApiListLintersRequest) Skip(skip int32) ApiListLintersRequest { + r.skip = &skip + return r +} + +// indicates the sort field for the resources +func (r ApiListLintersRequest) SortBy(sortBy string) ApiListLintersRequest { + r.sortBy = &sortBy + return r +} + +// indicates the sort direction for the resources +func (r ApiListLintersRequest) SortDirection(sortDirection string) ApiListLintersRequest { + r.sortDirection = &sortDirection + return r +} + +func (r ApiListLintersRequest) Execute() (*LinterResourceList, *http.Response, error) { + return r.ApiService.ListLintersExecute(r) +} + +/* +ListLinters List Linters + +List Linters available in Tracetest. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiListLintersRequest +*/ +func (a *ResourceApiApiService) ListLinters(ctx context.Context) ApiListLintersRequest { + return ApiListLintersRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return LinterResourceList +func (a *ResourceApiApiService) ListLintersExecute(r ApiListLintersRequest) (*LinterResourceList, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *LinterResourceList + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.ListLinters") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/linters" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.take != nil { + parameterAddToQuery(localVarQueryParams, "take", r.take, "") + } + if r.skip != nil { + parameterAddToQuery(localVarQueryParams, "skip", r.skip, "") + } + if r.sortBy != nil { + parameterAddToQuery(localVarQueryParams, "sortBy", r.sortBy, "") + } + if r.sortDirection != nil { + parameterAddToQuery(localVarQueryParams, "sortDirection", r.sortDirection, "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/yaml"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiListPollingProfileRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -2624,6 +3068,118 @@ func (a *ResourceApiApiService) UpdateEnvironmentExecute(r ApiUpdateEnvironmentR return localVarReturnValue, localVarHTTPResponse, nil } +type ApiUpdateLinterRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + linterId string + linterResource *LinterResource +} + +func (r ApiUpdateLinterRequest) LinterResource(linterResource LinterResource) ApiUpdateLinterRequest { + r.linterResource = &linterResource + return r +} + +func (r ApiUpdateLinterRequest) Execute() (*LinterResource, *http.Response, error) { + return r.ApiService.UpdateLinterExecute(r) +} + +/* +UpdateLinter Update a Linter + +Update a Linter used on Tracetest + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param linterId ID of an Linter + @return ApiUpdateLinterRequest +*/ +func (a *ResourceApiApiService) UpdateLinter(ctx context.Context, linterId string) ApiUpdateLinterRequest { + return ApiUpdateLinterRequest{ + ApiService: a, + ctx: ctx, + linterId: linterId, + } +} + +// Execute executes the request +// +// @return LinterResource +func (a *ResourceApiApiService) UpdateLinterExecute(r ApiUpdateLinterRequest) (*LinterResource, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPut + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *LinterResource + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.UpdateLinter") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/linters/{LinterId}" + localVarPath = strings.Replace(localVarPath, "{"+"LinterId"+"}", url.PathEscape(parameterValueToString(r.linterId, "linterId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json", "text/yaml"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/yaml"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.linterResource + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiUpdatePollingProfileRequest struct { ctx context.Context ApiService *ResourceApiApiService diff --git a/cli/openapi/model_linter_resource.go b/cli/openapi/model_linter_resource.go new file mode 100644 index 0000000000..2a8e2080c5 --- /dev/null +++ b/cli/openapi/model_linter_resource.go @@ -0,0 +1,160 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResource type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResource{} + +// LinterResource struct for LinterResource +type LinterResource struct { + Type *string `json:"type,omitempty"` + Spec *LinterResourceSpec `json:"spec,omitempty"` +} + +// NewLinterResource instantiates a new LinterResource object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResource() *LinterResource { + this := LinterResource{} + return &this +} + +// NewLinterResourceWithDefaults instantiates a new LinterResource object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResourceWithDefaults() *LinterResource { + this := LinterResource{} + return &this +} + +// GetType returns the Type field value if set, zero value otherwise. +func (o *LinterResource) GetType() string { + if o == nil || isNil(o.Type) { + var ret string + return ret + } + return *o.Type +} + +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResource) GetTypeOk() (*string, bool) { + if o == nil || isNil(o.Type) { + return nil, false + } + return o.Type, true +} + +// HasType returns a boolean if a field has been set. +func (o *LinterResource) HasType() bool { + if o != nil && !isNil(o.Type) { + return true + } + + return false +} + +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *LinterResource) SetType(v string) { + o.Type = &v +} + +// GetSpec returns the Spec field value if set, zero value otherwise. +func (o *LinterResource) GetSpec() LinterResourceSpec { + if o == nil || isNil(o.Spec) { + var ret LinterResourceSpec + return ret + } + return *o.Spec +} + +// GetSpecOk returns a tuple with the Spec field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResource) GetSpecOk() (*LinterResourceSpec, bool) { + if o == nil || isNil(o.Spec) { + return nil, false + } + return o.Spec, true +} + +// HasSpec returns a boolean if a field has been set. +func (o *LinterResource) HasSpec() bool { + if o != nil && !isNil(o.Spec) { + return true + } + + return false +} + +// SetSpec gets a reference to the given LinterResourceSpec and assigns it to the Spec field. +func (o *LinterResource) SetSpec(v LinterResourceSpec) { + o.Spec = &v +} + +func (o LinterResource) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResource) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Type) { + toSerialize["type"] = o.Type + } + if !isNil(o.Spec) { + toSerialize["spec"] = o.Spec + } + return toSerialize, nil +} + +type NullableLinterResource struct { + value *LinterResource + isSet bool +} + +func (v NullableLinterResource) Get() *LinterResource { + return v.value +} + +func (v *NullableLinterResource) Set(val *LinterResource) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResource) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResource) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResource(val *LinterResource) *NullableLinterResource { + return &NullableLinterResource{value: val, isSet: true} +} + +func (v NullableLinterResource) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResource) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_resource_list.go b/cli/openapi/model_linter_resource_list.go new file mode 100644 index 0000000000..1bd0bdf6de --- /dev/null +++ b/cli/openapi/model_linter_resource_list.go @@ -0,0 +1,124 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResourceList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResourceList{} + +// LinterResourceList struct for LinterResourceList +type LinterResourceList struct { + Items []LinterResource `json:"items,omitempty"` +} + +// NewLinterResourceList instantiates a new LinterResourceList object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResourceList() *LinterResourceList { + this := LinterResourceList{} + return &this +} + +// NewLinterResourceListWithDefaults instantiates a new LinterResourceList object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResourceListWithDefaults() *LinterResourceList { + this := LinterResourceList{} + return &this +} + +// GetItems returns the Items field value if set, zero value otherwise. +func (o *LinterResourceList) GetItems() []LinterResource { + if o == nil || isNil(o.Items) { + var ret []LinterResource + return ret + } + return o.Items +} + +// GetItemsOk returns a tuple with the Items field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceList) GetItemsOk() ([]LinterResource, bool) { + if o == nil || isNil(o.Items) { + return nil, false + } + return o.Items, true +} + +// HasItems returns a boolean if a field has been set. +func (o *LinterResourceList) HasItems() bool { + if o != nil && !isNil(o.Items) { + return true + } + + return false +} + +// SetItems gets a reference to the given []LinterResource and assigns it to the Items field. +func (o *LinterResourceList) SetItems(v []LinterResource) { + o.Items = v +} + +func (o LinterResourceList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResourceList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Items) { + toSerialize["items"] = o.Items + } + return toSerialize, nil +} + +type NullableLinterResourceList struct { + value *LinterResourceList + isSet bool +} + +func (v NullableLinterResourceList) Get() *LinterResourceList { + return v.value +} + +func (v *NullableLinterResourceList) Set(val *LinterResourceList) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResourceList) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResourceList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResourceList(val *LinterResourceList) *NullableLinterResourceList { + return &NullableLinterResourceList{value: val, isSet: true} +} + +func (v NullableLinterResourceList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResourceList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_resource_plugin.go b/cli/openapi/model_linter_resource_plugin.go new file mode 100644 index 0000000000..16339b88d7 --- /dev/null +++ b/cli/openapi/model_linter_resource_plugin.go @@ -0,0 +1,196 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResourcePlugin type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResourcePlugin{} + +// LinterResourcePlugin struct for LinterResourcePlugin +type LinterResourcePlugin struct { + Name *string `json:"name,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Required *bool `json:"required,omitempty"` +} + +// NewLinterResourcePlugin instantiates a new LinterResourcePlugin object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResourcePlugin() *LinterResourcePlugin { + this := LinterResourcePlugin{} + return &this +} + +// NewLinterResourcePluginWithDefaults instantiates a new LinterResourcePlugin object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResourcePluginWithDefaults() *LinterResourcePlugin { + this := LinterResourcePlugin{} + return &this +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *LinterResourcePlugin) GetName() string { + if o == nil || isNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourcePlugin) GetNameOk() (*string, bool) { + if o == nil || isNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *LinterResourcePlugin) HasName() bool { + if o != nil && !isNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *LinterResourcePlugin) SetName(v string) { + o.Name = &v +} + +// GetEnabled returns the Enabled field value if set, zero value otherwise. +func (o *LinterResourcePlugin) GetEnabled() bool { + if o == nil || isNil(o.Enabled) { + var ret bool + return ret + } + return *o.Enabled +} + +// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourcePlugin) GetEnabledOk() (*bool, bool) { + if o == nil || isNil(o.Enabled) { + return nil, false + } + return o.Enabled, true +} + +// HasEnabled returns a boolean if a field has been set. +func (o *LinterResourcePlugin) HasEnabled() bool { + if o != nil && !isNil(o.Enabled) { + return true + } + + return false +} + +// SetEnabled gets a reference to the given bool and assigns it to the Enabled field. +func (o *LinterResourcePlugin) SetEnabled(v bool) { + o.Enabled = &v +} + +// GetRequired returns the Required field value if set, zero value otherwise. +func (o *LinterResourcePlugin) GetRequired() bool { + if o == nil || isNil(o.Required) { + var ret bool + return ret + } + return *o.Required +} + +// GetRequiredOk returns a tuple with the Required field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourcePlugin) GetRequiredOk() (*bool, bool) { + if o == nil || isNil(o.Required) { + return nil, false + } + return o.Required, true +} + +// HasRequired returns a boolean if a field has been set. +func (o *LinterResourcePlugin) HasRequired() bool { + if o != nil && !isNil(o.Required) { + return true + } + + return false +} + +// SetRequired gets a reference to the given bool and assigns it to the Required field. +func (o *LinterResourcePlugin) SetRequired(v bool) { + o.Required = &v +} + +func (o LinterResourcePlugin) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResourcePlugin) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Name) { + toSerialize["name"] = o.Name + } + if !isNil(o.Enabled) { + toSerialize["enabled"] = o.Enabled + } + if !isNil(o.Required) { + toSerialize["required"] = o.Required + } + return toSerialize, nil +} + +type NullableLinterResourcePlugin struct { + value *LinterResourcePlugin + isSet bool +} + +func (v NullableLinterResourcePlugin) Get() *LinterResourcePlugin { + return v.value +} + +func (v *NullableLinterResourcePlugin) Set(val *LinterResourcePlugin) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResourcePlugin) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResourcePlugin) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResourcePlugin(val *LinterResourcePlugin) *NullableLinterResourcePlugin { + return &NullableLinterResourcePlugin{value: val, isSet: true} +} + +func (v NullableLinterResourcePlugin) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResourcePlugin) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_resource_spec.go b/cli/openapi/model_linter_resource_spec.go new file mode 100644 index 0000000000..97004d7e98 --- /dev/null +++ b/cli/openapi/model_linter_resource_spec.go @@ -0,0 +1,268 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResourceSpec type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResourceSpec{} + +// LinterResourceSpec struct for LinterResourceSpec +type LinterResourceSpec struct { + Id *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + MinimumScore *int32 `json:"minimumScore,omitempty"` + Plugins []LinterResourcePlugin `json:"plugins,omitempty"` +} + +// NewLinterResourceSpec instantiates a new LinterResourceSpec object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResourceSpec() *LinterResourceSpec { + this := LinterResourceSpec{} + return &this +} + +// NewLinterResourceSpecWithDefaults instantiates a new LinterResourceSpec object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResourceSpecWithDefaults() *LinterResourceSpec { + this := LinterResourceSpec{} + return &this +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *LinterResourceSpec) GetId() string { + if o == nil || isNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceSpec) GetIdOk() (*string, bool) { + if o == nil || isNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *LinterResourceSpec) HasId() bool { + if o != nil && !isNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *LinterResourceSpec) SetId(v string) { + o.Id = &v +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *LinterResourceSpec) GetName() string { + if o == nil || isNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceSpec) GetNameOk() (*string, bool) { + if o == nil || isNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *LinterResourceSpec) HasName() bool { + if o != nil && !isNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *LinterResourceSpec) SetName(v string) { + o.Name = &v +} + +// GetEnabled returns the Enabled field value if set, zero value otherwise. +func (o *LinterResourceSpec) GetEnabled() bool { + if o == nil || isNil(o.Enabled) { + var ret bool + return ret + } + return *o.Enabled +} + +// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceSpec) GetEnabledOk() (*bool, bool) { + if o == nil || isNil(o.Enabled) { + return nil, false + } + return o.Enabled, true +} + +// HasEnabled returns a boolean if a field has been set. +func (o *LinterResourceSpec) HasEnabled() bool { + if o != nil && !isNil(o.Enabled) { + return true + } + + return false +} + +// SetEnabled gets a reference to the given bool and assigns it to the Enabled field. +func (o *LinterResourceSpec) SetEnabled(v bool) { + o.Enabled = &v +} + +// GetMinimumScore returns the MinimumScore field value if set, zero value otherwise. +func (o *LinterResourceSpec) GetMinimumScore() int32 { + if o == nil || isNil(o.MinimumScore) { + var ret int32 + return ret + } + return *o.MinimumScore +} + +// GetMinimumScoreOk returns a tuple with the MinimumScore field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceSpec) GetMinimumScoreOk() (*int32, bool) { + if o == nil || isNil(o.MinimumScore) { + return nil, false + } + return o.MinimumScore, true +} + +// HasMinimumScore returns a boolean if a field has been set. +func (o *LinterResourceSpec) HasMinimumScore() bool { + if o != nil && !isNil(o.MinimumScore) { + return true + } + + return false +} + +// SetMinimumScore gets a reference to the given int32 and assigns it to the MinimumScore field. +func (o *LinterResourceSpec) SetMinimumScore(v int32) { + o.MinimumScore = &v +} + +// GetPlugins returns the Plugins field value if set, zero value otherwise. +func (o *LinterResourceSpec) GetPlugins() []LinterResourcePlugin { + if o == nil || isNil(o.Plugins) { + var ret []LinterResourcePlugin + return ret + } + return o.Plugins +} + +// GetPluginsOk returns a tuple with the Plugins field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResourceSpec) GetPluginsOk() ([]LinterResourcePlugin, bool) { + if o == nil || isNil(o.Plugins) { + return nil, false + } + return o.Plugins, true +} + +// HasPlugins returns a boolean if a field has been set. +func (o *LinterResourceSpec) HasPlugins() bool { + if o != nil && !isNil(o.Plugins) { + return true + } + + return false +} + +// SetPlugins gets a reference to the given []LinterResourcePlugin and assigns it to the Plugins field. +func (o *LinterResourceSpec) SetPlugins(v []LinterResourcePlugin) { + o.Plugins = v +} + +func (o LinterResourceSpec) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResourceSpec) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Id) { + toSerialize["id"] = o.Id + } + if !isNil(o.Name) { + toSerialize["name"] = o.Name + } + if !isNil(o.Enabled) { + toSerialize["enabled"] = o.Enabled + } + if !isNil(o.MinimumScore) { + toSerialize["minimumScore"] = o.MinimumScore + } + if !isNil(o.Plugins) { + toSerialize["plugins"] = o.Plugins + } + return toSerialize, nil +} + +type NullableLinterResourceSpec struct { + value *LinterResourceSpec + isSet bool +} + +func (v NullableLinterResourceSpec) Get() *LinterResourceSpec { + return v.value +} + +func (v *NullableLinterResourceSpec) Set(val *LinterResourceSpec) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResourceSpec) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResourceSpec) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResourceSpec(val *LinterResourceSpec) *NullableLinterResourceSpec { + return &NullableLinterResourceSpec{value: val, isSet: true} +} + +func (v NullableLinterResourceSpec) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResourceSpec) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_result.go b/cli/openapi/model_linter_result.go new file mode 100644 index 0000000000..51b01e9c3b --- /dev/null +++ b/cli/openapi/model_linter_result.go @@ -0,0 +1,196 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResult type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResult{} + +// LinterResult struct for LinterResult +type LinterResult struct { + Passed *bool `json:"passed,omitempty"` + Score *int32 `json:"score,omitempty"` + Plugins []LinterResultPlugin `json:"plugins,omitempty"` +} + +// NewLinterResult instantiates a new LinterResult object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResult() *LinterResult { + this := LinterResult{} + return &this +} + +// NewLinterResultWithDefaults instantiates a new LinterResult object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResultWithDefaults() *LinterResult { + this := LinterResult{} + return &this +} + +// GetPassed returns the Passed field value if set, zero value otherwise. +func (o *LinterResult) GetPassed() bool { + if o == nil || isNil(o.Passed) { + var ret bool + return ret + } + return *o.Passed +} + +// GetPassedOk returns a tuple with the Passed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResult) GetPassedOk() (*bool, bool) { + if o == nil || isNil(o.Passed) { + return nil, false + } + return o.Passed, true +} + +// HasPassed returns a boolean if a field has been set. +func (o *LinterResult) HasPassed() bool { + if o != nil && !isNil(o.Passed) { + return true + } + + return false +} + +// SetPassed gets a reference to the given bool and assigns it to the Passed field. +func (o *LinterResult) SetPassed(v bool) { + o.Passed = &v +} + +// GetScore returns the Score field value if set, zero value otherwise. +func (o *LinterResult) GetScore() int32 { + if o == nil || isNil(o.Score) { + var ret int32 + return ret + } + return *o.Score +} + +// GetScoreOk returns a tuple with the Score field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResult) GetScoreOk() (*int32, bool) { + if o == nil || isNil(o.Score) { + return nil, false + } + return o.Score, true +} + +// HasScore returns a boolean if a field has been set. +func (o *LinterResult) HasScore() bool { + if o != nil && !isNil(o.Score) { + return true + } + + return false +} + +// SetScore gets a reference to the given int32 and assigns it to the Score field. +func (o *LinterResult) SetScore(v int32) { + o.Score = &v +} + +// GetPlugins returns the Plugins field value if set, zero value otherwise. +func (o *LinterResult) GetPlugins() []LinterResultPlugin { + if o == nil || isNil(o.Plugins) { + var ret []LinterResultPlugin + return ret + } + return o.Plugins +} + +// GetPluginsOk returns a tuple with the Plugins field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResult) GetPluginsOk() ([]LinterResultPlugin, bool) { + if o == nil || isNil(o.Plugins) { + return nil, false + } + return o.Plugins, true +} + +// HasPlugins returns a boolean if a field has been set. +func (o *LinterResult) HasPlugins() bool { + if o != nil && !isNil(o.Plugins) { + return true + } + + return false +} + +// SetPlugins gets a reference to the given []LinterResultPlugin and assigns it to the Plugins field. +func (o *LinterResult) SetPlugins(v []LinterResultPlugin) { + o.Plugins = v +} + +func (o LinterResult) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResult) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Passed) { + toSerialize["passed"] = o.Passed + } + if !isNil(o.Score) { + toSerialize["score"] = o.Score + } + if !isNil(o.Plugins) { + toSerialize["plugins"] = o.Plugins + } + return toSerialize, nil +} + +type NullableLinterResult struct { + value *LinterResult + isSet bool +} + +func (v NullableLinterResult) Get() *LinterResult { + return v.value +} + +func (v *NullableLinterResult) Set(val *LinterResult) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResult) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResult) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResult(val *LinterResult) *NullableLinterResult { + return &NullableLinterResult{value: val, isSet: true} +} + +func (v NullableLinterResult) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResult) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_result_plugin.go b/cli/openapi/model_linter_result_plugin.go new file mode 100644 index 0000000000..294fc29e1b --- /dev/null +++ b/cli/openapi/model_linter_result_plugin.go @@ -0,0 +1,268 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResultPlugin type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResultPlugin{} + +// LinterResultPlugin struct for LinterResultPlugin +type LinterResultPlugin struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Passed *bool `json:"passed,omitempty"` + Score *int32 `json:"score,omitempty"` + Rules []LinterResultPluginRule `json:"rules,omitempty"` +} + +// NewLinterResultPlugin instantiates a new LinterResultPlugin object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResultPlugin() *LinterResultPlugin { + this := LinterResultPlugin{} + return &this +} + +// NewLinterResultPluginWithDefaults instantiates a new LinterResultPlugin object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResultPluginWithDefaults() *LinterResultPlugin { + this := LinterResultPlugin{} + return &this +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *LinterResultPlugin) GetName() string { + if o == nil || isNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPlugin) GetNameOk() (*string, bool) { + if o == nil || isNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *LinterResultPlugin) HasName() bool { + if o != nil && !isNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *LinterResultPlugin) SetName(v string) { + o.Name = &v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *LinterResultPlugin) GetDescription() string { + if o == nil || isNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPlugin) GetDescriptionOk() (*string, bool) { + if o == nil || isNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *LinterResultPlugin) HasDescription() bool { + if o != nil && !isNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *LinterResultPlugin) SetDescription(v string) { + o.Description = &v +} + +// GetPassed returns the Passed field value if set, zero value otherwise. +func (o *LinterResultPlugin) GetPassed() bool { + if o == nil || isNil(o.Passed) { + var ret bool + return ret + } + return *o.Passed +} + +// GetPassedOk returns a tuple with the Passed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPlugin) GetPassedOk() (*bool, bool) { + if o == nil || isNil(o.Passed) { + return nil, false + } + return o.Passed, true +} + +// HasPassed returns a boolean if a field has been set. +func (o *LinterResultPlugin) HasPassed() bool { + if o != nil && !isNil(o.Passed) { + return true + } + + return false +} + +// SetPassed gets a reference to the given bool and assigns it to the Passed field. +func (o *LinterResultPlugin) SetPassed(v bool) { + o.Passed = &v +} + +// GetScore returns the Score field value if set, zero value otherwise. +func (o *LinterResultPlugin) GetScore() int32 { + if o == nil || isNil(o.Score) { + var ret int32 + return ret + } + return *o.Score +} + +// GetScoreOk returns a tuple with the Score field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPlugin) GetScoreOk() (*int32, bool) { + if o == nil || isNil(o.Score) { + return nil, false + } + return o.Score, true +} + +// HasScore returns a boolean if a field has been set. +func (o *LinterResultPlugin) HasScore() bool { + if o != nil && !isNil(o.Score) { + return true + } + + return false +} + +// SetScore gets a reference to the given int32 and assigns it to the Score field. +func (o *LinterResultPlugin) SetScore(v int32) { + o.Score = &v +} + +// GetRules returns the Rules field value if set, zero value otherwise. +func (o *LinterResultPlugin) GetRules() []LinterResultPluginRule { + if o == nil || isNil(o.Rules) { + var ret []LinterResultPluginRule + return ret + } + return o.Rules +} + +// GetRulesOk returns a tuple with the Rules field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPlugin) GetRulesOk() ([]LinterResultPluginRule, bool) { + if o == nil || isNil(o.Rules) { + return nil, false + } + return o.Rules, true +} + +// HasRules returns a boolean if a field has been set. +func (o *LinterResultPlugin) HasRules() bool { + if o != nil && !isNil(o.Rules) { + return true + } + + return false +} + +// SetRules gets a reference to the given []LinterResultPluginRule and assigns it to the Rules field. +func (o *LinterResultPlugin) SetRules(v []LinterResultPluginRule) { + o.Rules = v +} + +func (o LinterResultPlugin) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResultPlugin) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Name) { + toSerialize["name"] = o.Name + } + if !isNil(o.Description) { + toSerialize["description"] = o.Description + } + if !isNil(o.Passed) { + toSerialize["passed"] = o.Passed + } + if !isNil(o.Score) { + toSerialize["score"] = o.Score + } + if !isNil(o.Rules) { + toSerialize["rules"] = o.Rules + } + return toSerialize, nil +} + +type NullableLinterResultPlugin struct { + value *LinterResultPlugin + isSet bool +} + +func (v NullableLinterResultPlugin) Get() *LinterResultPlugin { + return v.value +} + +func (v *NullableLinterResultPlugin) Set(val *LinterResultPlugin) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResultPlugin) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResultPlugin) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResultPlugin(val *LinterResultPlugin) *NullableLinterResultPlugin { + return &NullableLinterResultPlugin{value: val, isSet: true} +} + +func (v NullableLinterResultPlugin) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResultPlugin) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_result_plugin_rule.go b/cli/openapi/model_linter_result_plugin_rule.go new file mode 100644 index 0000000000..d4997116bc --- /dev/null +++ b/cli/openapi/model_linter_result_plugin_rule.go @@ -0,0 +1,304 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResultPluginRule type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResultPluginRule{} + +// LinterResultPluginRule struct for LinterResultPluginRule +type LinterResultPluginRule struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Passed *bool `json:"passed,omitempty"` + Weight *int32 `json:"weight,omitempty"` + Tips []string `json:"tips,omitempty"` + Results []LinterResultPluginRuleResult `json:"results,omitempty"` +} + +// NewLinterResultPluginRule instantiates a new LinterResultPluginRule object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResultPluginRule() *LinterResultPluginRule { + this := LinterResultPluginRule{} + return &this +} + +// NewLinterResultPluginRuleWithDefaults instantiates a new LinterResultPluginRule object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResultPluginRuleWithDefaults() *LinterResultPluginRule { + this := LinterResultPluginRule{} + return &this +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetName() string { + if o == nil || isNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetNameOk() (*string, bool) { + if o == nil || isNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasName() bool { + if o != nil && !isNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *LinterResultPluginRule) SetName(v string) { + o.Name = &v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetDescription() string { + if o == nil || isNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetDescriptionOk() (*string, bool) { + if o == nil || isNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasDescription() bool { + if o != nil && !isNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *LinterResultPluginRule) SetDescription(v string) { + o.Description = &v +} + +// GetPassed returns the Passed field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetPassed() bool { + if o == nil || isNil(o.Passed) { + var ret bool + return ret + } + return *o.Passed +} + +// GetPassedOk returns a tuple with the Passed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetPassedOk() (*bool, bool) { + if o == nil || isNil(o.Passed) { + return nil, false + } + return o.Passed, true +} + +// HasPassed returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasPassed() bool { + if o != nil && !isNil(o.Passed) { + return true + } + + return false +} + +// SetPassed gets a reference to the given bool and assigns it to the Passed field. +func (o *LinterResultPluginRule) SetPassed(v bool) { + o.Passed = &v +} + +// GetWeight returns the Weight field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetWeight() int32 { + if o == nil || isNil(o.Weight) { + var ret int32 + return ret + } + return *o.Weight +} + +// GetWeightOk returns a tuple with the Weight field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetWeightOk() (*int32, bool) { + if o == nil || isNil(o.Weight) { + return nil, false + } + return o.Weight, true +} + +// HasWeight returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasWeight() bool { + if o != nil && !isNil(o.Weight) { + return true + } + + return false +} + +// SetWeight gets a reference to the given int32 and assigns it to the Weight field. +func (o *LinterResultPluginRule) SetWeight(v int32) { + o.Weight = &v +} + +// GetTips returns the Tips field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetTips() []string { + if o == nil || isNil(o.Tips) { + var ret []string + return ret + } + return o.Tips +} + +// GetTipsOk returns a tuple with the Tips field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetTipsOk() ([]string, bool) { + if o == nil || isNil(o.Tips) { + return nil, false + } + return o.Tips, true +} + +// HasTips returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasTips() bool { + if o != nil && !isNil(o.Tips) { + return true + } + + return false +} + +// SetTips gets a reference to the given []string and assigns it to the Tips field. +func (o *LinterResultPluginRule) SetTips(v []string) { + o.Tips = v +} + +// GetResults returns the Results field value if set, zero value otherwise. +func (o *LinterResultPluginRule) GetResults() []LinterResultPluginRuleResult { + if o == nil || isNil(o.Results) { + var ret []LinterResultPluginRuleResult + return ret + } + return o.Results +} + +// GetResultsOk returns a tuple with the Results field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRule) GetResultsOk() ([]LinterResultPluginRuleResult, bool) { + if o == nil || isNil(o.Results) { + return nil, false + } + return o.Results, true +} + +// HasResults returns a boolean if a field has been set. +func (o *LinterResultPluginRule) HasResults() bool { + if o != nil && !isNil(o.Results) { + return true + } + + return false +} + +// SetResults gets a reference to the given []LinterResultPluginRuleResult and assigns it to the Results field. +func (o *LinterResultPluginRule) SetResults(v []LinterResultPluginRuleResult) { + o.Results = v +} + +func (o LinterResultPluginRule) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResultPluginRule) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Name) { + toSerialize["name"] = o.Name + } + if !isNil(o.Description) { + toSerialize["description"] = o.Description + } + if !isNil(o.Passed) { + toSerialize["passed"] = o.Passed + } + if !isNil(o.Weight) { + toSerialize["weight"] = o.Weight + } + if !isNil(o.Tips) { + toSerialize["tips"] = o.Tips + } + if !isNil(o.Results) { + toSerialize["results"] = o.Results + } + return toSerialize, nil +} + +type NullableLinterResultPluginRule struct { + value *LinterResultPluginRule + isSet bool +} + +func (v NullableLinterResultPluginRule) Get() *LinterResultPluginRule { + return v.value +} + +func (v *NullableLinterResultPluginRule) Set(val *LinterResultPluginRule) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResultPluginRule) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResultPluginRule) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResultPluginRule(val *LinterResultPluginRule) *NullableLinterResultPluginRule { + return &NullableLinterResultPluginRule{value: val, isSet: true} +} + +func (v NullableLinterResultPluginRule) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResultPluginRule) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_linter_result_plugin_rule_result.go b/cli/openapi/model_linter_result_plugin_rule_result.go new file mode 100644 index 0000000000..bd480aa6d7 --- /dev/null +++ b/cli/openapi/model_linter_result_plugin_rule_result.go @@ -0,0 +1,232 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the LinterResultPluginRuleResult type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &LinterResultPluginRuleResult{} + +// LinterResultPluginRuleResult struct for LinterResultPluginRuleResult +type LinterResultPluginRuleResult struct { + SpanId *string `json:"spanId,omitempty"` + Errors []string `json:"errors,omitempty"` + Passed *bool `json:"passed,omitempty"` + Severity *string `json:"severity,omitempty"` +} + +// NewLinterResultPluginRuleResult instantiates a new LinterResultPluginRuleResult object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewLinterResultPluginRuleResult() *LinterResultPluginRuleResult { + this := LinterResultPluginRuleResult{} + return &this +} + +// NewLinterResultPluginRuleResultWithDefaults instantiates a new LinterResultPluginRuleResult object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewLinterResultPluginRuleResultWithDefaults() *LinterResultPluginRuleResult { + this := LinterResultPluginRuleResult{} + return &this +} + +// GetSpanId returns the SpanId field value if set, zero value otherwise. +func (o *LinterResultPluginRuleResult) GetSpanId() string { + if o == nil || isNil(o.SpanId) { + var ret string + return ret + } + return *o.SpanId +} + +// GetSpanIdOk returns a tuple with the SpanId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRuleResult) GetSpanIdOk() (*string, bool) { + if o == nil || isNil(o.SpanId) { + return nil, false + } + return o.SpanId, true +} + +// HasSpanId returns a boolean if a field has been set. +func (o *LinterResultPluginRuleResult) HasSpanId() bool { + if o != nil && !isNil(o.SpanId) { + return true + } + + return false +} + +// SetSpanId gets a reference to the given string and assigns it to the SpanId field. +func (o *LinterResultPluginRuleResult) SetSpanId(v string) { + o.SpanId = &v +} + +// GetErrors returns the Errors field value if set, zero value otherwise. +func (o *LinterResultPluginRuleResult) GetErrors() []string { + if o == nil || isNil(o.Errors) { + var ret []string + return ret + } + return o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRuleResult) GetErrorsOk() ([]string, bool) { + if o == nil || isNil(o.Errors) { + return nil, false + } + return o.Errors, true +} + +// HasErrors returns a boolean if a field has been set. +func (o *LinterResultPluginRuleResult) HasErrors() bool { + if o != nil && !isNil(o.Errors) { + return true + } + + return false +} + +// SetErrors gets a reference to the given []string and assigns it to the Errors field. +func (o *LinterResultPluginRuleResult) SetErrors(v []string) { + o.Errors = v +} + +// GetPassed returns the Passed field value if set, zero value otherwise. +func (o *LinterResultPluginRuleResult) GetPassed() bool { + if o == nil || isNil(o.Passed) { + var ret bool + return ret + } + return *o.Passed +} + +// GetPassedOk returns a tuple with the Passed field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRuleResult) GetPassedOk() (*bool, bool) { + if o == nil || isNil(o.Passed) { + return nil, false + } + return o.Passed, true +} + +// HasPassed returns a boolean if a field has been set. +func (o *LinterResultPluginRuleResult) HasPassed() bool { + if o != nil && !isNil(o.Passed) { + return true + } + + return false +} + +// SetPassed gets a reference to the given bool and assigns it to the Passed field. +func (o *LinterResultPluginRuleResult) SetPassed(v bool) { + o.Passed = &v +} + +// GetSeverity returns the Severity field value if set, zero value otherwise. +func (o *LinterResultPluginRuleResult) GetSeverity() string { + if o == nil || isNil(o.Severity) { + var ret string + return ret + } + return *o.Severity +} + +// GetSeverityOk returns a tuple with the Severity field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LinterResultPluginRuleResult) GetSeverityOk() (*string, bool) { + if o == nil || isNil(o.Severity) { + return nil, false + } + return o.Severity, true +} + +// HasSeverity returns a boolean if a field has been set. +func (o *LinterResultPluginRuleResult) HasSeverity() bool { + if o != nil && !isNil(o.Severity) { + return true + } + + return false +} + +// SetSeverity gets a reference to the given string and assigns it to the Severity field. +func (o *LinterResultPluginRuleResult) SetSeverity(v string) { + o.Severity = &v +} + +func (o LinterResultPluginRuleResult) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o LinterResultPluginRuleResult) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.SpanId) { + toSerialize["spanId"] = o.SpanId + } + if !isNil(o.Errors) { + toSerialize["errors"] = o.Errors + } + if !isNil(o.Passed) { + toSerialize["passed"] = o.Passed + } + if !isNil(o.Severity) { + toSerialize["severity"] = o.Severity + } + return toSerialize, nil +} + +type NullableLinterResultPluginRuleResult struct { + value *LinterResultPluginRuleResult + isSet bool +} + +func (v NullableLinterResultPluginRuleResult) Get() *LinterResultPluginRuleResult { + return v.value +} + +func (v *NullableLinterResultPluginRuleResult) Set(val *LinterResultPluginRuleResult) { + v.value = val + v.isSet = true +} + +func (v NullableLinterResultPluginRuleResult) IsSet() bool { + return v.isSet +} + +func (v *NullableLinterResultPluginRuleResult) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableLinterResultPluginRuleResult(val *LinterResultPluginRuleResult) *NullableLinterResultPluginRuleResult { + return &NullableLinterResultPluginRuleResult{value: val, isSet: true} +} + +func (v NullableLinterResultPluginRuleResult) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableLinterResultPluginRuleResult) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_span.go b/cli/openapi/model_span.go index d254a0cc5e..a01e672549 100644 --- a/cli/openapi/model_span.go +++ b/cli/openapi/model_span.go @@ -22,6 +22,7 @@ type Span struct { Id *string `json:"id,omitempty"` ParentId *string `json:"parentId,omitempty"` Name *string `json:"name,omitempty"` + Kind *string `json:"kind,omitempty"` // span start time in unix milli format StartTime *int64 `json:"startTime,omitempty"` // span end time in unix milli format @@ -144,6 +145,38 @@ func (o *Span) SetName(v string) { o.Name = &v } +// GetKind returns the Kind field value if set, zero value otherwise. +func (o *Span) GetKind() string { + if o == nil || isNil(o.Kind) { + var ret string + return ret + } + return *o.Kind +} + +// GetKindOk returns a tuple with the Kind field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Span) GetKindOk() (*string, bool) { + if o == nil || isNil(o.Kind) { + return nil, false + } + return o.Kind, true +} + +// HasKind returns a boolean if a field has been set. +func (o *Span) HasKind() bool { + if o != nil && !isNil(o.Kind) { + return true + } + + return false +} + +// SetKind gets a reference to the given string and assigns it to the Kind field. +func (o *Span) SetKind(v string) { + o.Kind = &v +} + // GetStartTime returns the StartTime field value if set, zero value otherwise. func (o *Span) GetStartTime() int64 { if o == nil || isNil(o.StartTime) { @@ -291,6 +324,9 @@ func (o Span) ToMap() (map[string]interface{}, error) { if !isNil(o.Name) { toSerialize["name"] = o.Name } + if !isNil(o.Kind) { + toSerialize["kind"] = o.Kind + } if !isNil(o.StartTime) { toSerialize["startTime"] = o.StartTime } diff --git a/cli/openapi/model_test_run.go b/cli/openapi/model_test_run.go index e416af192c..4044e6bbe4 100644 --- a/cli/openapi/model_test_run.go +++ b/cli/openapi/model_test_run.go @@ -42,6 +42,7 @@ type TestRun struct { TriggerResult *TriggerResult `json:"triggerResult,omitempty"` Trace *Trace `json:"trace,omitempty"` Result *AssertionResults `json:"result,omitempty"` + Linter *LinterResult `json:"linter,omitempty"` Outputs []TestRunOutputsInner `json:"outputs,omitempty"` Metadata *map[string]string `json:"metadata,omitempty"` TransactionId *string `json:"transactionId,omitempty"` @@ -609,6 +610,38 @@ func (o *TestRun) SetResult(v AssertionResults) { o.Result = &v } +// GetLinter returns the Linter field value if set, zero value otherwise. +func (o *TestRun) GetLinter() LinterResult { + if o == nil || isNil(o.Linter) { + var ret LinterResult + return ret + } + return *o.Linter +} + +// GetLinterOk returns a tuple with the Linter field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TestRun) GetLinterOk() (*LinterResult, bool) { + if o == nil || isNil(o.Linter) { + return nil, false + } + return o.Linter, true +} + +// HasLinter returns a boolean if a field has been set. +func (o *TestRun) HasLinter() bool { + if o != nil && !isNil(o.Linter) { + return true + } + + return false +} + +// SetLinter gets a reference to the given LinterResult and assigns it to the Linter field. +func (o *TestRun) SetLinter(v LinterResult) { + o.Linter = &v +} + // GetOutputs returns the Outputs field value if set, zero value otherwise. func (o *TestRun) GetOutputs() []TestRunOutputsInner { if o == nil || isNil(o.Outputs) { @@ -792,6 +825,9 @@ func (o TestRun) ToMap() (map[string]interface{}, error) { if !isNil(o.Result) { toSerialize["result"] = o.Result } + if !isNil(o.Linter) { + toSerialize["linter"] = o.Linter + } if !isNil(o.Outputs) { toSerialize["outputs"] = o.Outputs } diff --git a/examples/quick-start-jaeger-nodejs/tracetest/docker-compose.yaml b/examples/quick-start-jaeger-nodejs/tracetest/docker-compose.yaml index 2b17c6a00f..22dfdf71cc 100644 --- a/examples/quick-start-jaeger-nodejs/tracetest/docker-compose.yaml +++ b/examples/quick-start-jaeger-nodejs/tracetest/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3" services: tracetest: image: kubeshop/tracetest:${TAG:-latest} - platform: linux/amd64 + # platform: linux/amd64 volumes: - type: bind source: ./tracetest/tracetest.config.yaml @@ -38,6 +38,8 @@ services: interval: 1s timeout: 5s retries: 60 + ports: + - "5432:5432" otel-collector: image: otel/opentelemetry-collector-contrib:0.59.0 @@ -52,6 +54,7 @@ services: restart: unless-stopped ports: - "16686:16686" + - "16685:16685" healthcheck: test: ["CMD", "wget", "--spider", "localhost:16686"] interval: 1s diff --git a/go.work.sum b/go.work.sum index 6246b36641..84d2eed6b5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -150,27 +150,35 @@ cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v59.4.0+incompatible h1:gDA8odnngdNd3KYHL2NoK1j9vpWBgEnFSjKKLpkC8Aw= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI= github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -182,7 +190,6 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4= @@ -196,20 +203,13 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio= github.com/knadh/koanf v1.4.3/go.mod h1:5FAkuykKXZvLqhAbP4peWgM5CTcZmn7L1d27k/a+kfg= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -219,9 +219,7 @@ github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrb github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -241,7 +239,6 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/collector/semconv v0.60.0/go.mod h1:aRkHuJ/OshtDFYluKEtnG5nkKTsy1HZuvZVHmakx+Vo= go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0/go.mod h1:fk1+icoN47ytLSgkoWHLJrtVTSQ+HgmkNgPTKrk/Nsc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 h1:9NkMW03wwEzPtP/KciZ4Ozu/Uz5ZA7kfqXJIObnrjGU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0/go.mod h1:548ZsYzmT4PL4zWKRd8q/N4z0Wxzn/ZxUE+lkEpwWQA= go.opentelemetry.io/contrib/zpages v0.34.0/go.mod h1:zuVCe4eoOREH+liRJLCtGITqL3NiUvkdr6U/4j9iQRg= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= @@ -346,12 +343,5 @@ google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= diff --git a/server/app/app.go b/server/app/app.go index af2fa22f72..00112a6cbe 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -23,6 +23,7 @@ import ( httpServer "github.com/kubeshop/tracetest/server/http" "github.com/kubeshop/tracetest/server/http/mappings" "github.com/kubeshop/tracetest/server/http/websocket" + linterResource "github.com/kubeshop/tracetest/server/linter/resource" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" "github.com/kubeshop/tracetest/server/otlp" @@ -195,6 +196,7 @@ func (app *App) Start(opts ...appOption) error { pollingProfileRepo := pollingprofile.NewRepository(db) dataStoreRepo := datastoreresource.NewRepository(db) environmentRepo := environment.NewRepository(db) + linterRepo := linterResource.NewRepository(db) eventEmitter := executor.NewEventEmitter(testDB, subscriptionManager) registerOtlpServer(app, testDB, eventEmitter, dataStoreRepo) @@ -202,6 +204,7 @@ func (app *App) Start(opts ...appOption) error { rf := newRunnerFacades( pollingProfileRepo, dataStoreRepo, + linterRepo, testDB, transactionsRepository, applicationTracer, @@ -216,6 +219,7 @@ func (app *App) Start(opts ...appOption) error { rf.runner.Start(5) rf.transactionRunner.Start(5) rf.assertionRunner.Start(5) + rf.linterRunner.Start(5) app.registerStopFn(func() { fmt.Println("stopping tracePoller") @@ -257,6 +261,7 @@ func (app *App) Start(opts ...appOption) error { registerDemosResource(demoRepo, apiRouter, db, provisioner, tracer) registerDataStoreResource(dataStoreRepo, apiRouter, db, provisioner, tracer) + registerlinterResource(linterRepo, apiRouter, db, provisioner, tracer) isTracetestDev := os.Getenv("TRACETEST_DEV") != "" registerSPAHandler(router, app.cfg, configFromDB.IsAnalyticsEnabled(), serverID, isTracetestDev) @@ -312,6 +317,18 @@ func registerOtlpServer(app *App, testDB model.Repository, eventEmitter executor }) } +func registerlinterResource(linterRepo *linterResource.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { + manager := resourcemanager.New[linterResource.Linter]( + linterResource.ResourceName, + linterResource.ResourceNamePlural, + linterRepo, + resourcemanager.WithOperations(linterResource.Operations...), + resourcemanager.WithTracer(tracer), + ) + manager.RegisterRoutes(router) + provisioner.AddResourceProvisioner(manager) +} + func registerTransactionResource(repo *tests.TransactionsRepository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[tests.Transaction]( tests.TransactionResourceName, diff --git a/server/app/facade.go b/server/app/facade.go index 825c8f572f..5d652c00bb 100644 --- a/server/app/facade.go +++ b/server/app/facade.go @@ -7,6 +7,7 @@ import ( "github.com/kubeshop/tracetest/server/executor" "github.com/kubeshop/tracetest/server/executor/pollingprofile" "github.com/kubeshop/tracetest/server/executor/trigger" + linterResource "github.com/kubeshop/tracetest/server/linter/resource" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" @@ -22,6 +23,7 @@ type runnerFacade struct { transactionRunner executor.PersistentTransactionRunner assertionRunner executor.AssertionRunner tracePoller executor.PersistentTracePoller + linterRunner executor.LinterRunner } func (rf runnerFacade) StopTest(testID id.ID, runID int) { @@ -51,6 +53,7 @@ func (rf runnerFacade) RunAssertions(ctx context.Context, request executor.Asser func newRunnerFacades( ppRepo *pollingprofile.Repository, dsRepo *datastoreresource.Repository, + lintRepo *linterResource.Repository, testDB model.Repository, transactions *tests.TransactionsRepository, appTracer trace.Tracer, @@ -72,6 +75,14 @@ func newRunnerFacades( eventEmitter, ) + linterRunner := executor.NewlinterRunner( + execTestUpdater, + subscriptionManager, + eventEmitter, + assertionRunner, + lintRepo, + ) + pollerExecutor := executor.NewPollerExecutor( ppRepo, tracer, @@ -85,7 +96,7 @@ func newRunnerFacades( pollerExecutor, ppRepo, execTestUpdater, - assertionRunner, + linterRunner, subscriptionManager, eventEmitter, ) @@ -115,5 +126,6 @@ func newRunnerFacades( transactionRunner: transactionRunner, assertionRunner: assertionRunner, tracePoller: tracePoller, + linterRunner: linterRunner, } } diff --git a/server/executor/linter_runner.go b/server/executor/linter_runner.go new file mode 100644 index 0000000000..22dbcf4f7b --- /dev/null +++ b/server/executor/linter_runner.go @@ -0,0 +1,200 @@ +package executor + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/kubeshop/tracetest/server/analytics" + "github.com/kubeshop/tracetest/server/linter" + linterResource "github.com/kubeshop/tracetest/server/linter/resource" + "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/model/events" + "github.com/kubeshop/tracetest/server/subscription" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +type LinterRequest struct { + carrier propagation.MapCarrier + Test model.Test + Run model.Run +} + +func (r LinterRequest) Context() context.Context { + ctx := context.Background() + return otel.GetTextMapPropagator().Extract(ctx, r.carrier) +} + +type LinterRunner interface { + RunLinter(ctx context.Context, request LinterRequest) + WorkerPool +} + +type LinterResourceGetter interface { + GetDefault(ctx context.Context) linterResource.Linter +} + +type defaultlinterRunner struct { + updater RunUpdater + inputChannel chan LinterRequest + exitChannel chan bool + subscriptionManager *subscription.Manager + eventEmitter EventEmitter + linterResourceGetter LinterResourceGetter + assertionRunner AssertionRunner +} + +var _ WorkerPool = &defaultlinterRunner{} +var _ LinterRunner = &defaultlinterRunner{} + +func NewlinterRunner( + updater RunUpdater, + subscriptionManager *subscription.Manager, + eventEmitter EventEmitter, + assertionRunner AssertionRunner, + linterResourceGetter LinterResourceGetter, +) LinterRunner { + return &defaultlinterRunner{ + updater: updater, + inputChannel: make(chan LinterRequest, 1), + subscriptionManager: subscriptionManager, + eventEmitter: eventEmitter, + assertionRunner: assertionRunner, + linterResourceGetter: linterResourceGetter, + } +} + +func (e *defaultlinterRunner) Start(workers int) { + e.exitChannel = make(chan bool, workers) + + for i := 0; i < workers; i++ { + go e.startWorker() + } +} + +func (e *defaultlinterRunner) Stop() { + ticker := time.NewTicker(1 * time.Second) + for { + select { + case <-ticker.C: + e.exitChannel <- true + return + } + } +} + +func (e *defaultlinterRunner) startWorker() { + for { + select { + case <-e.exitChannel: + fmt.Println("Exiting linter executor worker") + return + case request := <-e.inputChannel: + ctx := request.Context() + lintResource := e.linterResourceGetter.GetDefault(ctx) + linter := linter.Newlinter(lintResource, linter.AvailablePlugins...) + + shouldSkip, reason := linter.ShouldSkip() + if shouldSkip { + log.Printf("[linterRunner] Skipping Tracelinter. Reason %s\n", reason) + err := e.eventEmitter.Emit(ctx, events.TraceLinterSkip(request.Test.ID, request.Run.ID, reason)) + if err != nil { + log.Printf("[linterRunner] Test %s Run %d: fail to emit TracelinterSkip event: %s\n", request.Test.ID, request.Run.ID, err.Error()) + } + + e.onFinish(ctx, request, request.Run) + return + } + + err := linter.IsValid() + if err != nil { + e.onError(ctx, request, request.Run, err) + return + } + + run, err := e.onRun(ctx, request, linter, lintResource) + log.Printf("[linterRunner] Test %s Run %d: update channel start\n", request.Test.ID, request.Run.ID) + e.subscriptionManager.PublishUpdate(subscription.Message{ + ResourceID: run.TransactionStepResourceID(), + Type: "run_update", + Content: RunResult{Run: run, Err: err}, + }) + log.Printf("[linterRunner] Test %s Run %d: update channel complete\n", request.Test.ID, request.Run.ID) + + if err != nil { + log.Printf("[linterRunner] Test %s Run %d: error with runlinterRunLinterAndUpdateResult: %s\n", request.Test.ID, request.Run.ID, err.Error()) + return + } + + err = lintResource.ValidateResult(run.Linter) + if err != nil { + e.onError(ctx, request, run, err) + return + } + + e.onFinish(ctx, request, run) + } + } +} + +func (e *defaultlinterRunner) RunLinter(ctx context.Context, request LinterRequest) { + carrier := propagation.MapCarrier{} + otel.GetTextMapPropagator().Inject(ctx, carrier) + request.carrier = carrier + + e.inputChannel <- request +} + +func (e *defaultlinterRunner) onFinish(ctx context.Context, request LinterRequest, run model.Run) { + assertionRequest := AssertionRequest{ + Test: request.Test, + Run: run, + } + e.assertionRunner.RunAssertions(ctx, assertionRequest) +} + +func (e *defaultlinterRunner) onRun(ctx context.Context, request LinterRequest, linter linter.Linter, linterResource linterResource.Linter) (model.Run, error) { + run := request.Run + log.Printf("[linterRunner] Test %s Run %d: Starting\n", request.Test.ID, request.Run.ID) + + err := e.eventEmitter.Emit(ctx, events.TraceLinterStart(request.Test.ID, request.Run.ID)) + if err != nil { + log.Printf("[linterRunner] Test %s Run %d: fail to emit TracelinterStart event: %s\n", request.Test.ID, request.Run.ID, err.Error()) + } + + result, err := linter.Run(ctx, *run.Trace) + if err != nil { + return e.onError(ctx, request, run, err) + } + + run = run.SuccessfullinterExecution(result) + err = e.updater.Update(ctx, run) + if err != nil { + log.Printf("[linterRunner] Test %s Run %d: error updating run: %s\n", request.Test.ID, request.Run.ID, err.Error()) + return model.Run{}, fmt.Errorf("could not save result on database: %w", err) + } + + err = e.eventEmitter.Emit(ctx, events.TraceLinterSuccess(request.Test.ID, request.Run.ID)) + if err != nil { + log.Printf("[linterRunner] Test %s Run %d: fail to emit TracelinterSuccess event: %s\n", request.Test.ID, request.Run.ID, err.Error()) + } + + return run, nil +} + +func (e *defaultlinterRunner) onError(ctx context.Context, request LinterRequest, run model.Run, err error) (model.Run, error) { + log.Printf("[linterRunner] Test %s Run %d: Linter failed. Reason: %s\n", request.Test.ID, request.Run.ID, err.Error()) + anotherErr := e.eventEmitter.Emit(ctx, events.TraceLinterError(request.Test.ID, request.Run.ID, err)) + if anotherErr != nil { + log.Printf("[linterRunner] Test %s Run %d: fail to emit TracelinterError event: %s\n", request.Test.ID, request.Run.ID, anotherErr.Error()) + } + + analytics.SendEvent("test_run_finished", "error", "", &map[string]string{ + "finalState": string(run.State), + }) + + run = run.LinterError(err) + return run, e.updater.Update(ctx, run) +} diff --git a/server/executor/trace_poller.go b/server/executor/trace_poller.go index c2193c01ed..95785e9f31 100644 --- a/server/executor/trace_poller.go +++ b/server/executor/trace_poller.go @@ -43,7 +43,7 @@ func NewTracePoller( pe PollerExecutor, ppGetter PollingProfileGetter, updater RunUpdater, - assertionRunner AssertionRunner, + linterRunner LinterRunner, subscriptionManager *subscription.Manager, eventEmitter EventEmitter, ) PersistentTracePoller { @@ -53,7 +53,7 @@ func NewTracePoller( pollerExecutor: pe, executeQueue: make(chan PollingRequest, 5), exit: make(chan bool, 1), - assertionRunner: assertionRunner, + linterRunner: linterRunner, subscriptionManager: subscriptionManager, eventEmitter: eventEmitter, } @@ -63,7 +63,7 @@ type tracePoller struct { updater RunUpdater ppGetter PollingProfileGetter pollerExecutor PollerExecutor - assertionRunner AssertionRunner + linterRunner LinterRunner subscriptionManager *subscription.Manager eventEmitter EventEmitter @@ -191,12 +191,12 @@ func (tp tracePoller) processJob(job PollingRequest) { } func (tp tracePoller) runAssertions(job PollingRequest) { - assertionRequest := AssertionRequest{ + linterRequest := LinterRequest{ Test: job.test, Run: job.run, } - tp.assertionRunner.RunAssertions(job.ctx, assertionRequest) + tp.linterRunner.RunLinter(job.ctx, linterRequest) } func (tp tracePoller) handleTraceDBError(job PollingRequest, err error) (bool, string) { diff --git a/server/http/mappings/linter.go b/server/http/mappings/linter.go new file mode 100644 index 0000000000..7f742f8e70 --- /dev/null +++ b/server/http/mappings/linter.go @@ -0,0 +1,59 @@ +package mappings + +import ( + "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/openapi" +) + +func (m OpenAPI) LinterResult(in model.LinterResult) openapi.LinterResult { + plugins := make([]openapi.LinterResultPlugin, len(in.Plugins)) + for i, plugin := range in.Plugins { + plugins[i] = m.LinterResultPlugin(plugin) + } + + return openapi.LinterResult{ + Passed: in.Passed, + Score: int32(in.Score), + Plugins: plugins, + } +} + +func (m OpenAPI) LinterResultPlugin(in model.PluginResult) openapi.LinterResultPlugin { + rules := make([]openapi.LinterResultPluginRule, len(in.Rules)) + for i, rule := range in.Rules { + rules[i] = m.LinterResultPluginRule(rule) + } + + return openapi.LinterResultPlugin{ + Passed: in.Passed, + Score: int32(in.Score), + Description: in.Description, + Name: in.Name, + Rules: rules, + } +} + +func (m OpenAPI) LinterResultPluginRule(in model.RuleResult) openapi.LinterResultPluginRule { + results := make([]openapi.LinterResultPluginRuleResult, len(in.Results)) + for i, result := range in.Results { + results[i] = m.LinterResultPluginRuleResult(result) + } + + return openapi.LinterResultPluginRule{ + Passed: in.Passed, + Description: in.Description, + Name: in.Name, + Weight: int32(in.Weight), + Tips: in.Tips, + Results: results, + } +} + +func (m OpenAPI) LinterResultPluginRuleResult(in model.Result) openapi.LinterResultPluginRuleResult { + return openapi.LinterResultPluginRuleResult{ + SpanId: in.SpanID, + Passed: in.Passed, + Severity: "", + Errors: in.Errors, + } +} diff --git a/server/http/mappings/tests.go b/server/http/mappings/tests.go index 570517e081..bc091788c0 100644 --- a/server/http/mappings/tests.go +++ b/server/http/mappings/tests.go @@ -290,6 +290,7 @@ func (m OpenAPI) Run(in *model.Run) openapi.TestRun { Environment: m.Environment(in.Environment), TransactionId: in.TransactionID, TransactionRunId: in.TransactionRunID, + Linter: m.LinterResult(in.Linter), } } diff --git a/server/http/mappings/traces.go b/server/http/mappings/traces.go index d88c8f7803..778a17c686 100644 --- a/server/http/mappings/traces.go +++ b/server/http/mappings/traces.go @@ -44,9 +44,15 @@ func (m OpenAPI) Span(in model.Span) openapi.Span { } } + kind := string(in.Kind) + if kind == "" { + kind = string(model.SpanKindUnespecified) + } + return openapi.Span{ Id: in.ID.String(), ParentId: parentID, + Kind: kind, StartTime: in.StartTime.UnixMilli(), EndTime: in.EndTime.UnixMilli(), Attributes: attributes, diff --git a/server/linter/linter.go b/server/linter/linter.go new file mode 100644 index 0000000000..6c229f79f9 --- /dev/null +++ b/server/linter/linter.go @@ -0,0 +1,108 @@ +package linter + +import ( + "context" + "fmt" + + linter_plugin_common "github.com/kubeshop/tracetest/server/linter/plugins/common" + linter_plugin_security "github.com/kubeshop/tracetest/server/linter/plugins/security" + linter_plugin_standards "github.com/kubeshop/tracetest/server/linter/plugins/standards" + linter_resource "github.com/kubeshop/tracetest/server/linter/resource" + "github.com/kubeshop/tracetest/server/model" +) + +var ( + AvailablePlugins = []model.Plugin{ + linter_plugin_standards.NewStandardsPlugin(), + linter_plugin_security.NewSecurityPlugin(), + linter_plugin_common.NewCommonPlugin(), + } +) + +type Linter interface { + Run(context.Context, model.Trace) (model.LinterResult, error) + ShouldSkip() (bool, string) + IsValid() error +} + +type linter struct { + plugins []model.Plugin + linterResource linter_resource.Linter +} + +func Newlinter(linterResource linter_resource.Linter, plugins ...model.Plugin) linter { + return linter{plugins, linterResource} +} + +var _ Linter = &linter{} + +func (l linter) Run(ctx context.Context, trace model.Trace) (model.LinterResult, error) { + pluginResults := make([]model.PluginResult, len(l.plugins)) + + totalScore := 0 + passed := true + for i, plugin := range l.plugins { + pluginResult, err := plugin.Execute(ctx, trace) + if err != nil { + return model.LinterResult{}, err + } + + passed = passed && pluginResult.Passed + totalScore += pluginResult.Score + pluginResults[i] = pluginResult + } + + return model.LinterResult{ + Plugins: pluginResults, + Score: totalScore / len(l.plugins), + Passed: passed, + }, nil +} + +func (l linter) ShouldSkip() (bool, string) { + if !l.linterResource.Enabled { + return true, "linter is disabled" + } + + return false, "" +} + +func (l linter) IsValid() error { + plugins, err := l.getPlugins() + if err != nil { + return err + } + + if len(plugins) == 0 { + return fmt.Errorf("No plugins found") + } + + return nil +} + +func (l linter) getPlugins() ([]model.Plugin, error) { + plugins := []model.Plugin{} + + for _, cfgPlugin := range l.linterResource.Plugins { + if cfgPlugin.Enabled { + plugin, err := l.findPlugin(cfgPlugin.Name) + if err != nil { + return nil, err + } + + plugins = append(plugins, *plugin) + } + } + + return plugins, nil +} + +func (l linter) findPlugin(pluginName string) (*model.Plugin, error) { + for _, plugin := range AvailablePlugins { + if plugin.Name() == pluginName { + return &plugin, nil + } + } + + return nil, fmt.Errorf("plugin %s is not configured", pluginName) +} diff --git a/server/linter/plugins/common/plugin.go b/server/linter/plugins/common/plugin.go new file mode 100644 index 0000000000..d63a51ea98 --- /dev/null +++ b/server/linter/plugins/common/plugin.go @@ -0,0 +1,49 @@ +package linter_plugin_common + +import ( + "context" + + "github.com/kubeshop/tracetest/server/linter/plugins/common/rules" + "github.com/kubeshop/tracetest/server/model" +) + +type CommonPlugin struct { + model.BasePlugin +} + +var ( + _ model.Plugin = &CommonPlugin{} +) + +func NewCommonPlugin() model.Plugin { + return CommonPlugin{ + BasePlugin: model.BasePlugin{ + Name: "Common Issues", + Description: "Helps you find common problems with your application", + Rules: []model.Rule{ + rules.NewEnforceDnsUsageRule(), + }, + }, + } +} + +func (p CommonPlugin) Name() string { + return "common" +} + +func (p CommonPlugin) Execute(ctx context.Context, trace model.Trace) (model.PluginResult, error) { + results := make([]model.RuleResult, len(p.Rules)) + for i, rule := range p.Rules { + result, err := rule.Evaluate(ctx, trace) + if err != nil { + return model.PluginResult{}, err + } + + results[i] = result + } + + return model.PluginResult{ + BasePlugin: p.BasePlugin, + Rules: results, + }.CalculateResults(), nil +} diff --git a/server/linter/plugins/common/rules/enforce_dns.go b/server/linter/plugins/common/rules/enforce_dns.go new file mode 100644 index 0000000000..ba08f43abd --- /dev/null +++ b/server/linter/plugins/common/rules/enforce_dns.go @@ -0,0 +1,63 @@ +package rules + +import ( + "context" + "fmt" + "regexp" + + "github.com/kubeshop/tracetest/server/model" +) + +type ensuresDnsUsage struct { + model.BaseRule +} + +var ( + dnsFields = []string{"net.peer.name", "http.url", "db.connection_string", "net.sock.peer.addr", "net.host.name"} +) + +func NewEnforceDnsUsageRule() model.Rule { + return &ensuresDnsUsage{ + BaseRule: model.BaseRule{ + Name: "DNS Over IP", + Description: "Enforce DNS usage over IP addresses", + Tips: []string{}, + Weight: 100, + }, + } +} + +func (r ensuresDnsUsage) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + passed := true + results := make([]model.Result, 0) + for _, span := range trace.Flat { + result := r.validate(span) + if !result.Passed { + passed = false + } + results = append(results, result) + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: passed, + Results: results, + }, nil +} + +func (r ensuresDnsUsage) validate(span *model.Span) model.Result { + ipFields := make([]string, 0) + ipRegexp := regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`) + + for _, field := range dnsFields { + if span.Attributes.Get(field) != "" && ipRegexp.MatchString(span.Attributes.Get(field)) { + ipFields = append(ipFields, fmt.Sprintf("Usage of a IP endpoint instead of DNS found for attribute: %s. Value: %s", field, span.Attributes.Get(field))) + } + } + + return model.Result{ + Passed: len(ipFields) == 0, + SpanID: span.ID.String(), + Errors: ipFields, + } +} diff --git a/server/linter/plugins/security/plugin.go b/server/linter/plugins/security/plugin.go new file mode 100644 index 0000000000..effa3c501f --- /dev/null +++ b/server/linter/plugins/security/plugin.go @@ -0,0 +1,50 @@ +package linter_plugin_security + +import ( + "context" + + "github.com/kubeshop/tracetest/server/linter/plugins/security/rules" + "github.com/kubeshop/tracetest/server/model" +) + +type SecurityPlugin struct { + model.BasePlugin +} + +var ( + _ model.Plugin = &SecurityPlugin{} +) + +func NewSecurityPlugin() model.Plugin { + return SecurityPlugin{ + BasePlugin: model.BasePlugin{ + Name: "Security", + Description: "Enforce security for spans and attributes", + Rules: []model.Rule{ + rules.NewEnforceHttpsProtocolRule(), + rules.NewEnsuresNoApiKeyLeakRule(), + }, + }, + } +} + +func (p SecurityPlugin) Name() string { + return "security" +} + +func (p SecurityPlugin) Execute(ctx context.Context, trace model.Trace) (model.PluginResult, error) { + results := make([]model.RuleResult, len(p.Rules)) + for i, rule := range p.Rules { + result, err := rule.Evaluate(ctx, trace) + if err != nil { + return model.PluginResult{}, err + } + + results[i] = result + } + + return model.PluginResult{ + BasePlugin: p.BasePlugin, + Rules: results, + }.CalculateResults(), nil +} diff --git a/server/linter/plugins/security/rules/enforce_secure_protocol.go b/server/linter/plugins/security/rules/enforce_secure_protocol.go new file mode 100644 index 0000000000..f03a97a043 --- /dev/null +++ b/server/linter/plugins/security/rules/enforce_secure_protocol.go @@ -0,0 +1,63 @@ +package rules + +import ( + "context" + "fmt" + "strings" + + "github.com/kubeshop/tracetest/server/model" +) + +type enforceHttpsProtocolRule struct { + model.BaseRule +} + +var ( + httpFields = []string{"http.scheme", "http.url"} +) + +func NewEnforceHttpsProtocolRule() model.Rule { + return &enforceHttpsProtocolRule{ + BaseRule: model.BaseRule{ + Name: "Enforce HTTPS Protocol", + Description: "Ensure all request use https", + Tips: []string{}, + Weight: 40, + }, + } +} + +func (r enforceHttpsProtocolRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + passed := true + results := make([]model.Result, 0) + for _, span := range trace.Flat { + if span.Attributes.Get("tracetest.span.type") == "http" { + result := r.validate(span) + if !result.Passed { + passed = false + } + results = append(results, result) + } + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: passed, + Results: results, + }, nil +} + +func (r enforceHttpsProtocolRule) validate(span *model.Span) model.Result { + insecureFields := make([]string, 0) + for _, field := range httpFields { + if span.Attributes.Get(field) != "" && !strings.Contains(span.Attributes.Get(field), "https") { + insecureFields = append(insecureFields, fmt.Sprintf("Insecure http schema found for attribute: %s. Value: %s", field, span.Attributes.Get(field))) + } + } + + return model.Result{ + Passed: len(insecureFields) == 0, + SpanID: span.ID.String(), + Errors: insecureFields, + } +} diff --git a/server/linter/plugins/security/rules/ensures_no_api_key_leak.go b/server/linter/plugins/security/rules/ensures_no_api_key_leak.go new file mode 100644 index 0000000000..af4db012ef --- /dev/null +++ b/server/linter/plugins/security/rules/ensures_no_api_key_leak.go @@ -0,0 +1,70 @@ +package rules + +import ( + "context" + "fmt" + + "github.com/kubeshop/tracetest/server/model" +) + +type ensuresNoApiKeyLeakRule struct { + model.BaseRule +} + +var ( + httpHeadersFields = []string{"authorization", "x-api-key"} + httpResponseHeader = "http.request.header." + httpRequestHeader = "http.response.header." +) + +func NewEnsuresNoApiKeyLeakRule() model.Rule { + return &ensuresNoApiKeyLeakRule{ + BaseRule: model.BaseRule{ + Name: "No API Key Leak", + Description: "Ensure no API keys are leaked in http headers", + Tips: []string{}, + Weight: 60, + }, + } +} + +func (r ensuresNoApiKeyLeakRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + passed := true + results := make([]model.Result, 0) + for _, span := range trace.Flat { + if span.Attributes.Get("tracetest.span.type") == "http" { + result := r.validate(span) + if !result.Passed { + passed = false + } + results = append(results, result) + } + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: passed, + Results: results, + }, nil +} + +func (r ensuresNoApiKeyLeakRule) validate(span *model.Span) model.Result { + leakedFields := make([]string, 0) + for _, field := range httpHeadersFields { + requestHeader := fmt.Sprintf("%s%s", httpRequestHeader, field) + if span.Attributes.Get(requestHeader) != "" { + leakedFields = append(leakedFields, fmt.Sprintf("Leaked Request API Key found for attribute: %s. Value: %s", field, span.Attributes.Get(requestHeader))) + } + + responseHeader := fmt.Sprintf("%s%s", httpResponseHeader, field) + if span.Attributes.Get(responseHeader) != "" { + leakedFields = append(leakedFields, fmt.Sprintf("Leaked Response API Key found for attribute: %s. Value: %s", field, span.Attributes.Get(responseHeader))) + } + } + + return model.Result{ + Passed: len(leakedFields) == 0, + SpanID: span.ID.String(), + Errors: leakedFields, + } +} diff --git a/server/linter/plugins/standards/plugin.go b/server/linter/plugins/standards/plugin.go new file mode 100644 index 0000000000..4887df7ee7 --- /dev/null +++ b/server/linter/plugins/standards/plugin.go @@ -0,0 +1,52 @@ +package linter_plugin_standards + +import ( + "context" + + "github.com/kubeshop/tracetest/server/linter/plugins/standards/rules" + "github.com/kubeshop/tracetest/server/model" +) + +type StandardsPlugin struct { + model.BasePlugin +} + +var ( + _ model.Plugin = &StandardsPlugin{} +) + +func NewStandardsPlugin() model.Plugin { + return StandardsPlugin{ + BasePlugin: model.BasePlugin{ + Name: "Global Standards", + Description: "Enforce standards for spans and attributes", + Rules: []model.Rule{ + rules.NewEnsureSpanNamingRule(), + rules.NewRequiredAttributesRule(), + rules.NewEnsureAttributeNamingRule(), + rules.NewNotEmptyAttributesRule(), + }, + }, + } +} + +func (p StandardsPlugin) Name() string { + return "standards" +} + +func (p StandardsPlugin) Execute(ctx context.Context, trace model.Trace) (model.PluginResult, error) { + results := make([]model.RuleResult, len(p.Rules)) + for i, rule := range p.Rules { + result, err := rule.Evaluate(ctx, trace) + if err != nil { + return model.PluginResult{}, err + } + + results[i] = result + } + + return model.PluginResult{ + BasePlugin: p.BasePlugin, + Rules: results, + }.CalculateResults(), nil +} diff --git a/server/linter/plugins/standards/rules/ensure_attribute_naming_rule.go b/server/linter/plugins/standards/rules/ensure_attribute_naming_rule.go new file mode 100644 index 0000000000..f2eba7a827 --- /dev/null +++ b/server/linter/plugins/standards/rules/ensure_attribute_naming_rule.go @@ -0,0 +1,71 @@ +package rules + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/kubeshop/tracetest/server/model" +) + +type ensureAttributeNamingRule struct { + model.BaseRule +} + +func NewEnsureAttributeNamingRule() model.Rule { + return &ensureAttributeNamingRule{ + BaseRule: model.BaseRule{ + Name: "Attribute Naming", + Description: "Ensure all attributes follow the naming convention", + Tips: []string{ + "You should always add namespaces to your span names to ensure they will not be overwritten", + "Use snake_case to separate multi-words. Ex: http.status_code instead of http.statusCode"}, + Weight: 25, + }, + } +} + +func (r ensureAttributeNamingRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + regex := regexp.MustCompile(`^([a-z0-9_]+\.)+[a-z0-9_]+$`) + results := make([]model.Result, 0) + passed := true + + for _, span := range trace.Flat { + errors := make([]string, 0) + namespaces := make([]string, 0) + for name := range span.Attributes { + if !regex.MatchString(name) { + errors = append(errors, fmt.Sprintf(`Attribute "%s" doesnt follow naming convention`, name)) + continue + } + + namespaces = append(namespaces, name[0:strings.LastIndex(name, ".")]) + } + + // ensure no attribute is named after a namespace + for name := range span.Attributes { + for _, namespace := range namespaces { + if name == namespace { + errors = append(errors, fmt.Sprintf(`Attribute "%s" uses the same name as an existing namespace in the same span`, name)) + } + } + } + + if len(errors) > 0 { + passed = false + } + + results = append(results, model.Result{ + SpanID: span.ID.String(), + Passed: len(errors) == 0, + Errors: errors, + }) + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: passed, + Results: results, + }, nil +} diff --git a/server/linter/plugins/standards/rules/ensure_attribute_naming_rule_test.go b/server/linter/plugins/standards/rules/ensure_attribute_naming_rule_test.go new file mode 100644 index 0000000000..fbe3be60c9 --- /dev/null +++ b/server/linter/plugins/standards/rules/ensure_attribute_naming_rule_test.go @@ -0,0 +1,64 @@ +package rules_test + +import ( + "context" + "testing" + + "github.com/kubeshop/tracetest/server/linter/plugins/standards/rules" + "github.com/stretchr/testify/assert" +) + +func TestEnsureAttributeNamingRule(t *testing.T) { + t.Run("compliant trace", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.method": "POST", "http.url": "http://localhost:11633"}), + spanWithAttributes("http", map[string]string{"http.method": "GET", "http.url": "http://localhost:11633"}), + spanWithAttributes("messaging", map[string]string{"messaging.target": "user.sync", "messaging.system1": "kafka"}), + spanWithAttributes("database", map[string]string{"db.statement": "INSERT INTO users (name, email) VALUES ($1, $2)"}), + ) + + rule := rules.NewEnsureAttributeNamingRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.True(t, result.Passed) + }) + + t.Run("no namespace", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"method": "POST"}), + ) + + rule := rules.NewEnsureAttributeNamingRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + assert.Len(t, result.Results, 1) + assert.Equal(t, `Attribute "method" doesnt follow naming convention`, result.Results[0].Errors[0]) + }) + + t.Run("span name with camel case", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.statusCode": "200"}), + ) + + rule := rules.NewEnsureAttributeNamingRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + assert.Len(t, result.Results, 1) + assert.Equal(t, `Attribute "http.statusCode" doesnt follow naming convention`, result.Results[0].Errors[0]) + }) + + t.Run("attribute named after namespace", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"tracetest.span": "POST"}), + ) + + rule := rules.NewEnsureAttributeNamingRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + assert.Len(t, result.Results, 1) + assert.Equal(t, `Attribute "tracetest.span" uses the same name as an existing namespace in the same span`, result.Results[0].Errors[0]) + }) +} diff --git a/server/linter/plugins/standards/rules/ensure_span_naming_rule.go b/server/linter/plugins/standards/rules/ensure_span_naming_rule.go new file mode 100644 index 0000000000..48647dc505 --- /dev/null +++ b/server/linter/plugins/standards/rules/ensure_span_naming_rule.go @@ -0,0 +1,144 @@ +package rules + +import ( + "context" + "fmt" + + "github.com/kubeshop/tracetest/server/model" +) + +type ensureSpanNamingRule struct { + model.BaseRule +} + +func NewEnsureSpanNamingRule() model.Rule { + return &ensureSpanNamingRule{ + BaseRule: model.BaseRule{ + Name: "Span Name Convention", + Description: "Ensure all span follow the name convention", + Tips: []string{}, + Weight: 25, + }, + } +} + +func (r ensureSpanNamingRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + results := make([]model.Result, 0) + for _, span := range trace.Flat { + results = append(results, r.validateSpanName(ctx, span)) + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Results: results, + }, nil +} + +func (r ensureSpanNamingRule) validateSpanName(ctx context.Context, span *model.Span) model.Result { + switch span.Attributes.Get("tracetest.span.type") { + case "http": + return r.validateHTTPSpanName(ctx, span) + case "database": + return r.validateDatabaseSpanName(ctx, span) + case "rpc": + return r.validateRPCSpanName(ctx, span) + case "messaging": + return r.validateMessagingSpanName(ctx, span) + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} + +func (r ensureSpanNamingRule) validateHTTPSpanName(ctx context.Context, span *model.Span) model.Result { + expectedName := "" + if span.Kind == model.SpanKindServer { + expectedName = fmt.Sprintf("%s %s", span.Attributes.Get("http.method"), span.Attributes.Get("http.route")) + } + + if span.Kind == model.SpanKindClient { + expectedName = span.Attributes.Get("http.method") + } + + if span.Name != expectedName { + return model.Result{ + SpanID: span.ID.String(), + Passed: false, + Errors: []string{fmt.Sprintf(`Span name is not matching the naming convention. Expected: %s`, expectedName)}, + } + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} + +func (r ensureSpanNamingRule) validateDatabaseSpanName(ctx context.Context, span *model.Span) model.Result { + var expectedName string + dbOperation := span.Attributes.Get("db.operation") + dbName := span.Attributes.Get("db.operation") + tableName := span.Attributes.Get("db.sql.table") + + if tableName != "" { + expectedName = fmt.Sprintf("%s %s %s", dbOperation, dbName, tableName) + } else { + expectedName = fmt.Sprintf("%s %s", dbOperation, dbName) + } + + if span.Name != expectedName { + return model.Result{ + SpanID: span.ID.String(), + Passed: false, + Errors: []string{fmt.Sprintf(`Span name is not matching the naming convention. Expected: %s`, expectedName)}, + } + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} + +func (r ensureSpanNamingRule) validateRPCSpanName(ctx context.Context, span *model.Span) model.Result { + rpcPackage := span.Attributes.Get("rpc.package") + rpcService := span.Attributes.Get("rpc.service") + rpcMethod := span.Attributes.Get("rpc.method") + + expectedName := fmt.Sprintf("%s.%s/%s", rpcPackage, rpcService, rpcMethod) + + if span.Name != expectedName { + return model.Result{ + SpanID: span.ID.String(), + Passed: false, + Errors: []string{fmt.Sprintf(`Span name is not matching the naming convention. Expected: %s`, expectedName)}, + } + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} + +func (r ensureSpanNamingRule) validateMessagingSpanName(ctx context.Context, span *model.Span) model.Result { + destination := span.Attributes.Get("messaging.destination") + operation := span.Attributes.Get("messaging.operation") + + expectedName := fmt.Sprintf("%s %s", destination, operation) + + if span.Name != expectedName { + return model.Result{ + SpanID: span.ID.String(), + Passed: false, + Errors: []string{fmt.Sprintf(`Span name is not matching the naming convention. Expected: %s`, expectedName)}, + } + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} diff --git a/server/linter/plugins/standards/rules/not_empty_attribute_rule.go b/server/linter/plugins/standards/rules/not_empty_attribute_rule.go new file mode 100644 index 0000000000..c1c828f07a --- /dev/null +++ b/server/linter/plugins/standards/rules/not_empty_attribute_rule.go @@ -0,0 +1,57 @@ +package rules + +import ( + "context" + "fmt" + + "github.com/kubeshop/tracetest/server/model" +) + +type notEmptyRuleAttributesRule struct { + model.BaseRule +} + +func NewNotEmptyAttributesRule() model.Rule { + return ¬EmptyRuleAttributesRule{ + BaseRule: model.BaseRule{ + Name: "Not Empty Attributes", + Description: "Does not allow empty attribute values in any span", + Tips: []string{"Empty attributes don't provide any information about the operation and should be removed"}, + Weight: 25, + }, + } +} + +func (r notEmptyRuleAttributesRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + results := make([]model.Result, 0, len(trace.Flat)) + passed := true + for _, span := range trace.Flat { + emptyAttributes := make([]string, 0) + for name, value := range span.Attributes { + if value == "" { + emptyAttributes = append(emptyAttributes, name) + } + } + + errors := make([]string, 0, len(emptyAttributes)) + for _, emptyAttribute := range emptyAttributes { + errors = append(errors, fmt.Sprintf(`Attribute "%s" is empty`, emptyAttribute)) + } + + if len(errors) > 0 { + passed = false + } + + results = append(results, model.Result{ + SpanID: span.ID.String(), + Passed: len(emptyAttributes) == 0, + Errors: errors, + }) + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: passed, + Results: results, + }, nil +} diff --git a/server/linter/plugins/standards/rules/not_empty_attribute_rule_test.go b/server/linter/plugins/standards/rules/not_empty_attribute_rule_test.go new file mode 100644 index 0000000000..4c88ee4c14 --- /dev/null +++ b/server/linter/plugins/standards/rules/not_empty_attribute_rule_test.go @@ -0,0 +1,38 @@ +package rules_test + +import ( + "context" + "testing" + + "github.com/kubeshop/tracetest/server/linter/plugins/standards/rules" + "github.com/stretchr/testify/assert" +) + +func TestNotEmptyAttributeRule(t *testing.T) { + t.Run("no empty attributes", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.method": "POST", "http.url": "http://localhost:11633"}), + spanWithAttributes("http", map[string]string{"http.method": "GET", "http.url": "http://localhost:11633"}), + spanWithAttributes("messaging", map[string]string{"messaging.target1": "user.sync", "messaging.system1": "kafka"}), + spanWithAttributes("database", map[string]string{"db.statement": "INSERT INTO users (name, email) VALUES ($1, $2)"}), + ) + + rule := rules.NewNotEmptyAttributesRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.True(t, result.Passed) + }) + + t.Run("empty attribute", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.method": ""}), + ) + + rule := rules.NewNotEmptyAttributesRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + assert.Len(t, result.Results, 1) + assert.Equal(t, `Attribute "http.method" is empty`, result.Results[0].Errors[0]) + }) +} diff --git a/server/linter/plugins/standards/rules/required_attribute_rule.go b/server/linter/plugins/standards/rules/required_attribute_rule.go new file mode 100644 index 0000000000..b020cceb89 --- /dev/null +++ b/server/linter/plugins/standards/rules/required_attribute_rule.go @@ -0,0 +1,42 @@ +package rules + +import ( + "context" + + "github.com/kubeshop/tracetest/server/model" +) + +type requiredAttributesRule struct { + model.BaseRule +} + +func NewRequiredAttributesRule() model.Rule { + return requiredAttributesRule{ + BaseRule: model.BaseRule{ + Name: "Required Attributes by Span Type", + Description: "Ensure all spans have required attributes", + Tips: []string{"This rule checks if all required attributes are present in spans of given type"}, + Weight: 25, + }, + } +} + +func (r requiredAttributesRule) Evaluate(ctx context.Context, trace model.Trace) (model.RuleResult, error) { + results := make([]model.Result, 0) + for _, span := range trace.Flat { + results = append(results, r.validateSpan(span)) + } + + var allPassed bool = true + for _, result := range results { + if !result.Passed { + allPassed = false + } + } + + return model.RuleResult{ + BaseRule: r.BaseRule, + Passed: allPassed, + Results: results, + }, nil +} diff --git a/server/linter/plugins/standards/rules/required_attribute_rule_test.go b/server/linter/plugins/standards/rules/required_attribute_rule_test.go new file mode 100644 index 0000000000..6a7a382d85 --- /dev/null +++ b/server/linter/plugins/standards/rules/required_attribute_rule_test.go @@ -0,0 +1,81 @@ +package rules_test + +import ( + "context" + "testing" + + "github.com/kubeshop/tracetest/server/linter/plugins/standards/rules" + "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/trace" +) + +func TestRequiredAttributesRule(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.method": "POST", "http.url": "http://localhost:11633"}), + spanWithAttributes("http", map[string]string{"http.method": "GET", "http.url": "http://localhost:11633"}), + spanWithAttributes("messaging", map[string]string{"messaging.target1": "user.sync", "messaging.system1": "kafka"}), + spanWithAttributes("database", map[string]string{"db.statement": "INSERT INTO users (name, email) VALUES ($1, $2)"}), + ) + + t.Run("When all required attributes are found", func(t *testing.T) { + trace := traceWithSpans( + spanWithAttributes("http", map[string]string{"http.method": "POST", "http.url": "http://localhost:11633"}), + spanWithAttributes("http", map[string]string{"http.method": "GET", "http.url": "http://localhost:11633"}), + spanWithAttributes("messaging", map[string]string{"messaging.operation": "user.sync", "messaging.system": "kafka"}), + spanWithAttributes("database", map[string]string{"db.system": "postgres"}), + ) + + rule := rules.NewRequiredAttributesRule() + result, _ := rule.Evaluate(context.Background(), trace) + + for _, result := range result.Results { + assert.True(t, result.Passed) + } + + assert.True(t, result.Passed) + }) + + t.Run("When some attribute is missing", func(t *testing.T) { + rule := rules.NewRequiredAttributesRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + }) + + t.Run("When all attributes are missing", func(t *testing.T) { + rule := rules.NewRequiredAttributesRule() + result, _ := rule.Evaluate(context.Background(), trace) + + assert.False(t, result.Passed) + }) +} + +func traceWithSpans(spans ...model.Span) model.Trace { + trace := model.Trace{ + Flat: make(map[trace.SpanID]*model.Span, 0), + } + + for _, span := range spans { + realSpan := span + span.ID = id.NewRandGenerator().SpanID() + trace.Flat[span.ID] = &realSpan + } + + return trace +} + +func spanWithAttributes(spanType string, attributes map[string]string) model.Span { + span := model.Span{ + Attributes: make(model.Attributes, 0), + } + + for name, value := range attributes { + span.Attributes[name] = value + } + + span.Attributes["tracetest.span.type"] = spanType + + return span +} diff --git a/server/linter/plugins/standards/rules/required_attributes.go b/server/linter/plugins/standards/rules/required_attributes.go new file mode 100644 index 0000000000..a19ceafa86 --- /dev/null +++ b/server/linter/plugins/standards/rules/required_attributes.go @@ -0,0 +1,137 @@ +package rules + +import ( + "fmt" + + "github.com/kubeshop/tracetest/server/model" +) + +var ( + httpAttr = []string{"http.method"} + httpAttrClient = []string{"http.url", "net.peer.name"} + httpAttrServer = []string{"http.target", "http.scheme", "net.host.name"} + + databaseAttr = []string{"db.system"} + rpcAttr = []string{"rpc.system", "neet.peer.name"} + messagingAttr = []string{"messaging.system", "messaging.operation"} + faasAttrServer = []string{"faas.trigger"} + faasAttrClient = []string{"faas.invoked_name", "faas.invoked_provider"} +) + +func (r requiredAttributesRule) validateSpan(span *model.Span) model.Result { + switch span.Attributes.Get("tracetest.span.type") { + case "http": + return r.validateHttpSpan(span) + case "database": + return r.validateDatabaseSpan(span) + case "rpc": + return r.validateRPCSpan(span) + case "messaging": + return r.validateMessagingSpan(span) + case "faas": + return r.validateFaasSpan(span) + } + + return model.Result{ + Passed: true, + SpanID: span.ID.String(), + } +} + +func (r requiredAttributesRule) validateHttpSpan(span *model.Span) model.Result { + missingAttrs := r.getMissingAttrs(span, httpAttr, "http") + result := model.Result{ + Passed: true, + SpanID: span.ID.String(), + } + + if span.Kind == model.SpanKindClient { + missingAttrs = append(missingAttrs, r.getMissingAttrs(span, httpAttrClient, "http")...) + } else if span.Kind == model.SpanKindServer { + missingAttrs = append(missingAttrs, r.getMissingAttrs(span, httpAttrServer, "http")...) + } + + if len(missingAttrs) > 0 { + result.Passed = false + result.Errors = missingAttrs + } + + return result +} + +func (r requiredAttributesRule) getMissingAttrs(span *model.Span, matchingAttrList []string, spanType string) []string { + missingAttributes := make([]string, 0) + for _, requiredAttribute := range matchingAttrList { + if _, attributeExists := span.Attributes[requiredAttribute]; !attributeExists { + missingAttributes = append(missingAttributes, fmt.Sprintf(`Attribute "%s" is missing from span of type "%s"`, requiredAttribute, spanType)) + } + } + + return missingAttributes +} + +func (r requiredAttributesRule) validateDatabaseSpan(span *model.Span) model.Result { + missingAttrs := r.getMissingAttrs(span, databaseAttr, "database") + result := model.Result{ + Passed: true, + SpanID: span.ID.String(), + } + + if len(missingAttrs) > 0 { + result.Passed = false + result.Errors = missingAttrs + } + + return result +} + +func (r requiredAttributesRule) validateRPCSpan(span *model.Span) model.Result { + missingAttrs := r.getMissingAttrs(span, rpcAttr, "rpc") + result := model.Result{ + Passed: true, + SpanID: span.ID.String(), + } + + if len(missingAttrs) > 0 { + result.Passed = false + result.Errors = missingAttrs + } + + return result +} + +func (r requiredAttributesRule) validateMessagingSpan(span *model.Span) model.Result { + missingAttrs := r.getMissingAttrs(span, messagingAttr, "messaging") + result := model.Result{ + Passed: true, + SpanID: span.ID.String(), + } + + if len(missingAttrs) > 0 { + result.Passed = false + result.Errors = missingAttrs + } + + return result +} + +func (r requiredAttributesRule) validateFaasSpan(span *model.Span) model.Result { + missingAttrs := []string{} + result := model.Result{ + Passed: true, + SpanID: span.ID.String(), + } + + if span.Kind == model.SpanKindClient { + missingAttrs = r.getMissingAttrs(span, faasAttrClient, "faas") + } else if span.Kind == model.SpanKindServer { + missingAttrs = r.getMissingAttrs(span, faasAttrServer, "faas") + } + + if len(missingAttrs) > 0 { + result.Passed = false + result.Errors = missingAttrs + } + + return result +} diff --git a/server/linter/resource/main_test.go b/server/linter/resource/main_test.go new file mode 100644 index 0000000000..680ae4bf53 --- /dev/null +++ b/server/linter/resource/main_test.go @@ -0,0 +1,18 @@ +package linter_resource_test + +import ( + "os" + "testing" + + "github.com/kubeshop/tracetest/server/testmock" +) + +func TestMain(m *testing.M) { + testmock.StartTestEnvironment() + + exitVal := m.Run() + + testmock.StopTestEnvironment() + + os.Exit(exitVal) +} diff --git a/server/linter/resource/manager.go b/server/linter/resource/manager.go new file mode 100644 index 0000000000..03c35a2bf1 --- /dev/null +++ b/server/linter/resource/manager.go @@ -0,0 +1,195 @@ +package linter_resource + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + + "github.com/kubeshop/tracetest/server/pkg/id" +) + +type Repository struct { + db *sql.DB +} + +var defaultlinter = Linter{ + ID: id.ID("current"), + Name: "linter", + Enabled: true, + MinimumScore: 0, + Plugins: []LinterPlugin{ + {Name: "standards", Enabled: true, Required: true}, + {Name: "security", Enabled: true, Required: true}, + {Name: "common", Enabled: true, Required: true}, + }, +} + +func NewRepository(db *sql.DB) *Repository { + return &Repository{db} +} + +const ( + insertQuery = ` + INSERT INTO linters ( + "id", + "name", + "enabled", + "minimum_score", + "plugins" + ) VALUES ($1, $2, $3, $4, $5)` + + getQuery = ` + SELECT + l.id, + l.name, + l.enabled, + l.minimum_score, + l.plugins + FROM linters l +` + + deleteQuery = "DELETE FROM linters WHERE id = $1" +) + +func (r *Repository) GetDefault(ctx context.Context) Linter { + pp, _ := r.Get(ctx, id.ID("current")) + return pp +} + +func (r *Repository) SetID(linters Linter, id id.ID) Linter { + linters.ID = id + return linters +} + +func (r *Repository) Update(ctx context.Context, linter Linter) (Linter, error) { + // enforce ID and name + updated := Linter{ + ID: id.ID("current"), + Name: linter.Name, + Enabled: linter.Enabled, + MinimumScore: linter.MinimumScore, + Plugins: linter.Plugins, + } + + tx, err := r.db.BeginTx(ctx, nil) + defer tx.Rollback() + if err != nil { + return Linter{}, err + } + + _, err = tx.ExecContext(ctx, deleteQuery, updated.ID) + if err != nil { + return Linter{}, fmt.Errorf("sql exec delete: %w", err) + } + + var pluginsJSON []byte + if updated.Plugins != nil { + pluginsJSON, err = json.Marshal(updated.Plugins) + if err != nil { + return Linter{}, fmt.Errorf("could not marshal plugins configuration: %w", err) + } + } + + _, err = tx.ExecContext( + ctx, + insertQuery, + updated.ID, + updated.Name, + updated.Enabled, + updated.MinimumScore, + pluginsJSON, + ) + if err != nil { + return Linter{}, fmt.Errorf("sql exec insert: %w", err) + } + + err = tx.Commit() + if err != nil { + return Linter{}, fmt.Errorf("commit: %w", err) + } + + return updated, nil +} + +func (r *Repository) Delete(ctx context.Context, id id.ID) error { + _, err := r.Get(ctx, id) + if err != nil { + return err + } + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("sql BeginTx: %w", err) + } + defer tx.Rollback() + + _, err = tx.ExecContext(ctx, deleteQuery, id) + if err != nil { + return fmt.Errorf("sql error: %w", err) + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("sql Commit: %w", err) + } + + return nil +} + +func (r *Repository) List(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Linter, error) { + linter, err := r.Get(ctx, id.ID("current")) + if err != nil { + return []Linter{}, err + } + + return []Linter{linter}, nil +} + +func (r *Repository) Count(ctx context.Context, query string) (int, error) { + return 1, nil +} + +func (*Repository) SortingFields() []string { + return []string{"name"} +} + +func (r *Repository) Get(ctx context.Context, id id.ID) (Linter, error) { + linter := defaultlinter + + var rawPlugins []byte + err := r.db. + QueryRowContext(ctx, getQuery). + Scan( + &linter.ID, + &linter.Name, + &linter.Enabled, + &linter.MinimumScore, + &rawPlugins, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return linter, nil + } + return Linter{}, fmt.Errorf("sql query: %w", err) + } + + if string(rawPlugins) != "null" { + var plugins []LinterPlugin + err = json.Unmarshal(rawPlugins, &plugins) + if err != nil { + return Linter{}, fmt.Errorf("could not unmarshal plugins: %w", err) + } + + linter.Plugins = plugins + } + + return linter, nil +} + +func (r *Repository) Provision(ctx context.Context, linter Linter) error { + _, err := r.Update(ctx, linter) + return err +} diff --git a/server/linter/resource/manager_test.go b/server/linter/resource/manager_test.go new file mode 100644 index 0000000000..a88b582c47 --- /dev/null +++ b/server/linter/resource/manager_test.go @@ -0,0 +1,90 @@ +package linter_resource_test + +import ( + "database/sql" + "testing" + + "github.com/gorilla/mux" + linter_resource "github.com/kubeshop/tracetest/server/linter/resource" + "github.com/kubeshop/tracetest/server/resourcemanager" + rmtests "github.com/kubeshop/tracetest/server/resourcemanager/testutil" +) + +func TestlinterResource(t *testing.T) { + rmtests.TestResourceType(t, rmtests.ResourceTypeTest{ + ResourceTypeSingular: linter_resource.ResourceName, + ResourceTypePlural: linter_resource.ResourceNamePlural, + RegisterManagerFn: func(router *mux.Router, db *sql.DB) resourcemanager.Manager { + repo := linter_resource.NewRepository(db) + + manager := resourcemanager.New[linter_resource.Linter]( + linter_resource.ResourceName, + linter_resource.ResourceNamePlural, + repo, + resourcemanager.WithOperations(linter_resource.Operations...), + ) + manager.RegisterRoutes(router) + + return manager + }, + SampleJSON: `{ + "type": "linter", + "spec": { + "id": "current", + "name": "linter", + "enabled": true, + "minimumScore": 80, + "plugins": [ + { + "name": "standards", + "enabled": true, + "required": true + }, + { + "name": "security", + "enabled": true, + "required": true + }, + { + "name": "common", + "enabled": true, + "required": true + } + ] + } + }`, + SampleJSONUpdated: `{ + "type": "linter", + "spec": { + "id": "current", + "name": "linter", + "enabled": true, + "minimumScore": 50, + "plugins": [ + { + "name": "standards", + "enabled": false, + "required": false + }, + { + "name": "security", + "enabled": true, + "required": true + }, + { + "name": "common", + "enabled": true, + "required": true + } + ] + } + }`, + }, + rmtests.ExcludeOperations( + rmtests.OperationGetNotFound, + rmtests.OperationUpdateNotFound, + rmtests.OperationListSortSuccess, + rmtests.OperationListNoResults, + ), + ) +} diff --git a/server/linter/resource/model.go b/server/linter/resource/model.go new file mode 100644 index 0000000000..1b3bdb41f8 --- /dev/null +++ b/server/linter/resource/model.go @@ -0,0 +1,80 @@ +package linter_resource + +import ( + "fmt" + + "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/pkg/id" + "golang.org/x/exp/slices" +) + +type ( + Linter struct { + ID id.ID `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + MinimumScore int `json:"minimumScore"` + Plugins []LinterPlugin `json:"plugins"` + } + + LinterPlugin struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Required bool `json:"required"` + } +) + +func (l Linter) Validate() error { + if l.Name == "" { + return fmt.Errorf("linter name cannot be empty") + } + + for _, p := range l.Plugins { + if p.Name == "" { + return fmt.Errorf("plugin name cannot be empty") + } + } + + return nil +} + +func (l Linter) HasID() bool { + return l.ID != "" +} + +func (l Linter) GetID() id.ID { + return l.ID +} + +func (l Linter) ValidateResult(result model.LinterResult) error { + if l.MinimumScore != 0 && result.Score < l.MinimumScore { + return fmt.Errorf("linter score validation failed. Minimum %d, Actual: %d", l.MinimumScore, result.Score) + } + + failedPlugins := make([]string, 0) + for _, plugin := range result.Plugins { + if !plugin.Passed { + failedPlugins = append(failedPlugins, plugin.Name) + } + } + + if len(failedPlugins) == 0 { + return nil + } + + requiredPlugins := make([]string, 0) + for _, plugin := range l.Plugins { + if plugin.Required { + requiredPlugins = append(requiredPlugins, plugin.Name) + } + } + + for _, plugin := range requiredPlugins { + index := slices.IndexFunc(failedPlugins, func(failedPlugin string) bool { return failedPlugin == plugin }) + if index >= 0 { + return fmt.Errorf("linter failed. Required plugin %s failed", plugin) + } + } + + return nil +} diff --git a/server/linter/resource/resource.go b/server/linter/resource/resource.go new file mode 100644 index 0000000000..9ec7005da9 --- /dev/null +++ b/server/linter/resource/resource.go @@ -0,0 +1,14 @@ +package linter_resource + +import "github.com/kubeshop/tracetest/server/resourcemanager" + +const ( + ResourceName = "Linter" + ResourceNamePlural = "Linters" +) + +var Operations = []resourcemanager.Operation{ + resourcemanager.OperationGet, + resourcemanager.OperationList, + resourcemanager.OperationUpdate, +} diff --git a/server/migrations/26_add_linter.down.sql b/server/migrations/26_add_linter.down.sql new file mode 100644 index 0000000000..1de442ce9c --- /dev/null +++ b/server/migrations/26_add_linter.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE "linters"; + +COMMIT; diff --git a/server/migrations/26_add_linter.up.sql b/server/migrations/26_add_linter.up.sql new file mode 100644 index 0000000000..7507ade37b --- /dev/null +++ b/server/migrations/26_add_linter.up.sql @@ -0,0 +1,14 @@ +BEGIN; + +CREATE TABLE "linters" ( + "id" varchar not null primary key, + "name" varchar not null, + "enabled" boolean not null default true, + "minimum_score" integer not null, + "plugins" JSONB +); + +ALTER TABLE test_runs + ADD COLUMN linter JSONB; + +COMMIT; diff --git a/server/model/events/events.go b/server/model/events/events.go index 4bf15d9dc0..b841c46736 100644 --- a/server/model/events/events.go +++ b/server/model/events/events.go @@ -415,3 +415,63 @@ func TraceOtlpServerReceivedSpans(testID id.ID, runID, spanCount int, requestTyp Outputs: []model.OutputInfo{}, } } + +func TraceLinterStart(testID id.ID, runID int) model.TestRunEvent { + return model.TestRunEvent{ + TestID: testID, + RunID: runID, + Stage: model.StageTrace, + Type: "TRACE_LINTER_START", + Title: "Trace linter started", + Description: "The trace linter process has started", + CreatedAt: time.Now(), + DataStoreConnection: model.ConnectionResult{}, + Polling: model.PollingInfo{}, + Outputs: []model.OutputInfo{}, + } +} + +func TraceLinterSkip(testID id.ID, runID int, reason string) model.TestRunEvent { + return model.TestRunEvent{ + TestID: testID, + RunID: runID, + Stage: model.StageTrace, + Type: "TRACE_LINTER_SKIPPED", + Title: "Trace linter skipped", + Description: fmt.Sprintf("The trace linter process has been skipped. Reason: %s", reason), + CreatedAt: time.Now(), + DataStoreConnection: model.ConnectionResult{}, + Polling: model.PollingInfo{}, + Outputs: []model.OutputInfo{}, + } +} + +func TraceLinterSuccess(testID id.ID, runID int) model.TestRunEvent { + return model.TestRunEvent{ + TestID: testID, + RunID: runID, + Stage: model.StageTrace, + Type: "TRACE_LINTER_SUCCESS", + Title: "Trace linter succeeded", + Description: "The trace linter process was performed successfully", + CreatedAt: time.Now(), + DataStoreConnection: model.ConnectionResult{}, + Polling: model.PollingInfo{}, + Outputs: []model.OutputInfo{}, + } +} + +func TraceLinterError(testID id.ID, runID int, err error) model.TestRunEvent { + return model.TestRunEvent{ + TestID: testID, + RunID: runID, + Stage: model.StageTrace, + Type: "TRACE_LINTER_ERROR", + Title: "Trace linter error", + Description: fmt.Sprintf("The trace linter encountered fatal errors. Error: %s", err), + CreatedAt: time.Now(), + DataStoreConnection: model.ConnectionResult{}, + Polling: model.PollingInfo{}, + Outputs: []model.OutputInfo{}, + } +} diff --git a/server/model/linter.go b/server/model/linter.go new file mode 100644 index 0000000000..c60aa94e81 --- /dev/null +++ b/server/model/linter.go @@ -0,0 +1,68 @@ +package model + +import ( + "context" +) + +type PluginResult struct { + BasePlugin + Passed bool `json:"passed"` + Score int `json:"score"` + Rules []RuleResult `json:"rules"` +} + +func (pr PluginResult) CalculateResults() PluginResult { + failedScore := 0 + passed := true + + for _, result := range pr.Rules { + if !result.Passed { + passed = false + failedScore += result.Weight + } + } + + pr.Score = 100 - failedScore + pr.Passed = passed + return pr +} + +type Plugin interface { + Execute(context.Context, Trace) (PluginResult, error) + Name() string +} + +type BasePlugin struct { + Name string `json:"name"` + Description string `json:"description"` + Rules []Rule `json:"rules"` +} + +type Rule interface { + Evaluate(context.Context, Trace) (RuleResult, error) +} + +type LinterResult struct { + Plugins []PluginResult `json:"plugins"` + Score int `json:"score"` + Passed bool `json:"passed"` +} + +type BaseRule struct { + Name string + Description string + Tips []string + Weight int +} + +type RuleResult struct { + BaseRule + Passed bool `json:"passed"` + Results []Result `json:"results"` +} + +type Result struct { + SpanID string `json:"span_id"` + Passed bool `json:"passed"` + Errors []string `json:"error"` +} diff --git a/server/model/run.go b/server/model/run.go index ce2491225b..b81563c494 100644 --- a/server/model/run.go +++ b/server/model/run.go @@ -96,7 +96,7 @@ func (r Run) SuccessfullyTriggered() Run { } func (r Run) SuccessfullyPolledTraces(t *Trace) Run { - r.State = RunStateAwaitingTestResults + r.State = RunStateAnalyzingTrace r.Trace = t r.ObtainedTraceAt = time.Now() return r @@ -145,3 +145,16 @@ func (r Run) AssertionFailed(err error) Run { r.LastError = err return r.Finish() } + +func (r Run) LinterError(err error) Run { + r.State = RunStateAnalyzingError + r.LastError = err + return r.Finish() +} + +func (r Run) SuccessfullinterExecution(linter LinterResult) Run { + r.State = RunStateAwaitingTestResults + r.Linter = linter + + return r +} diff --git a/server/model/spans.go b/server/model/spans.go index 67028fd54d..9ea23e13a7 100644 --- a/server/model/spans.go +++ b/server/model/spans.go @@ -44,12 +44,24 @@ func (s Spans) OrEmpty(fn func()) Spans { return s } +type SpanKind string + +var ( + SpanKindClient SpanKind = "client" + SpanKindServer SpanKind = "server" + SpanKindConsumer SpanKind = "consumer" + SpanKindProducer SpanKind = "producer" + SpanKindInternal SpanKind = "internal" + SpanKindUnespecified SpanKind = "unespecified" +) + type Span struct { ID trace.SpanID Name string StartTime time.Time EndTime time.Time Attributes Attributes + Kind SpanKind Parent *Span `json:"-"` Children []*Span `json:"-"` diff --git a/server/model/tests.go b/server/model/tests.go index d9d7899e64..dc2c4ac2e2 100644 --- a/server/model/tests.go +++ b/server/model/tests.go @@ -114,6 +114,7 @@ type ( TransactionID string TransactionRunID string + Linter LinterResult } RunResults struct { @@ -196,6 +197,8 @@ const ( RunStateTriggerFailed RunState = "TRIGGER_FAILED" RunStateTraceFailed RunState = "TRACE_FAILED" RunStateAssertionFailed RunState = "ASSERTION_FAILED" + RunStateAnalyzingTrace RunState = "ANALYZING_TRACE" + RunStateAnalyzingError RunState = "ANALYZING_ERROR" RunStateFinished RunState = "FINISHED" RunStateStopped RunState = "STOPPED" RunStateAwaitingTestResults RunState = "AWAITING_TEST_RESULTS" diff --git a/server/openapi/api.go b/server/openapi/api.go index 5d1c2c5866..a8181e207b 100644 --- a/server/openapi/api.go +++ b/server/openapi/api.go @@ -58,15 +58,18 @@ type ApiApiRouter interface { type ResourceApiApiRouter interface { CreateDemo(http.ResponseWriter, *http.Request) CreateEnvironment(http.ResponseWriter, *http.Request) + CreateLinter(http.ResponseWriter, *http.Request) CreateTransaction(http.ResponseWriter, *http.Request) DeleteDataStore(http.ResponseWriter, *http.Request) DeleteDemo(http.ResponseWriter, *http.Request) DeleteEnvironment(http.ResponseWriter, *http.Request) + DeleteLinter(http.ResponseWriter, *http.Request) DeleteTransaction(http.ResponseWriter, *http.Request) GetConfiguration(http.ResponseWriter, *http.Request) GetDataStore(http.ResponseWriter, *http.Request) GetDemo(http.ResponseWriter, *http.Request) GetEnvironment(http.ResponseWriter, *http.Request) + GetLinter(http.ResponseWriter, *http.Request) GetPollingProfile(http.ResponseWriter, *http.Request) GetTransaction(http.ResponseWriter, *http.Request) GetTransactions(http.ResponseWriter, *http.Request) @@ -74,11 +77,13 @@ type ResourceApiApiRouter interface { ListDataStore(http.ResponseWriter, *http.Request) ListDemos(http.ResponseWriter, *http.Request) ListEnvironments(http.ResponseWriter, *http.Request) + ListLinters(http.ResponseWriter, *http.Request) ListPollingProfile(http.ResponseWriter, *http.Request) UpdateConfiguration(http.ResponseWriter, *http.Request) UpdateDataStore(http.ResponseWriter, *http.Request) UpdateDemo(http.ResponseWriter, *http.Request) UpdateEnvironment(http.ResponseWriter, *http.Request) + UpdateLinter(http.ResponseWriter, *http.Request) UpdatePollingProfile(http.ResponseWriter, *http.Request) UpdateTransaction(http.ResponseWriter, *http.Request) } @@ -129,15 +134,18 @@ type ApiApiServicer interface { type ResourceApiApiServicer interface { CreateDemo(context.Context, Demo) (ImplResponse, error) CreateEnvironment(context.Context, EnvironmentResource) (ImplResponse, error) + CreateLinter(context.Context, LinterResource) (ImplResponse, error) CreateTransaction(context.Context, TransactionResource) (ImplResponse, error) DeleteDataStore(context.Context, string) (ImplResponse, error) DeleteDemo(context.Context, string) (ImplResponse, error) DeleteEnvironment(context.Context, string) (ImplResponse, error) + DeleteLinter(context.Context, string) (ImplResponse, error) DeleteTransaction(context.Context, string) (ImplResponse, error) GetConfiguration(context.Context, string) (ImplResponse, error) GetDataStore(context.Context, string) (ImplResponse, error) GetDemo(context.Context, string) (ImplResponse, error) GetEnvironment(context.Context, string) (ImplResponse, error) + GetLinter(context.Context, string) (ImplResponse, error) GetPollingProfile(context.Context, string) (ImplResponse, error) GetTransaction(context.Context, string) (ImplResponse, error) GetTransactions(context.Context, int32, int32, string, string, string) (ImplResponse, error) @@ -145,11 +153,13 @@ type ResourceApiApiServicer interface { ListDataStore(context.Context, int32, int32, string, string) (ImplResponse, error) ListDemos(context.Context, int32, int32, string, string) (ImplResponse, error) ListEnvironments(context.Context, int32, int32, string, string) (ImplResponse, error) + ListLinters(context.Context, int32, int32, string, string) (ImplResponse, error) ListPollingProfile(context.Context, int32, int32, string, string) (ImplResponse, error) UpdateConfiguration(context.Context, string, ConfigurationResource) (ImplResponse, error) UpdateDataStore(context.Context, string, DataStore) (ImplResponse, error) UpdateDemo(context.Context, string, Demo) (ImplResponse, error) UpdateEnvironment(context.Context, string, EnvironmentResource) (ImplResponse, error) + UpdateLinter(context.Context, string, LinterResource) (ImplResponse, error) UpdatePollingProfile(context.Context, string, PollingProfile) (ImplResponse, error) UpdateTransaction(context.Context, string, TransactionResource) (ImplResponse, error) } diff --git a/server/openapi/api_resource_api.go b/server/openapi/api_resource_api.go index a4611f2031..630bb497a4 100644 --- a/server/openapi/api_resource_api.go +++ b/server/openapi/api_resource_api.go @@ -62,6 +62,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/environments", c.CreateEnvironment, }, + { + "CreateLinter", + strings.ToUpper("Post"), + "/api/linters", + c.CreateLinter, + }, { "CreateTransaction", strings.ToUpper("Post"), @@ -86,6 +92,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/environments/{environmentId}", c.DeleteEnvironment, }, + { + "DeleteLinter", + strings.ToUpper("Delete"), + "/api/linters/{LinterId}", + c.DeleteLinter, + }, { "DeleteTransaction", strings.ToUpper("Delete"), @@ -116,6 +128,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/environments/{environmentId}", c.GetEnvironment, }, + { + "GetLinter", + strings.ToUpper("Get"), + "/api/linters/{LinterId}", + c.GetLinter, + }, { "GetPollingProfile", strings.ToUpper("Get"), @@ -158,6 +176,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/environments", c.ListEnvironments, }, + { + "ListLinters", + strings.ToUpper("Get"), + "/api/linters", + c.ListLinters, + }, { "ListPollingProfile", strings.ToUpper("Get"), @@ -188,6 +212,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/environments/{environmentId}", c.UpdateEnvironment, }, + { + "UpdateLinter", + strings.ToUpper("Put"), + "/api/linters/{LinterId}", + c.UpdateLinter, + }, { "UpdatePollingProfile", strings.ToUpper("Put"), @@ -251,6 +281,30 @@ func (c *ResourceApiApiController) CreateEnvironment(w http.ResponseWriter, r *h } +// CreateLinter - Create an Linter +func (c *ResourceApiApiController) CreateLinter(w http.ResponseWriter, r *http.Request) { + linterResourceParam := LinterResource{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&linterResourceParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertLinterResourceRequired(linterResourceParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.CreateLinter(r.Context(), linterResourceParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // CreateTransaction - Create new transaction func (c *ResourceApiApiController) CreateTransaction(w http.ResponseWriter, r *http.Request) { transactionResourceParam := TransactionResource{} @@ -323,6 +377,22 @@ func (c *ResourceApiApiController) DeleteEnvironment(w http.ResponseWriter, r *h } +// DeleteLinter - Delete an Linter +func (c *ResourceApiApiController) DeleteLinter(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + linterIdParam := params["LinterId"] + + result, err := c.service.DeleteLinter(r.Context(), linterIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // DeleteTransaction - delete a transaction func (c *ResourceApiApiController) DeleteTransaction(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -403,6 +473,22 @@ func (c *ResourceApiApiController) GetEnvironment(w http.ResponseWriter, r *http } +// GetLinter - Get a specific Linter +func (c *ResourceApiApiController) GetLinter(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + linterIdParam := params["LinterId"] + + result, err := c.service.GetLinter(r.Context(), linterIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // GetPollingProfile - Get Polling Profile func (c *ResourceApiApiController) GetPollingProfile(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -566,6 +652,32 @@ func (c *ResourceApiApiController) ListEnvironments(w http.ResponseWriter, r *ht } +// ListLinters - List Linters +func (c *ResourceApiApiController) ListLinters(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + takeParam, err := parseInt32Parameter(query.Get("take"), false) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + skipParam, err := parseInt32Parameter(query.Get("skip"), false) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + sortByParam := query.Get("sortBy") + sortDirectionParam := query.Get("sortDirection") + result, err := c.service.ListLinters(r.Context(), takeParam, skipParam, sortByParam, sortDirectionParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // ListPollingProfile - List Polling Profile Configuration func (c *ResourceApiApiController) ListPollingProfile(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() @@ -700,6 +812,33 @@ func (c *ResourceApiApiController) UpdateEnvironment(w http.ResponseWriter, r *h } +// UpdateLinter - Update a Linter +func (c *ResourceApiApiController) UpdateLinter(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + linterIdParam := params["LinterId"] + + linterResourceParam := LinterResource{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&linterResourceParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertLinterResourceRequired(linterResourceParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.UpdateLinter(r.Context(), linterIdParam, linterResourceParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // UpdatePollingProfile - Update a Polling Profile func (c *ResourceApiApiController) UpdatePollingProfile(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/server/openapi/api_resource_api_service.go b/server/openapi/api_resource_api_service.go index 484bbdb1bd..1ea9c8f88a 100644 --- a/server/openapi/api_resource_api_service.go +++ b/server/openapi/api_resource_api_service.go @@ -54,6 +54,20 @@ func (s *ResourceApiApiService) CreateEnvironment(ctx context.Context, environme return Response(http.StatusNotImplemented, nil), errors.New("CreateEnvironment method not implemented") } +// CreateLinter - Create an Linter +func (s *ResourceApiApiService) CreateLinter(ctx context.Context, linterResource LinterResource) (ImplResponse, error) { + // TODO - update CreateLinter with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(201, LinterResource{}) or use other options such as http.Ok ... + //return Response(201, LinterResource{}), nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("CreateLinter method not implemented") +} + // CreateTransaction - Create new transaction func (s *ResourceApiApiService) CreateTransaction(ctx context.Context, transactionResource TransactionResource) (ImplResponse, error) { // TODO - update CreateTransaction with the required logic for this service method. @@ -122,6 +136,26 @@ func (s *ResourceApiApiService) DeleteEnvironment(ctx context.Context, environme return Response(http.StatusNotImplemented, nil), errors.New("DeleteEnvironment method not implemented") } +// DeleteLinter - Delete an Linter +func (s *ResourceApiApiService) DeleteLinter(ctx context.Context, linterId string) (ImplResponse, error) { + // TODO - update DeleteLinter with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(204, {}) or use other options such as http.Ok ... + //return Response(204, nil),nil + + //TODO: Uncomment the next line to return response Response(400, {}) or use other options such as http.Ok ... + //return Response(400, nil),nil + + //TODO: Uncomment the next line to return response Response(404, {}) or use other options such as http.Ok ... + //return Response(404, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("DeleteLinter method not implemented") +} + // DeleteTransaction - delete a transaction func (s *ResourceApiApiService) DeleteTransaction(ctx context.Context, transactionId string) (ImplResponse, error) { // TODO - update DeleteTransaction with the required logic for this service method. @@ -204,6 +238,23 @@ func (s *ResourceApiApiService) GetEnvironment(ctx context.Context, environmentI return Response(http.StatusNotImplemented, nil), errors.New("GetEnvironment method not implemented") } +// GetLinter - Get a specific Linter +func (s *ResourceApiApiService) GetLinter(ctx context.Context, linterId string) (ImplResponse, error) { + // TODO - update GetLinter with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, LinterResource{}) or use other options such as http.Ok ... + //return Response(200, LinterResource{}), nil + + //TODO: Uncomment the next line to return response Response(404, {}) or use other options such as http.Ok ... + //return Response(404, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("GetLinter method not implemented") +} + // GetPollingProfile - Get Polling Profile func (s *ResourceApiApiService) GetPollingProfile(ctx context.Context, pollingProfileId string) (ImplResponse, error) { // TODO - update GetPollingProfile with the required logic for this service method. @@ -317,6 +368,23 @@ func (s *ResourceApiApiService) ListEnvironments(ctx context.Context, take int32 return Response(http.StatusNotImplemented, nil), errors.New("ListEnvironments method not implemented") } +// ListLinters - List Linters +func (s *ResourceApiApiService) ListLinters(ctx context.Context, take int32, skip int32, sortBy string, sortDirection string) (ImplResponse, error) { + // TODO - update ListLinters with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, LinterResourceList{}) or use other options such as http.Ok ... + //return Response(200, LinterResourceList{}), nil + + //TODO: Uncomment the next line to return response Response(400, {}) or use other options such as http.Ok ... + //return Response(400, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("ListLinters method not implemented") +} + // ListPollingProfile - List Polling Profile Configuration func (s *ResourceApiApiService) ListPollingProfile(ctx context.Context, take int32, skip int32, sortBy string, sortDirection string) (ImplResponse, error) { // TODO - update ListPollingProfile with the required logic for this service method. @@ -402,6 +470,26 @@ func (s *ResourceApiApiService) UpdateEnvironment(ctx context.Context, environme return Response(http.StatusNotImplemented, nil), errors.New("UpdateEnvironment method not implemented") } +// UpdateLinter - Update a Linter +func (s *ResourceApiApiService) UpdateLinter(ctx context.Context, linterId string, linterResource LinterResource) (ImplResponse, error) { + // TODO - update UpdateLinter with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, LinterResource{}) or use other options such as http.Ok ... + //return Response(200, LinterResource{}), nil + + //TODO: Uncomment the next line to return response Response(400, {}) or use other options such as http.Ok ... + //return Response(400, nil),nil + + //TODO: Uncomment the next line to return response Response(404, {}) or use other options such as http.Ok ... + //return Response(404, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("UpdateLinter method not implemented") +} + // UpdatePollingProfile - Update a Polling Profile func (s *ResourceApiApiService) UpdatePollingProfile(ctx context.Context, pollingProfileId string, pollingProfile PollingProfile) (ImplResponse, error) { // TODO - update UpdatePollingProfile with the required logic for this service method. diff --git a/server/openapi/model_linter_resource.go b/server/openapi/model_linter_resource.go new file mode 100644 index 0000000000..cba0ff5de6 --- /dev/null +++ b/server/openapi/model_linter_resource.go @@ -0,0 +1,36 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResource struct { + Type string `json:"type,omitempty"` + + Spec LinterResourceSpec `json:"spec,omitempty"` +} + +// AssertLinterResourceRequired checks if the required fields are not zero-ed +func AssertLinterResourceRequired(obj LinterResource) error { + if err := AssertLinterResourceSpecRequired(obj.Spec); err != nil { + return err + } + return nil +} + +// AssertRecurseLinterResourceRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResource (e.g. [][]LinterResource), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResourceRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResource, ok := obj.(LinterResource) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResourceRequired(aLinterResource) + }) +} diff --git a/server/openapi/model_linter_resource_list.go b/server/openapi/model_linter_resource_list.go new file mode 100644 index 0000000000..fb83e03ca2 --- /dev/null +++ b/server/openapi/model_linter_resource_list.go @@ -0,0 +1,36 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResourceList struct { + Items []LinterResource `json:"items,omitempty"` +} + +// AssertLinterResourceListRequired checks if the required fields are not zero-ed +func AssertLinterResourceListRequired(obj LinterResourceList) error { + for _, el := range obj.Items { + if err := AssertLinterResourceRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseLinterResourceListRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResourceList (e.g. [][]LinterResourceList), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResourceListRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResourceList, ok := obj.(LinterResourceList) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResourceListRequired(aLinterResourceList) + }) +} diff --git a/server/openapi/model_linter_resource_plugin.go b/server/openapi/model_linter_resource_plugin.go new file mode 100644 index 0000000000..bfca8f74ef --- /dev/null +++ b/server/openapi/model_linter_resource_plugin.go @@ -0,0 +1,35 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResourcePlugin struct { + Name string `json:"name,omitempty"` + + Enabled bool `json:"enabled,omitempty"` + + Required bool `json:"required,omitempty"` +} + +// AssertLinterResourcePluginRequired checks if the required fields are not zero-ed +func AssertLinterResourcePluginRequired(obj LinterResourcePlugin) error { + return nil +} + +// AssertRecurseLinterResourcePluginRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResourcePlugin (e.g. [][]LinterResourcePlugin), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResourcePluginRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResourcePlugin, ok := obj.(LinterResourcePlugin) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResourcePluginRequired(aLinterResourcePlugin) + }) +} diff --git a/server/openapi/model_linter_resource_spec.go b/server/openapi/model_linter_resource_spec.go new file mode 100644 index 0000000000..ffb5bdd093 --- /dev/null +++ b/server/openapi/model_linter_resource_spec.go @@ -0,0 +1,44 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResourceSpec struct { + Id string `json:"id,omitempty"` + + Name string `json:"name,omitempty"` + + Enabled bool `json:"enabled,omitempty"` + + MinimumScore int32 `json:"minimumScore,omitempty"` + + Plugins []LinterResourcePlugin `json:"plugins,omitempty"` +} + +// AssertLinterResourceSpecRequired checks if the required fields are not zero-ed +func AssertLinterResourceSpecRequired(obj LinterResourceSpec) error { + for _, el := range obj.Plugins { + if err := AssertLinterResourcePluginRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseLinterResourceSpecRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResourceSpec (e.g. [][]LinterResourceSpec), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResourceSpecRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResourceSpec, ok := obj.(LinterResourceSpec) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResourceSpecRequired(aLinterResourceSpec) + }) +} diff --git a/server/openapi/model_linter_result.go b/server/openapi/model_linter_result.go new file mode 100644 index 0000000000..7b6aa35ea3 --- /dev/null +++ b/server/openapi/model_linter_result.go @@ -0,0 +1,40 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResult struct { + Passed bool `json:"passed,omitempty"` + + Score int32 `json:"score,omitempty"` + + Plugins []LinterResultPlugin `json:"plugins,omitempty"` +} + +// AssertLinterResultRequired checks if the required fields are not zero-ed +func AssertLinterResultRequired(obj LinterResult) error { + for _, el := range obj.Plugins { + if err := AssertLinterResultPluginRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseLinterResultRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResult (e.g. [][]LinterResult), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResultRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResult, ok := obj.(LinterResult) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResultRequired(aLinterResult) + }) +} diff --git a/server/openapi/model_linter_result_plugin.go b/server/openapi/model_linter_result_plugin.go new file mode 100644 index 0000000000..17a0bf786e --- /dev/null +++ b/server/openapi/model_linter_result_plugin.go @@ -0,0 +1,44 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResultPlugin struct { + Name string `json:"name,omitempty"` + + Description string `json:"description,omitempty"` + + Passed bool `json:"passed,omitempty"` + + Score int32 `json:"score,omitempty"` + + Rules []LinterResultPluginRule `json:"rules,omitempty"` +} + +// AssertLinterResultPluginRequired checks if the required fields are not zero-ed +func AssertLinterResultPluginRequired(obj LinterResultPlugin) error { + for _, el := range obj.Rules { + if err := AssertLinterResultPluginRuleRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseLinterResultPluginRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResultPlugin (e.g. [][]LinterResultPlugin), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResultPluginRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResultPlugin, ok := obj.(LinterResultPlugin) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResultPluginRequired(aLinterResultPlugin) + }) +} diff --git a/server/openapi/model_linter_result_plugin_rule.go b/server/openapi/model_linter_result_plugin_rule.go new file mode 100644 index 0000000000..21dea751c8 --- /dev/null +++ b/server/openapi/model_linter_result_plugin_rule.go @@ -0,0 +1,46 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResultPluginRule struct { + Name string `json:"name,omitempty"` + + Description string `json:"description,omitempty"` + + Passed bool `json:"passed,omitempty"` + + Weight int32 `json:"weight,omitempty"` + + Tips []string `json:"tips,omitempty"` + + Results []LinterResultPluginRuleResult `json:"results,omitempty"` +} + +// AssertLinterResultPluginRuleRequired checks if the required fields are not zero-ed +func AssertLinterResultPluginRuleRequired(obj LinterResultPluginRule) error { + for _, el := range obj.Results { + if err := AssertLinterResultPluginRuleResultRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseLinterResultPluginRuleRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResultPluginRule (e.g. [][]LinterResultPluginRule), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResultPluginRuleRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResultPluginRule, ok := obj.(LinterResultPluginRule) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResultPluginRuleRequired(aLinterResultPluginRule) + }) +} diff --git a/server/openapi/model_linter_result_plugin_rule_result.go b/server/openapi/model_linter_result_plugin_rule_result.go new file mode 100644 index 0000000000..0aefeb68ed --- /dev/null +++ b/server/openapi/model_linter_result_plugin_rule_result.go @@ -0,0 +1,37 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type LinterResultPluginRuleResult struct { + SpanId string `json:"spanId,omitempty"` + + Errors []string `json:"errors,omitempty"` + + Passed bool `json:"passed,omitempty"` + + Severity string `json:"severity,omitempty"` +} + +// AssertLinterResultPluginRuleResultRequired checks if the required fields are not zero-ed +func AssertLinterResultPluginRuleResultRequired(obj LinterResultPluginRuleResult) error { + return nil +} + +// AssertRecurseLinterResultPluginRuleResultRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of LinterResultPluginRuleResult (e.g. [][]LinterResultPluginRuleResult), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseLinterResultPluginRuleResultRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aLinterResultPluginRuleResult, ok := obj.(LinterResultPluginRuleResult) + if !ok { + return ErrTypeAssertionError + } + return AssertLinterResultPluginRuleResultRequired(aLinterResultPluginRuleResult) + }) +} diff --git a/server/openapi/model_span.go b/server/openapi/model_span.go index 1b507cdf71..daca847549 100644 --- a/server/openapi/model_span.go +++ b/server/openapi/model_span.go @@ -16,6 +16,8 @@ type Span struct { Name string `json:"name,omitempty"` + Kind string `json:"kind,omitempty"` + // span start time in unix milli format StartTime int64 `json:"startTime,omitempty"` diff --git a/server/openapi/model_test_run.go b/server/openapi/model_test_run.go index d40a1f129d..667528bd18 100644 --- a/server/openapi/model_test_run.go +++ b/server/openapi/model_test_run.go @@ -53,6 +53,8 @@ type TestRun struct { Result AssertionResults `json:"result,omitempty"` + Linter LinterResult `json:"linter,omitempty"` + Outputs []TestRunOutputsInner `json:"outputs,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` @@ -76,6 +78,9 @@ func AssertTestRunRequired(obj TestRun) error { if err := AssertAssertionResultsRequired(obj.Result); err != nil { return err } + if err := AssertLinterResultRequired(obj.Linter); err != nil { + return err + } for _, el := range obj.Outputs { if err := AssertTestRunOutputsInnerRequired(el); err != nil { return err diff --git a/server/testdb/runs.go b/server/testdb/runs.go index 7976b80877..9e500ff5b3 100644 --- a/server/testdb/runs.go +++ b/server/testdb/runs.go @@ -49,7 +49,10 @@ INSERT INTO test_runs ( "metadata", -- environment - "environment" + "environment", + + -- linter + "linter" ) VALUES ( nextval('` + runSequenceName + `'), -- id $1, -- test_id @@ -77,7 +80,8 @@ INSERT INTO test_runs ( 0, -- fail $12, -- metadata - $13 -- environment + $13, -- environment + $14 -- linter ) RETURNING "id"` @@ -138,6 +142,11 @@ func (td *postgresDB) CreateRun(ctx context.Context, test model.Test, run model. return model.Run{}, fmt.Errorf("environment encoding error: %w", err) } + jsonlinter, err := json.Marshal(run.Linter) + if err != nil { + return model.Run{}, fmt.Errorf("environment encoding error: %w", err) + } + tx, err := td.db.BeginTx(ctx, nil) if err != nil { return model.Run{}, fmt.Errorf("sql beginTx: %w", err) @@ -166,6 +175,7 @@ func (td *postgresDB) CreateRun(ctx context.Context, test model.Test, run model. jsonTrace, jsonMetadata, jsonEnvironment, + jsonlinter, ).Scan(&runID) if err != nil { tx.Rollback() @@ -201,7 +211,10 @@ UPDATE test_runs SET "fail" = $14, "metadata" = $15, - "environment" = $18 + "environment" = $18, + + --- linter + "linter" = $19 WHERE id = $16 AND test_id = $17 ` @@ -243,6 +256,11 @@ func (td *postgresDB) UpdateRun(ctx context.Context, r model.Run) error { return fmt.Errorf("encoding error: %w", err) } + jsonlinter, err := json.Marshal(r.Linter) + if err != nil { + return fmt.Errorf("encoding error: %w", err) + } + var lastError *string if r.LastError != nil { e := r.LastError.Error() @@ -271,6 +289,7 @@ func (td *postgresDB) UpdateRun(ctx context.Context, r model.Run) error { r.ID, r.TestID, jsonEnvironment, + jsonlinter, ) if err != nil { return fmt.Errorf("sql exec: %w", err) @@ -335,7 +354,9 @@ SELECT -- transaction run transaction_run_steps.transaction_run_id, - transaction_run_steps.transaction_run_transaction_id + transaction_run_steps.transaction_run_transaction_id, + "linter" + FROM test_runs LEFT OUTER JOIN transaction_run_steps @@ -440,6 +461,7 @@ func readRunRow(row scanner) (model.Run, error) { jsonTrace, jsonOutputs, jsonEnvironment, + jsonLinter, jsonMetadata []byte lastError *string @@ -471,6 +493,7 @@ func readRunRow(row scanner) (model.Run, error) { &jsonEnvironment, &transactionRunID, &transactionID, + &jsonLinter, ) switch err { @@ -494,6 +517,13 @@ func readRunRow(row scanner) (model.Run, error) { } } + if jsonLinter != nil { + err = json.Unmarshal(jsonLinter, &r.Linter) + if err != nil { + return model.Run{}, fmt.Errorf("cannot parse linter: %w", err) + } + } + err = json.Unmarshal(jsonOutputs, &r.Outputs) if err != nil { // try with raw outputs diff --git a/server/traces/otel_converter.go b/server/traces/otel_converter.go index 5224f10960..7cab8b699d 100644 --- a/server/traces/otel_converter.go +++ b/server/traces/otel_converter.go @@ -56,6 +56,7 @@ func ConvertOtelSpanIntoSpan(span *v1.Span) *model.Span { return &model.Span{ ID: spanID, Name: span.Name, + Kind: spanKind(span), StartTime: startTime, EndTime: endTime, Parent: nil, @@ -64,6 +65,23 @@ func ConvertOtelSpanIntoSpan(span *v1.Span) *model.Span { } } +func spanKind(span *v1.Span) model.SpanKind { + switch span.Kind { + case v1.Span_SPAN_KIND_CLIENT: + return model.SpanKindClient + case v1.Span_SPAN_KIND_SERVER: + return model.SpanKindServer + case v1.Span_SPAN_KIND_INTERNAL: + return model.SpanKindInternal + case v1.Span_SPAN_KIND_PRODUCER: + return model.SpanKindProducer + case v1.Span_SPAN_KIND_CONSUMER: + return model.SpanKindConsumer + default: + return model.SpanKindUnespecified + } +} + func getAttributeValue(value *v11.AnyValue) string { switch v := value.GetValue().(type) { case *v11.AnyValue_StringValue: diff --git a/web/cypress/e2e/Transactions/TransactionsRun.spec.ts b/web/cypress/e2e/Transactions/TransactionsRun.spec.ts index bccac0622f..5a7f3d801e 100644 --- a/web/cypress/e2e/Transactions/TransactionsRun.spec.ts +++ b/web/cypress/e2e/Transactions/TransactionsRun.spec.ts @@ -62,7 +62,7 @@ describe('Transactions', () => { cy.submitCreateForm('CreateTransactionFactory'); cy.get('[data-cy=transaction-details-name').should('have.text', `${name} (v1)`); - cy.get('[data-cy=transaction-run-button]', {timeout: 30000}).should('be.visible'); + cy.get('[data-cy=transaction-run-button]', {timeout: 50000}).should('be.visible'); cy.get('[data-cy^=transaction-execution-step-]').should('have.length', 2); const updateName = `${name} - updated`; @@ -75,7 +75,7 @@ describe('Transactions', () => { cy.get('[data-cy=edit-transaction-submit]').click(); cy.get('[data-cy=transaction-details-name').should('have.text', `${updateName} (v2)`); - cy.get('[data-cy=transaction-run-button]', {timeout: 30000}).should('be.visible'); + cy.get('[data-cy=transaction-run-button]', {timeout: 50000}).should('be.visible'); cy.get('[data-cy^=transaction-execution-step-]').should('have.length', 4); }); }); diff --git a/web/src/components/BetaBadge/BetaBadge.styled.ts b/web/src/components/BetaBadge/BetaBadge.styled.ts new file mode 100644 index 0000000000..cf7b96279e --- /dev/null +++ b/web/src/components/BetaBadge/BetaBadge.styled.ts @@ -0,0 +1,12 @@ +import {Tag} from 'antd'; +import styled from 'styled-components'; + +export const Badge = styled(Tag).attrs(({theme}) => ({ + color: theme.color.primary, +}))` + font-size: 10px; + margin-left: 5px; + padding: 0 2px; + margin-right: 0; + line-height: 15px; +`; diff --git a/web/src/components/BetaBadge/BetaBadge.tsx b/web/src/components/BetaBadge/BetaBadge.tsx new file mode 100644 index 0000000000..f6e146101f --- /dev/null +++ b/web/src/components/BetaBadge/BetaBadge.tsx @@ -0,0 +1,5 @@ +import * as S from './BetaBadge.styled'; + +const BetaBadge = () => beta; + +export default BetaBadge; diff --git a/web/src/components/BetaBadge/index.ts b/web/src/components/BetaBadge/index.ts new file mode 100644 index 0000000000..a4764d7f36 --- /dev/null +++ b/web/src/components/BetaBadge/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export {default} from './BetaBadge'; diff --git a/web/src/components/LintScore/LintScore.styled.ts b/web/src/components/LintScore/LintScore.styled.ts new file mode 100644 index 0000000000..184db4e2b6 --- /dev/null +++ b/web/src/components/LintScore/LintScore.styled.ts @@ -0,0 +1,40 @@ +import {Progress, Typography} from 'antd'; +import styled from 'styled-components'; + +export const ScoreWrapper = styled.div` + position: relative; +`; + +export const ScoreTexContainer = styled.div` + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +export const Score = styled(Typography.Title)` + && { + font-size: 12px; + margin-bottom: 0; + } +`; + +export const ScoreContainer = styled.div` + margin-bottom: 24px; + text-align: center; +`; + +export const ScoreProgress = styled(Progress)<{$height?: string, $width?: string }>` + .ant-progress-inner { + height: ${({$height = "50px"}) => $height} !important; + width: ${({$width = "50px"}) => $width} !important; + } + + .ant-progress-circle-trail, + .ant-progress-circle-path { + stroke-width: 20px; + } +`; diff --git a/web/src/components/LintScore/LintScore.tsx b/web/src/components/LintScore/LintScore.tsx new file mode 100644 index 0000000000..a9c8814dc3 --- /dev/null +++ b/web/src/components/LintScore/LintScore.tsx @@ -0,0 +1,28 @@ +import * as S from './LintScore.styled'; + +interface IProps { + score: number; + passed: boolean; + height?: string; + width?: string; +} + +const LintScore = ({score, passed, height, width}: IProps) => { + return ( + + + {score} + + ''} + percent={score} + status={passed ? 'success' : 'exception'} + type="circle" + /> + + ); +}; + +export default LintScore; diff --git a/web/src/components/LintScore/index.ts b/web/src/components/LintScore/index.ts new file mode 100644 index 0000000000..4b8dc04e4e --- /dev/null +++ b/web/src/components/LintScore/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export {default} from './LintScore'; diff --git a/web/src/components/RunDetailTrace/CollapseIcon.tsx b/web/src/components/RunDetailTrace/CollapseIcon.tsx new file mode 100644 index 0000000000..19daf86e2e --- /dev/null +++ b/web/src/components/RunDetailTrace/CollapseIcon.tsx @@ -0,0 +1,13 @@ +import * as S from './LintResults.styled'; + +interface IProps { + isCollapsed: boolean; +} + +const CollapseIcon = ({isCollapsed}: IProps) => { + return ( + {isCollapsed ? : } + ); +}; + +export default CollapseIcon; diff --git a/web/src/components/RunDetailTrace/LintResults.styled.ts b/web/src/components/RunDetailTrace/LintResults.styled.ts new file mode 100644 index 0000000000..de10138bd6 --- /dev/null +++ b/web/src/components/RunDetailTrace/LintResults.styled.ts @@ -0,0 +1,135 @@ +import {CheckCircleFilled, CloseCircleFilled, DownOutlined, UpOutlined} from '@ant-design/icons'; +import {Button, Collapse, Progress, Typography} from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + padding: 24px; +`; + +export const Title = styled(Typography.Title)` + && { + margin-bottom: 8px; + display: flex; + align-items: center; + } +`; + +export const Description = styled(Typography.Paragraph).attrs({ + type: 'secondary', +})` + && { + margin-bottom: 30px; + } +`; + +export const ScoreWrapper = styled.div` + position: relative; +`; + +export const ScoreTexContainer = styled.div` + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +export const Score = styled(Typography.Title)` + && { + font-size: 12px; + margin-bottom: 0; + } +`; + +export const ScoreContainer = styled.div` + margin-bottom: 24px; + text-align: center; + cursor: pointer; +`; + +export const RuleContainer = styled.div` + border-bottom: ${({theme}) => `1px dashed ${theme.color.borderLight}`}; + padding-bottom: 16px; + margin-bottom: 16px; + margin-left: 32px; +`; + +export const RuleHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +export const Column = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 8px; +`; + +export const RuleBody = styled(Column)` + padding-left: 20px; +`; + +export const Subtitle = styled(Typography.Title)` + && { + margin-bottom: 8px; + } +`; + +export const ScoreProgress = styled(Progress)` + .ant-progress-inner { + height: 50px !important; + width: 50px !important; + } + + .ant-progress-circle-trail, + .ant-progress-circle-path { + stroke-width: 20px; + } +`; + +export const PluginPanel = styled(Collapse.Panel)` + background-color: ${({theme}) => theme.color.white}; + + .ant-collapse-content { + background-color: ${({theme}) => theme.color.background}; + } +`; + +export const PassedIcon = styled(CheckCircleFilled)<{$small?: boolean}>` + color: ${({theme}) => theme.color.success}; + font-size: ${({$small}) => ($small ? '14px' : '20px')}; +`; + +export const FailedIcon = styled(CloseCircleFilled)<{$small?: boolean}>` + color: ${({theme}) => theme.color.error}; + font-size: ${({$small}) => ($small ? '14px' : '20px')}; +`; + +export const SpanButton = styled(Button)<{$error?: boolean}>` + color: ${({theme, $error}) => ($error ? theme.color.error : theme.color.success)}; + padding-left: 0; +`; + +export const CollapseIconContainer = styled.div` + display: flex; + position: absolute; + top: 25%; + right: 16px; + border-left: 1px solid ${({theme}) => theme.color.borderLight}}; + padding-left: 14px; + height: 24px; + align-items: center; +`; + +export const DownCollapseIcon = styled(DownOutlined)` + opacity: 0.5; + font-size: ${({theme}) => theme.size.xs}; +`; + +export const UpCollapseIcon = styled(UpOutlined)` + opacity: 0.5; + font-size: ${({theme}) => theme.size.xs}; +`; diff --git a/web/src/components/RunDetailTrace/LintResults.tsx b/web/src/components/RunDetailTrace/LintResults.tsx new file mode 100644 index 0000000000..10c9ad7c84 --- /dev/null +++ b/web/src/components/RunDetailTrace/LintResults.tsx @@ -0,0 +1,141 @@ +import {CaretUpFilled} from '@ant-design/icons'; +import {Link} from 'react-router-dom'; +import {Col, Collapse, Row, Space, Tooltip, Typography} from 'antd'; +import {useCallback} from 'react'; +import LinterResult from 'models/LinterResult.model'; +import Span from 'models/Span.model'; +import Trace from 'models/Trace.model'; +import {useAppDispatch} from 'redux/hooks'; +import {selectSpan} from 'redux/slices/Trace.slice'; +import {DISCORD_URL, OCTOLIINT_ISSUE_URL} from 'constants/Common.constants'; +import * as S from './LintResults.styled'; +import LintScore from '../LintScore/LintScore'; +import CollapseIcon from './CollapseIcon'; +import BetaBadge from '../BetaBadge/BetaBadge'; + +interface IProps { + linterResult: LinterResult; + trace: Trace; +} + +function getSpanName(spans: Span[], traceId: string) { + const span = spans.find(s => s.id === traceId); + return span?.name ?? ''; +} + +const LintResults = ({linterResult, trace}: IProps) => { + const dispatch = useAppDispatch(); + + const onSpanResultClick = useCallback( + (spanId: string) => { + dispatch(selectSpan({spanId})); + }, + [dispatch] + ); + + return ( + + + Linter Results + + + The Tracetest Linter its a plugin based framework used to analyze Open Telemetry traces to help teams improve + their instrumentation data, find potential problems and provide tips to fix the problems. If you want to disable + the linter for all tests, go to the settings page. We have released this initial + version to get feedback from the community. Have thoughts about how to improve the Tracetest Linter? Add to this + Issue or Discord! + + + + + + + Trace Lint Result{' '} + + + + + {linterResult?.plugins?.map(plugin => ( + + + + {plugin.name} + + + + + ))} + + + }> + {linterResult?.plugins?.map(plugin => ( + + + {plugin.name} + {plugin.description} + + } + key={plugin.name} + > + {plugin.rules.map(rule => ( + + + + + {rule.passed ? : } + + {rule.name} + + + + + {rule.description} + + + + + {rule?.results?.map((result, resultIndex) => ( + // eslint-disable-next-line react/no-array-index-key +
+ {result.passed ? ( + } + onClick={() => onSpanResultClick(result.spanId)} + type="link" + > + {getSpanName(trace.spans, result.spanId)} + + ) : ( + <> + } + onClick={() => onSpanResultClick(result.spanId)} + type="link" + $error + > + {getSpanName(trace.spans, result.spanId)} + +
+ {result.errors.map(error => ( +
+ {error} +
+ ))} +
+ + )} +
+ ))} +
+
+ ))} +
+ ))} +
+
+ ); +}; + +export default LintResults; diff --git a/web/src/components/RunDetailTrace/RunDetailTrace.styled.ts b/web/src/components/RunDetailTrace/RunDetailTrace.styled.ts index 2160eb4d6b..988e99fbe0 100644 --- a/web/src/components/RunDetailTrace/RunDetailTrace.styled.ts +++ b/web/src/components/RunDetailTrace/RunDetailTrace.styled.ts @@ -19,6 +19,18 @@ export const Section = styled.div` z-index: 1; `; +export const SectionLeft = styled(Section)` + background-color: ${({theme}) => theme.color.background}; + z-index: 1; +`; + +export const SectionRight = styled(Section)<{$shouldScroll: boolean}>` + background-color: ${({theme}) => theme.color.white}; + box-shadow: 0 20px 24px rgba(153, 155, 168, 0.18); + overflow-y: ${({$shouldScroll}) => ($shouldScroll ? 'scroll' : 'hidden')}; + z-index: 2; +`; + export const VisualizationContainer = styled.div` height: calc(100% - 52px); position: relative; diff --git a/web/src/components/RunDetailTrace/RunDetailTrace.tsx b/web/src/components/RunDetailTrace/RunDetailTrace.tsx index af9ae3e777..e443f4d597 100644 --- a/web/src/components/RunDetailTrace/RunDetailTrace.tsx +++ b/web/src/components/RunDetailTrace/RunDetailTrace.tsx @@ -3,13 +3,16 @@ import {useNavigate} from 'react-router-dom'; import {useAppSelector} from 'redux/hooks'; import Drawer from 'components/Drawer'; import SpanDetail from 'components/SpanDetail'; +import SkeletonResponse from 'components/RunDetailTriggerResponse/SkeletonResponse'; import Switch from 'components/Visualization/components/Switch'; import {TestState} from 'constants/TestRun.constants'; -import TestRun from 'models/TestRun.model'; +import TestRun, {isRunStateFinished} from 'models/TestRun.model'; +import Trace from 'models/Trace.model'; import TestRunEvent from 'models/TestRunEvent.model'; import SpanSelectors from 'selectors/Span.selectors'; import TraceSelectors from 'selectors/Trace.selectors'; import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service'; +import LintResults from './LintResults'; import * as S from './RunDetailTrace.styled'; import Search from './Search'; import Visualization from './Visualization'; @@ -43,31 +46,41 @@ const RunDetailTrace = ({run, runEvents, testId}: IProps) => { } rightPanel={ - - - - + + + + + - - - {run.state === TestState.FINISHED && ( - { - TraceAnalyticsService.onSwitchDiagramView(type); - setVisualizationType(type); - }} - type={visualizationType} - /> - )} - - - - + + + {run.state === TestState.FINISHED && ( + { + TraceAnalyticsService.onSwitchDiagramView(type); + setVisualizationType(type); + }} + type={visualizationType} + /> + )} + + + + + + + {isRunStateFinished(run.state) ? ( + + ) : ( + + )} + + } /> diff --git a/web/src/components/Settings/Linter/Linter.tsx b/web/src/components/Settings/Linter/Linter.tsx new file mode 100644 index 0000000000..04bd134ace --- /dev/null +++ b/web/src/components/Settings/Linter/Linter.tsx @@ -0,0 +1,34 @@ +import {DISCORD_URL, OCTOLIINT_ISSUE_URL} from 'constants/Common.constants'; +import LinterForm from './LinterForm'; +import * as S from '../common/Settings.styled'; + +const Linter = () => ( + + +

+ This beta release of the Tracetest Linter its a plugin based framework used to analyze Open Telemetry traces to + help teams improve their instrumentation data, find potential problems and provide tips to fix the problems. We + have released this initial version to get feedback from the community. Have thoughts about how to improve the + Tracetest Linter? Add to this + Issue or Discord! Currently, the linter supports the + following plugins: +

+ +
  • + Open Telemetry Semantic Conventions. Enforce standards for spans and attributes +
  • +
  • + Security. Enforce security for spans and attributes +
  • +
  • + Common problems. Helps you find common problems with your application +
  • +
    +
    + + + +
    +); + +export default Linter; diff --git a/web/src/components/Settings/Linter/LinterForm.tsx b/web/src/components/Settings/Linter/LinterForm.tsx new file mode 100644 index 0000000000..8f565d5e25 --- /dev/null +++ b/web/src/components/Settings/Linter/LinterForm.tsx @@ -0,0 +1,71 @@ +import {Button, Form, Input, Switch} from 'antd'; +import {useEffect} from 'react'; + +import {useSettings} from 'providers/Settings/Settings.provider'; +import {useSettingsValues} from 'providers/SettingsValues/SettingsValues.provider'; +import SettingService from 'services/Setting.service'; +import {ResourceType, TDraftLinter} from 'types/Settings.types'; +// import Plugin from './Plugin'; +import * as S from '../common/Settings.styled'; + +const FORM_ID = 'linter'; + +const LinterForm = () => { + const [form] = Form.useForm(); + const {isLoading, onSubmit} = useSettings(); + const {linter} = useSettingsValues(); + + useEffect(() => { + form.resetFields(); + }, [form, linter]); + + const handleOnSubmit = (values: TDraftLinter) => { + values.minimumScore = parseInt(String(values?.minimumScore ?? 0), 10); + onSubmit([SettingService.getDraftResource(ResourceType.LinterType, values)]); + }; + + return ( + + autoComplete="off" + form={form} + initialValues={linter} + layout="vertical" + name={FORM_ID} + onFinish={handleOnSubmit} + > +