diff --git a/chart-sync/go.mod b/chart-sync/go.mod index 8e87d039c..dc036e0eb 100644 --- a/chart-sync/go.mod +++ b/chart-sync/go.mod @@ -5,7 +5,7 @@ go 1.22.4 toolchain go1.22.6 replace ( - github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea + github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 helm.sh/helm/v3 v3.14.3 => github.com/devtron-labs/helm/v3 v3.14.1-0.20240401080259-90238cf69e42 ) diff --git a/chart-sync/go.sum b/chart-sync/go.sum index f7733a6f2..e711c1498 100644 --- a/chart-sync/go.sum +++ b/chart-sync/go.sum @@ -54,8 +54,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/devtron-labs/helm/v3 v3.14.1-0.20240401080259-90238cf69e42 h1:pJmK44QaSztOiZe0iQHNf0sdy5KwkAeceydyhOG4RaY= github.com/devtron-labs/helm/v3 v3.14.1-0.20240401080259-90238cf69e42/go.mod h1:v6myVbyseSBJTzhmeE39UcPLNv6cQK6qss3dvgAySaE= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/chart-sync/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/chart-sync/vendor/modules.txt b/chart-sync/vendor/modules.txt index 8486d7228..060edd2de 100644 --- a/chart-sync/vendor/modules.txt +++ b/chart-sync/vendor/modules.txt @@ -93,7 +93,7 @@ github.com/containerd/platforms # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/fetchAllEnv github.com/devtron-labs/common-lib/git-manager/util @@ -790,4 +790,4 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.3.0 ## explicit; go 1.12 sigs.k8s.io/yaml -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/ci-runner/Dockerfile b/ci-runner/Dockerfile index d516493ec..c33563bb9 100644 --- a/ci-runner/Dockerfile +++ b/ci-runner/Dockerfile @@ -18,7 +18,7 @@ FROM docker:20.10.24-dind RUN apk update && apk add --no-cache --virtual .build-deps && apk add bash && apk add make && apk add curl && apk add git && apk add zip && apk add jq && \ ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime && \ apk -Uuv add groff less python3 py3-pip && \ - pip3 install awscli && \ + pip3 install awscli==1.38.11 && \ apk --purge -v del py-pip && \ rm /var/cache/apk/* diff --git a/ci-runner/go.mod b/ci-runner/go.mod index 492785b93..1d0da664e 100644 --- a/ci-runner/go.mod +++ b/ci-runner/go.mod @@ -4,7 +4,7 @@ go 1.21 toolchain go1.21.8 -replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 require ( github.com/Knetic/govaluate v3.0.0+incompatible diff --git a/ci-runner/go.sum b/ci-runner/go.sum index efb262a73..b695d2900 100644 --- a/ci-runner/go.sum +++ b/ci-runner/go.sum @@ -95,8 +95,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= diff --git a/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/AwsS3Blob.go b/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/AwsS3Blob.go index 102cf2eed..5d8a3a12e 100644 --- a/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/AwsS3Blob.go +++ b/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/AwsS3Blob.go @@ -203,31 +203,55 @@ func (r *Resolver) ResolveEndpoint(_ context.Context, params s3v2.EndpointParame return transport.Endpoint{URI: u}, nil } -func GetS3BucketBasicsClient(ctx context.Context, region string, accessKey, secretKey string, endpointUrl string) (BucketBasics, error) { - cfg, err := config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentialsv2.NewStaticCredentialsProvider(accessKey, secretKey, ""))) - if err != nil { - return BucketBasics{}, err +func getS3DefaultSDKConfig(ctx context.Context, region, accessKey, secretKey, endpointUrl string) (s3Cfg awsv2.Config, err error) { + if len(endpointUrl) != 0 && len(region) == 0 { + // case handled for minio + region = "us-east-1" + s3Cfg = awsv2.Config{Region: region} + return s3Cfg, nil } - sdkConfig := awsv2.Config{Region: region} - sdkConfig.Credentials = cfg.Credentials - var s3Client *s3v2.Client - if len(endpointUrl) > 0 { - if len(region) == 0 { - region = "us-east-1" //for minio - sdkConfig = awsv2.Config{Region: region} + var cfg awsv2.Config + if len(accessKey) == 0 || len(secretKey) == 0 { + // case handled for S3 IAM role + cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return awsv2.Config{}, err } - endpointURL, err := url.Parse(endpointUrl) + } else { + // case handled for S3 with access key and secret key + cfg, err = config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentialsv2.NewStaticCredentialsProvider(accessKey, secretKey, ""))) + if err != nil { + return awsv2.Config{}, err + } + } + s3Cfg = awsv2.Config{Region: region, Credentials: cfg.Credentials} + return s3Cfg, nil +} + +func getS3Client(s3Cfg awsv2.Config, endpointUrl string) (s3Client *s3v2.Client, err error) { + if len(endpointUrl) > 0 { + parsedEndpointUrl, err := url.Parse(endpointUrl) if err != nil { - return BucketBasics{}, err + return s3Client, err } - s3Client = s3v2.NewFromConfig(sdkConfig, func(o *s3v2.Options) { + return s3v2.NewFromConfig(s3Cfg, func(o *s3v2.Options) { o.UsePathStyle = true - o.EndpointResolverV2 = &Resolver{URL: endpointURL} - }) + o.EndpointResolverV2 = &Resolver{URL: parsedEndpointUrl} + }), nil } else { - s3Client = s3v2.NewFromConfig(sdkConfig) + return s3v2.NewFromConfig(s3Cfg), nil } +} +func GetS3BucketBasicsClient(ctx context.Context, region, accessKey, secretKey, endpointUrl string) (BucketBasics, error) { + s3Cfg, err := getS3DefaultSDKConfig(ctx, region, accessKey, secretKey, endpointUrl) + if err != nil { + return BucketBasics{}, err + } + s3Client, err := getS3Client(s3Cfg, endpointUrl) + if err != nil { + return BucketBasics{}, err + } bucketBasics := BucketBasics{S3Client: s3Client} return bucketBasics, nil } diff --git a/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/BlobUtils.go b/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/BlobUtils.go index 1581edb33..c9879dc6f 100644 --- a/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/BlobUtils.go +++ b/ci-runner/vendor/github.com/devtron-labs/common-lib/blob-storage/BlobUtils.go @@ -22,11 +22,25 @@ import ( "os/exec" ) +const ( + WhenSupported = "when_supported" + WhenRequired = "when_required" +) + func setAWSEnvironmentVariables(s3Config *AwsS3BaseConfig, command *exec.Cmd) { if s3Config.AccessKey != "" && s3Config.Passkey != "" { - command.Env = append(os.Environ(), + command.Env = os.Environ() + command.Env = append(command.Env, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", s3Config.AccessKey), fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", s3Config.Passkey), ) } + if s3Config.EndpointUrl != "" { + command.Env = append(command.Env, + // The below is required for https://github.com/aws/aws-cli/issues/9214 + // This is only required for secure endpoints only https://github.com/boto/boto3/issues/4398#issuecomment-2712259341 + fmt.Sprintf("AWS_REQUEST_CHECKSUM_CALCULATION=%s", WhenRequired), + fmt.Sprintf("AWS_RESPONSE_CHECKSUM_VALIDATION=%s", WhenRequired), + ) + } } diff --git a/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/ci-runner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/ci-runner/vendor/modules.txt b/ci-runner/vendor/modules.txt index 65cd46d1d..e9d02581f 100644 --- a/ci-runner/vendor/modules.txt +++ b/ci-runner/vendor/modules.txt @@ -248,7 +248,7 @@ github.com/cespare/xxhash/v2 # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/devtron-labs/common-lib v0.19.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.19.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/blob-storage github.com/devtron-labs/common-lib/constants @@ -991,4 +991,4 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.3.0 ## explicit; go 1.12 sigs.k8s.io/yaml -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/common-lib/blob-storage/AwsS3Blob.go b/common-lib/blob-storage/AwsS3Blob.go index 102cf2eed..5d8a3a12e 100644 --- a/common-lib/blob-storage/AwsS3Blob.go +++ b/common-lib/blob-storage/AwsS3Blob.go @@ -203,31 +203,55 @@ func (r *Resolver) ResolveEndpoint(_ context.Context, params s3v2.EndpointParame return transport.Endpoint{URI: u}, nil } -func GetS3BucketBasicsClient(ctx context.Context, region string, accessKey, secretKey string, endpointUrl string) (BucketBasics, error) { - cfg, err := config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentialsv2.NewStaticCredentialsProvider(accessKey, secretKey, ""))) - if err != nil { - return BucketBasics{}, err +func getS3DefaultSDKConfig(ctx context.Context, region, accessKey, secretKey, endpointUrl string) (s3Cfg awsv2.Config, err error) { + if len(endpointUrl) != 0 && len(region) == 0 { + // case handled for minio + region = "us-east-1" + s3Cfg = awsv2.Config{Region: region} + return s3Cfg, nil } - sdkConfig := awsv2.Config{Region: region} - sdkConfig.Credentials = cfg.Credentials - var s3Client *s3v2.Client - if len(endpointUrl) > 0 { - if len(region) == 0 { - region = "us-east-1" //for minio - sdkConfig = awsv2.Config{Region: region} + var cfg awsv2.Config + if len(accessKey) == 0 || len(secretKey) == 0 { + // case handled for S3 IAM role + cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return awsv2.Config{}, err } - endpointURL, err := url.Parse(endpointUrl) + } else { + // case handled for S3 with access key and secret key + cfg, err = config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentialsv2.NewStaticCredentialsProvider(accessKey, secretKey, ""))) + if err != nil { + return awsv2.Config{}, err + } + } + s3Cfg = awsv2.Config{Region: region, Credentials: cfg.Credentials} + return s3Cfg, nil +} + +func getS3Client(s3Cfg awsv2.Config, endpointUrl string) (s3Client *s3v2.Client, err error) { + if len(endpointUrl) > 0 { + parsedEndpointUrl, err := url.Parse(endpointUrl) if err != nil { - return BucketBasics{}, err + return s3Client, err } - s3Client = s3v2.NewFromConfig(sdkConfig, func(o *s3v2.Options) { + return s3v2.NewFromConfig(s3Cfg, func(o *s3v2.Options) { o.UsePathStyle = true - o.EndpointResolverV2 = &Resolver{URL: endpointURL} - }) + o.EndpointResolverV2 = &Resolver{URL: parsedEndpointUrl} + }), nil } else { - s3Client = s3v2.NewFromConfig(sdkConfig) + return s3v2.NewFromConfig(s3Cfg), nil } +} +func GetS3BucketBasicsClient(ctx context.Context, region, accessKey, secretKey, endpointUrl string) (BucketBasics, error) { + s3Cfg, err := getS3DefaultSDKConfig(ctx, region, accessKey, secretKey, endpointUrl) + if err != nil { + return BucketBasics{}, err + } + s3Client, err := getS3Client(s3Cfg, endpointUrl) + if err != nil { + return BucketBasics{}, err + } bucketBasics := BucketBasics{S3Client: s3Client} return bucketBasics, nil } diff --git a/common-lib/blob-storage/BlobUtils.go b/common-lib/blob-storage/BlobUtils.go index 1581edb33..c9879dc6f 100644 --- a/common-lib/blob-storage/BlobUtils.go +++ b/common-lib/blob-storage/BlobUtils.go @@ -22,11 +22,25 @@ import ( "os/exec" ) +const ( + WhenSupported = "when_supported" + WhenRequired = "when_required" +) + func setAWSEnvironmentVariables(s3Config *AwsS3BaseConfig, command *exec.Cmd) { if s3Config.AccessKey != "" && s3Config.Passkey != "" { - command.Env = append(os.Environ(), + command.Env = os.Environ() + command.Env = append(command.Env, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", s3Config.AccessKey), fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", s3Config.Passkey), ) } + if s3Config.EndpointUrl != "" { + command.Env = append(command.Env, + // The below is required for https://github.com/aws/aws-cli/issues/9214 + // This is only required for secure endpoints only https://github.com/boto/boto3/issues/4398#issuecomment-2712259341 + fmt.Sprintf("AWS_REQUEST_CHECKSUM_CALCULATION=%s", WhenRequired), + fmt.Sprintf("AWS_RESPONSE_CHECKSUM_VALIDATION=%s", WhenRequired), + ) + } } diff --git a/common-lib/utils/CommonUtils.go b/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/common-lib/utils/CommonUtils.go +++ b/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/common-lib/utils/SqlUtil.go b/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/common-lib/utils/bean/bean.go b/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/common-lib/utils/bean/bean.go +++ b/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/common-lib/utils/sql/UtilStructs.go b/common-lib/utils/sql/AuditLog.go similarity index 100% rename from common-lib/utils/sql/UtilStructs.go rename to common-lib/utils/sql/AuditLog.go diff --git a/git-sensor/go.mod b/git-sensor/go.mod index 2981cea8d..76a283425 100644 --- a/git-sensor/go.mod +++ b/git-sensor/go.mod @@ -4,7 +4,7 @@ go 1.21 toolchain go1.22.4 -replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 require ( github.com/caarlos0/env v3.5.0+incompatible diff --git a/git-sensor/go.sum b/git-sensor/go.sum index 40a2e1106..463442be5 100644 --- a/git-sensor/go.sum +++ b/git-sensor/go.sum @@ -27,8 +27,8 @@ github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/devtron-labs/protos v0.0.3-0.20250323220609-ecf8a0f7305e h1:U6UdYbW8a7xn5IzFPd8cywjVVPfutGJCudjePAfL/Hs= github.com/devtron-labs/protos v0.0.3-0.20250323220609-ecf8a0f7305e/go.mod h1:1TqULGlTey+VNhAu/ag7NJuUvByJemkqodsc9L5PHJk= github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= diff --git a/git-sensor/internals/sql/WebhookEventDataMappingRepository.go b/git-sensor/internals/sql/WebhookEventDataMappingRepository.go index 4923541a6..a7b6bf94b 100644 --- a/git-sensor/internals/sql/WebhookEventDataMappingRepository.go +++ b/git-sensor/internals/sql/WebhookEventDataMappingRepository.go @@ -17,7 +17,6 @@ package sql import ( - "github.com/devtron-labs/git-sensor/util" "github.com/go-pg/pg" "github.com/go-pg/pg/orm" "time" @@ -61,15 +60,7 @@ func (impl WebhookEventDataMappingRepositoryImpl) GetCiPipelineMaterialWebhookDa Where("webhook_data_id =? ", webhookParsedDataId). Where("is_active = TRUE "). Select() - - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - - return &mapping, nil + return &mapping, err } func (impl WebhookEventDataMappingRepositoryImpl) SaveCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialWebhookDataMapping *CiPipelineMaterialWebhookDataMapping) error { @@ -89,15 +80,7 @@ func (impl WebhookEventDataMappingRepositoryImpl) GetMatchedCiPipelineMaterialWe Where("is_active = TRUE "). Where("condition_matched = TRUE "). Select() - - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - - return pipelineMaterials, nil + return pipelineMaterials, err } func (impl WebhookEventDataMappingRepositoryImpl) InactivateWebhookDataMappingForPipelineMaterials(ciPipelineMaterialIds []int) error { @@ -127,15 +110,7 @@ func (impl WebhookEventDataMappingRepositoryImpl) GetWebhookPayloadDataForPipeli Offset(offset). Order("updated_on " + sortOrder). Select() - - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - - return mappings, nil + return mappings, err } func (impl WebhookEventDataMappingRepositoryImpl) GetWebhookPayloadFilterDataForPipelineMaterialId(ciPipelineMaterialId int, webhookParsedDataId int) (*CiPipelineMaterialWebhookDataMapping, error) { @@ -149,13 +124,5 @@ func (impl WebhookEventDataMappingRepositoryImpl) GetWebhookPayloadFilterDataFor Where("webhook_data_id =? ", webhookParsedDataId). Where("is_active = TRUE "). Select() - - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - - return &mapping, nil + return &mapping, err } diff --git a/git-sensor/internals/sql/WebhookEventParsedDataRepository.go b/git-sensor/internals/sql/WebhookEventParsedDataRepository.go index 609b653c3..51890dab6 100644 --- a/git-sensor/internals/sql/WebhookEventParsedDataRepository.go +++ b/git-sensor/internals/sql/WebhookEventParsedDataRepository.go @@ -17,7 +17,6 @@ package sql import ( - "github.com/devtron-labs/git-sensor/util" "github.com/go-pg/pg" "time" ) @@ -54,13 +53,7 @@ func NewWebhookEventParsedDataRepositoryImpl(dbConnection *pg.DB) *WebhookEventP func (impl WebhookEventParsedDataRepositoryImpl) GetWebhookParsedEventDataByEventIdAndUniqueId(eventId int, uniqueId string) (*WebhookEventParsedData, error) { var webhookEventParsedData WebhookEventParsedData err := impl.dbConnection.Model(&webhookEventParsedData).Where("event_id =? ", eventId).Where("unique_id =? ", uniqueId).Select() - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - return &webhookEventParsedData, nil + return &webhookEventParsedData, err } func (impl WebhookEventParsedDataRepositoryImpl) SaveWebhookParsedEventData(webhookEventParsedData *WebhookEventParsedData) error { @@ -88,12 +81,5 @@ func (impl WebhookEventParsedDataRepositoryImpl) GetWebhookEventParsedDataById(i err := impl.dbConnection.Model(&webhookEventParsedData). Where("id = ? ", id). Select() - - if err != nil { - if util.IsErrNoRows(err) { - return nil, nil - } - return nil, err - } - return &webhookEventParsedData, nil + return &webhookEventParsedData, err } diff --git a/git-sensor/internals/sql/mocks/WebhookEventDataMappingRepository.go b/git-sensor/internals/sql/mocks/WebhookEventDataMappingRepository.go new file mode 100644 index 000000000..79701b03a --- /dev/null +++ b/git-sensor/internals/sql/mocks/WebhookEventDataMappingRepository.go @@ -0,0 +1,201 @@ +// Code generated by mockery v2.42.0. DO NOT EDIT. + +package mocks + +import ( + sql "github.com/devtron-labs/git-sensor/internals/sql" + mock "github.com/stretchr/testify/mock" +) + +// WebhookEventDataMappingRepository is an autogenerated mock type for the WebhookEventDataMappingRepository type +type WebhookEventDataMappingRepository struct { + mock.Mock +} + +// GetCiPipelineMaterialWebhookDataMapping provides a mock function with given fields: ciPipelineMaterialId, webhookParsedDataId +func (_m *WebhookEventDataMappingRepository) GetCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialId int, webhookParsedDataId int) (*sql.CiPipelineMaterialWebhookDataMapping, error) { + ret := _m.Called(ciPipelineMaterialId, webhookParsedDataId) + + if len(ret) == 0 { + panic("no return value specified for GetCiPipelineMaterialWebhookDataMapping") + } + + var r0 *sql.CiPipelineMaterialWebhookDataMapping + var r1 error + if rf, ok := ret.Get(0).(func(int, int) (*sql.CiPipelineMaterialWebhookDataMapping, error)); ok { + return rf(ciPipelineMaterialId, webhookParsedDataId) + } + if rf, ok := ret.Get(0).(func(int, int) *sql.CiPipelineMaterialWebhookDataMapping); ok { + r0 = rf(ciPipelineMaterialId, webhookParsedDataId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.CiPipelineMaterialWebhookDataMapping) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(ciPipelineMaterialId, webhookParsedDataId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMatchedCiPipelineMaterialWebhookDataMappingForPipelineMaterial provides a mock function with given fields: ciPipelineMaterialId +func (_m *WebhookEventDataMappingRepository) GetMatchedCiPipelineMaterialWebhookDataMappingForPipelineMaterial(ciPipelineMaterialId int) ([]*sql.CiPipelineMaterialWebhookDataMapping, error) { + ret := _m.Called(ciPipelineMaterialId) + + if len(ret) == 0 { + panic("no return value specified for GetMatchedCiPipelineMaterialWebhookDataMappingForPipelineMaterial") + } + + var r0 []*sql.CiPipelineMaterialWebhookDataMapping + var r1 error + if rf, ok := ret.Get(0).(func(int) ([]*sql.CiPipelineMaterialWebhookDataMapping, error)); ok { + return rf(ciPipelineMaterialId) + } + if rf, ok := ret.Get(0).(func(int) []*sql.CiPipelineMaterialWebhookDataMapping); ok { + r0 = rf(ciPipelineMaterialId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.CiPipelineMaterialWebhookDataMapping) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(ciPipelineMaterialId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetWebhookPayloadDataForPipelineMaterialId provides a mock function with given fields: ciPipelineMaterialId, limit, offset, eventTimeSortOrder +func (_m *WebhookEventDataMappingRepository) GetWebhookPayloadDataForPipelineMaterialId(ciPipelineMaterialId int, limit int, offset int, eventTimeSortOrder string) ([]*sql.CiPipelineMaterialWebhookDataMapping, error) { + ret := _m.Called(ciPipelineMaterialId, limit, offset, eventTimeSortOrder) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookPayloadDataForPipelineMaterialId") + } + + var r0 []*sql.CiPipelineMaterialWebhookDataMapping + var r1 error + if rf, ok := ret.Get(0).(func(int, int, int, string) ([]*sql.CiPipelineMaterialWebhookDataMapping, error)); ok { + return rf(ciPipelineMaterialId, limit, offset, eventTimeSortOrder) + } + if rf, ok := ret.Get(0).(func(int, int, int, string) []*sql.CiPipelineMaterialWebhookDataMapping); ok { + r0 = rf(ciPipelineMaterialId, limit, offset, eventTimeSortOrder) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.CiPipelineMaterialWebhookDataMapping) + } + } + + if rf, ok := ret.Get(1).(func(int, int, int, string) error); ok { + r1 = rf(ciPipelineMaterialId, limit, offset, eventTimeSortOrder) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetWebhookPayloadFilterDataForPipelineMaterialId provides a mock function with given fields: ciPipelineMaterialId, webhookParsedDataId +func (_m *WebhookEventDataMappingRepository) GetWebhookPayloadFilterDataForPipelineMaterialId(ciPipelineMaterialId int, webhookParsedDataId int) (*sql.CiPipelineMaterialWebhookDataMapping, error) { + ret := _m.Called(ciPipelineMaterialId, webhookParsedDataId) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookPayloadFilterDataForPipelineMaterialId") + } + + var r0 *sql.CiPipelineMaterialWebhookDataMapping + var r1 error + if rf, ok := ret.Get(0).(func(int, int) (*sql.CiPipelineMaterialWebhookDataMapping, error)); ok { + return rf(ciPipelineMaterialId, webhookParsedDataId) + } + if rf, ok := ret.Get(0).(func(int, int) *sql.CiPipelineMaterialWebhookDataMapping); ok { + r0 = rf(ciPipelineMaterialId, webhookParsedDataId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.CiPipelineMaterialWebhookDataMapping) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(ciPipelineMaterialId, webhookParsedDataId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InactivateWebhookDataMappingForPipelineMaterials provides a mock function with given fields: ciPipelineMaterialIds +func (_m *WebhookEventDataMappingRepository) InactivateWebhookDataMappingForPipelineMaterials(ciPipelineMaterialIds []int) error { + ret := _m.Called(ciPipelineMaterialIds) + + if len(ret) == 0 { + panic("no return value specified for InactivateWebhookDataMappingForPipelineMaterials") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]int) error); ok { + r0 = rf(ciPipelineMaterialIds) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SaveCiPipelineMaterialWebhookDataMapping provides a mock function with given fields: ciPipelineMaterialWebhookDataMapping +func (_m *WebhookEventDataMappingRepository) SaveCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialWebhookDataMapping *sql.CiPipelineMaterialWebhookDataMapping) error { + ret := _m.Called(ciPipelineMaterialWebhookDataMapping) + + if len(ret) == 0 { + panic("no return value specified for SaveCiPipelineMaterialWebhookDataMapping") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.CiPipelineMaterialWebhookDataMapping) error); ok { + r0 = rf(ciPipelineMaterialWebhookDataMapping) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateCiPipelineMaterialWebhookDataMapping provides a mock function with given fields: ciPipelineMaterialWebhookDataMapping +func (_m *WebhookEventDataMappingRepository) UpdateCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialWebhookDataMapping *sql.CiPipelineMaterialWebhookDataMapping) error { + ret := _m.Called(ciPipelineMaterialWebhookDataMapping) + + if len(ret) == 0 { + panic("no return value specified for UpdateCiPipelineMaterialWebhookDataMapping") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.CiPipelineMaterialWebhookDataMapping) error); ok { + r0 = rf(ciPipelineMaterialWebhookDataMapping) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewWebhookEventDataMappingRepository creates a new instance of WebhookEventDataMappingRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewWebhookEventDataMappingRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *WebhookEventDataMappingRepository { + mock := &WebhookEventDataMappingRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/git-sensor/internals/sql/mocks/WebhookEventParsedDataRepository.go b/git-sensor/internals/sql/mocks/WebhookEventParsedDataRepository.go new file mode 100644 index 000000000..7500db93c --- /dev/null +++ b/git-sensor/internals/sql/mocks/WebhookEventParsedDataRepository.go @@ -0,0 +1,153 @@ +// Code generated by mockery v2.42.0. DO NOT EDIT. + +package mocks + +import ( + sql "github.com/devtron-labs/git-sensor/internals/sql" + mock "github.com/stretchr/testify/mock" +) + +// WebhookEventParsedDataRepository is an autogenerated mock type for the WebhookEventParsedDataRepository type +type WebhookEventParsedDataRepository struct { + mock.Mock +} + +// GetWebhookEventParsedDataById provides a mock function with given fields: id +func (_m *WebhookEventParsedDataRepository) GetWebhookEventParsedDataById(id int) (*sql.WebhookEventParsedData, error) { + ret := _m.Called(id) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookEventParsedDataById") + } + + var r0 *sql.WebhookEventParsedData + var r1 error + if rf, ok := ret.Get(0).(func(int) (*sql.WebhookEventParsedData, error)); ok { + return rf(id) + } + if rf, ok := ret.Get(0).(func(int) *sql.WebhookEventParsedData); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.WebhookEventParsedData) + } + } + + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetWebhookEventParsedDataByIds provides a mock function with given fields: ids, limit +func (_m *WebhookEventParsedDataRepository) GetWebhookEventParsedDataByIds(ids []int, limit int) ([]*sql.WebhookEventParsedData, error) { + ret := _m.Called(ids, limit) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookEventParsedDataByIds") + } + + var r0 []*sql.WebhookEventParsedData + var r1 error + if rf, ok := ret.Get(0).(func([]int, int) ([]*sql.WebhookEventParsedData, error)); ok { + return rf(ids, limit) + } + if rf, ok := ret.Get(0).(func([]int, int) []*sql.WebhookEventParsedData); ok { + r0 = rf(ids, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.WebhookEventParsedData) + } + } + + if rf, ok := ret.Get(1).(func([]int, int) error); ok { + r1 = rf(ids, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetWebhookParsedEventDataByEventIdAndUniqueId provides a mock function with given fields: eventId, uniqueId +func (_m *WebhookEventParsedDataRepository) GetWebhookParsedEventDataByEventIdAndUniqueId(eventId int, uniqueId string) (*sql.WebhookEventParsedData, error) { + ret := _m.Called(eventId, uniqueId) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookParsedEventDataByEventIdAndUniqueId") + } + + var r0 *sql.WebhookEventParsedData + var r1 error + if rf, ok := ret.Get(0).(func(int, string) (*sql.WebhookEventParsedData, error)); ok { + return rf(eventId, uniqueId) + } + if rf, ok := ret.Get(0).(func(int, string) *sql.WebhookEventParsedData); ok { + r0 = rf(eventId, uniqueId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.WebhookEventParsedData) + } + } + + if rf, ok := ret.Get(1).(func(int, string) error); ok { + r1 = rf(eventId, uniqueId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SaveWebhookParsedEventData provides a mock function with given fields: webhookEventParsedData +func (_m *WebhookEventParsedDataRepository) SaveWebhookParsedEventData(webhookEventParsedData *sql.WebhookEventParsedData) error { + ret := _m.Called(webhookEventParsedData) + + if len(ret) == 0 { + panic("no return value specified for SaveWebhookParsedEventData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.WebhookEventParsedData) error); ok { + r0 = rf(webhookEventParsedData) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateWebhookParsedEventData provides a mock function with given fields: webhookEventParsedData +func (_m *WebhookEventParsedDataRepository) UpdateWebhookParsedEventData(webhookEventParsedData *sql.WebhookEventParsedData) error { + ret := _m.Called(webhookEventParsedData) + + if len(ret) == 0 { + panic("no return value specified for UpdateWebhookParsedEventData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.WebhookEventParsedData) error); ok { + r0 = rf(webhookEventParsedData) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewWebhookEventParsedDataRepository creates a new instance of WebhookEventParsedDataRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewWebhookEventParsedDataRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *WebhookEventParsedDataRepository { + mock := &WebhookEventParsedDataRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/git-sensor/pkg/RepoManages.go b/git-sensor/pkg/RepoManages.go index 9f73f4674..4aa005734 100644 --- a/git-sensor/pkg/RepoManages.go +++ b/git-sensor/pkg/RepoManages.go @@ -899,28 +899,28 @@ func (impl RepoManagerImpl) GetWebhookAndCiDataById(id int, ciPipelineMaterialId impl.logger.Debugw("Getting webhook data ", "id", id) webhookDataFromDb, err := impl.webhookEventParsedDataRepository.GetWebhookEventParsedDataById(id) - - if err != nil { - impl.logger.Errorw("error in getting webhook data for Id ", "Id", id, "err", err) - return nil, err - } - - filterData, err := impl.webhookEventDataMappingRepository.GetWebhookPayloadFilterDataForPipelineMaterialId(ciPipelineMaterialId, id) if err != nil { - impl.logger.Errorw("error in getting webhook payload filter data for webhookParsedId ", "Id", id, "err", err) + impl.logger.Errorw("error in getting webhook data for id ", "id", id, "err", err) return nil, err } - webhookData := impl.webhookEventBeanConverter.ConvertFromWebhookParsedDataSqlBean(webhookDataFromDb) webhookAndCiData := &git.WebhookAndCiData{ WebhookData: webhookData, } + filterData, err := impl.webhookEventDataMappingRepository.GetWebhookPayloadFilterDataForPipelineMaterialId(ciPipelineMaterialId, id) + if err != nil && !util2.IsErrNoRows(err) { + impl.logger.Errorw("error in getting webhook payload filter data for webhookParsedId ", "ciPipelineMaterialId", ciPipelineMaterialId, "id", id, "err", err) + return nil, err + } else if util2.IsErrNoRows(err) { + impl.logger.Warnw("no webhook payload filter data found for webhookParsedId", "ciPipelineMaterialId", ciPipelineMaterialId, "id", id) + return webhookAndCiData, nil + } + if filterData != nil { webhookAndCiData.ExtraEnvironmentVariables = util.BuildExtraEnvironmentVariablesForCi(filterData.FilterResults, webhookDataFromDb.CiEnvVariableData) } - return webhookAndCiData, nil } @@ -1070,18 +1070,25 @@ func (impl RepoManagerImpl) GetWebhookPayloadDataForPipelineMaterialId(request * } func (impl RepoManagerImpl) GetWebhookPayloadFilterDataForPipelineMaterialId(request *git.WebhookPayloadFilterDataRequest) (*git.WebhookPayloadFilterDataResponse, error) { - impl.logger.Debugw("Getting webhook payload filter data ", "request", request) + impl.logger.Debugw("Getting webhook payload filter data", "request", request) - mapping, err := impl.webhookEventDataMappingRepository.GetWebhookPayloadFilterDataForPipelineMaterialId(request.CiPipelineMaterialId, request.ParsedDataId) + parsedData, err := impl.webhookEventParsedDataRepository.GetWebhookEventParsedDataById(request.ParsedDataId) if err != nil { - impl.logger.Errorw("error in getting webhook filter payload data ", "err", err) + impl.logger.Errorw("error in getting parsed webhook data by id", "id", request.ParsedDataId, "err", err) return nil, err } - parsedData, err := impl.webhookEventParsedDataRepository.GetWebhookEventParsedDataById(request.ParsedDataId) - if err != nil { - impl.logger.Errorw("error in getting parsed webhook data ", "err", err) + webhookPayloadFilterDataResponse := &git.WebhookPayloadFilterDataResponse{ + PayloadId: parsedData.PayloadDataId, + } + + mapping, err := impl.webhookEventDataMappingRepository.GetWebhookPayloadFilterDataForPipelineMaterialId(request.CiPipelineMaterialId, request.ParsedDataId) + if err != nil && !util2.IsErrNoRows(err) { + impl.logger.Errorw("error in getting webhook filter payload data", "ciPipelineMaterialId", request.CiPipelineMaterialId, "parsedDataId", request.ParsedDataId, "err", err) return nil, err + } else if util2.IsErrNoRows(err) { + impl.logger.Warnw("no webhook filter payload data found for parsedDataId", "ciPipelineMaterialId", request.CiPipelineMaterialId, "parsedDataId", request.ParsedDataId) + return webhookPayloadFilterDataResponse, nil } filterResults := mapping.FilterResults @@ -1099,11 +1106,7 @@ func (impl RepoManagerImpl) GetWebhookPayloadFilterDataForPipelineMaterialId(req webhookPayloadFilterDataSelectorResponses = append(webhookPayloadFilterDataSelectorResponses, webhookPayloadFilterDataSelectorResponse) } } - - webhookPayloadFilterDataResponse := &git.WebhookPayloadFilterDataResponse{ - PayloadId: parsedData.PayloadDataId, - SelectorsData: webhookPayloadFilterDataSelectorResponses, - } + webhookPayloadFilterDataResponse.SelectorsData = webhookPayloadFilterDataSelectorResponses return webhookPayloadFilterDataResponse, nil } diff --git a/git-sensor/pkg/RepoManages_test.go b/git-sensor/pkg/RepoManages_test.go new file mode 100644 index 000000000..859f0cc2d --- /dev/null +++ b/git-sensor/pkg/RepoManages_test.go @@ -0,0 +1,196 @@ +package pkg + +import ( + "fmt" + "github.com/devtron-labs/git-sensor/internals/logger" + "github.com/devtron-labs/git-sensor/internals/sql" + "github.com/devtron-labs/git-sensor/internals/sql/mocks" + "github.com/devtron-labs/git-sensor/pkg/git" + "github.com/go-pg/pg" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "testing" +) + +func TestRepoManagerImpl_GetWebhookAndCiDataById(t *testing.T) { + type args struct { + id int + ciPipelineMaterialId int + } + tests := []struct { + name string + args args + want *git.WebhookAndCiData + webhookEventParsedDataMockExecution func(*mocks.WebhookEventParsedDataRepository) + webhookEventDataMappingMockExecution func(*mocks.WebhookEventDataMappingRepository) + wantErr assert.ErrorAssertionFunc + }{ + { + name: "WebhookEvent_ParsedData_And_DataMappings_Found", + args: args{ + id: 1, + ciPipelineMaterialId: 1, + }, + want: &git.WebhookAndCiData{ + ExtraEnvironmentVariables: map[string]string{ + "VAR1": "envValue1", + "VAR2": "envValue2", + "VAR3": "filterValue3", + "VAR4": "filterValue4", + }, + WebhookData: &git.WebhookData{ + Id: 1, + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + wantErr: assert.NoError, + webhookEventParsedDataMockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookEventParsedDataById", 1). + Return(&sql.WebhookEventParsedData{ + Id: 1, + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "VAR1": "envValue1", + "VAR2": "envValue2", + }, + }, nil).Once() + }, + webhookEventDataMappingMockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + On("GetWebhookPayloadFilterDataForPipelineMaterialId", 1, 1). + Return(&sql.CiPipelineMaterialWebhookDataMapping{ + Id: 1, + CiPipelineMaterialId: 1, + FilterResults: []*sql.CiPipelineMaterialWebhookDataMappingFilterResult{ + { + MatchedGroups: map[string]string{ + "VAR1": "filterValue1", + "VAR2": "filterValue2", + }, + }, + { + MatchedGroups: map[string]string{ + "VAR3": "filterValue3", + "VAR4": "filterValue4", + }, + }, + }, + }, nil).Once() + }, + }, + { + name: "WebhookEvent_ParsedData_Found_But_DataMappings_NotFound", + args: args{ + id: 1, + ciPipelineMaterialId: 1, + }, + want: &git.WebhookAndCiData{ + WebhookData: &git.WebhookData{ + Id: 1, + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + wantErr: assert.NoError, + webhookEventParsedDataMockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookEventParsedDataById", 1). + Return(&sql.WebhookEventParsedData{ + Id: 1, + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "VAR1": "envValue1", + "VAR2": "envValue2", + }, + }, nil).Once() + }, + webhookEventDataMappingMockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + On("GetWebhookPayloadFilterDataForPipelineMaterialId", 1, 1). + Return(nil, pg.ErrNoRows).Once() + }, + }, + { + name: "WebhookEvent_ParsedData_And_DataMappings_NotFound", + args: args{ + id: 1, + ciPipelineMaterialId: 1, + }, + want: nil, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, pg.ErrNoRows.Error(), msgAndArgs...) + }, + webhookEventParsedDataMockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookEventParsedDataById", 1). + Return(nil, pg.ErrNoRows).Once() + }, + webhookEventDataMappingMockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + AssertNotCalled(t, "GetWebhookPayloadFilterDataForPipelineMaterialId", 1, 1) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + impl := getRepoManagerImpl(t, tt.webhookEventParsedDataMockExecution, tt.webhookEventDataMappingMockExecution) + got, err := impl.GetWebhookAndCiDataById(tt.args.id, tt.args.ciPipelineMaterialId) + if !tt.wantErr(t, err, fmt.Sprintf("GetWebhookAndCiDataById(%v, %v)", tt.args.id, tt.args.ciPipelineMaterialId)) { + return + } + assert.Equalf(t, tt.want, got, "GetWebhookAndCiDataById(%v, %v)", tt.args.id, tt.args.ciPipelineMaterialId) + }) + } +} + +func getRepoManagerImpl( + t *testing.T, + webhookEventParsedDataMockExecution func(*mocks.WebhookEventParsedDataRepository), + webhookEventDataMappingMockExecution func(*mocks.WebhookEventDataMappingRepository), +) *RepoManagerImpl { + sugaredLogger := getLogger() + webhookEventDataMappingRepository := getWebhookEventDataMappingRepository(t) + if webhookEventDataMappingMockExecution != nil { + webhookEventDataMappingMockExecution(webhookEventDataMappingRepository) + } + webhookEventParsedDataRepositoryMock := getWebhookEventParsedDataRepositoryMock(t) + if webhookEventParsedDataMockExecution != nil { + webhookEventParsedDataMockExecution(webhookEventParsedDataRepositoryMock) + } + webhookEventBeanConverterImpl := git.NewWebhookEventBeanConverterImpl() + return NewRepoManagerImpl( + sugaredLogger, + nil, nil, nil, nil, nil, nil, nil, nil, + webhookEventParsedDataRepositoryMock, + webhookEventDataMappingRepository, + webhookEventBeanConverterImpl, nil, nil, + ) +} + +func getLogger() *zap.SugaredLogger { + return logger.NewSugaredLogger() +} + +func getWebhookEventParsedDataRepositoryMock(t *testing.T) *mocks.WebhookEventParsedDataRepository { + return mocks.NewWebhookEventParsedDataRepository(t) +} + +func getWebhookEventDataMappingRepository(t *testing.T) *mocks.WebhookEventDataMappingRepository { + return mocks.NewWebhookEventDataMappingRepository(t) +} diff --git a/git-sensor/pkg/git/Util.go b/git-sensor/pkg/git/Util.go index 7d4101734..2b96e6eee 100644 --- a/git-sensor/pkg/git/Util.go +++ b/git-sensor/pkg/git/Util.go @@ -60,6 +60,7 @@ func GetProjectName(url string) string { url = url[strings.LastIndex(url, "/")+1:] return strings.TrimSuffix(url, ".git") } + func GetCheckoutPath(url string, cloneLocation string) string { //url= https://github.com/devtron-labs/git-sensor.git cloneLocation= git-base/1/github.com/prakash100198 //then this function returns git-base/1/github.com/prakash100198/SampleGoLangProject/.git @@ -83,11 +84,13 @@ func GetUserNamePassword(gitProvider *sql.GitProvider) (userName, password strin return "", "", fmt.Errorf("unsupported %s", gitProvider.AuthMode) } } + func getSSHPrivateKeyFolderAndFilePath(gitProviderId int) (string, string) { sshPrivateKeyFolderPath := path.Join(SSH_PRIVATE_KEY_DIR, strconv.Itoa(gitProviderId)) sshPrivateKeyFilePath := path.Join(sshPrivateKeyFolderPath, SSH_PRIVATE_KEY_FILE_NAME) return sshPrivateKeyFolderPath, sshPrivateKeyFilePath } + func GetOrCreateSshPrivateKeyOnDisk(gitProviderId int, sshPrivateKeyContent string) (privateKeyPath string, err error) { sshPrivateKeyFolderPath, sshPrivateKeyFilePath := getSSHPrivateKeyFolderAndFilePath(gitProviderId) @@ -232,6 +235,7 @@ func processFileStatOutputNameOnly(commitDiff string) (FileStats, error) { return filestat, nil } + func IsRepoShallowCloned(checkoutPath string) bool { return strings.Contains(checkoutPath, "/.git") } @@ -240,3 +244,7 @@ func getTestBaseDir() string { dir, _ := os.UserHomeDir() return path.Join(dir, "/tmp") } + +var ( + ErrWebhookEventParsedDataNotFound = fmt.Errorf("webhook event parsed data not found") +) diff --git a/git-sensor/pkg/git/WebhookEventService.go b/git-sensor/pkg/git/WebhookEventService.go index 8d44fcdfb..40a49d0da 100644 --- a/git-sensor/pkg/git/WebhookEventService.go +++ b/git-sensor/pkg/git/WebhookEventService.go @@ -22,6 +22,7 @@ import ( pubsub "github.com/devtron-labs/common-lib/pubsub-lib" "github.com/devtron-labs/git-sensor/internals/sql" "github.com/devtron-labs/git-sensor/internals/util" + util2 "github.com/devtron-labs/git-sensor/util" _ "github.com/robfig/cron/v3" "go.uber.org/zap" "regexp" @@ -94,17 +95,18 @@ func (impl WebhookEventServiceImpl) GetAllGitHostWebhookEventByGitHostName(gitHo func (impl WebhookEventServiceImpl) GetWebhookParsedEventDataByEventIdAndUniqueId(eventId int, uniqueId string) (*sql.WebhookEventParsedData, error) { impl.logger.Debugw("fetching webhook event parsed data for ", "eventId", eventId, "uniqueId", uniqueId) - if len(uniqueId) == 0 { - return nil, nil + impl.logger.Warn("uniqueId is blank. so skipping fetching webhook event parsed data...") + return nil, ErrWebhookEventParsedDataNotFound } - webhookEventParsedData, err := impl.webhookEventParsedDataRepository.GetWebhookParsedEventDataByEventIdAndUniqueId(eventId, uniqueId) - if err != nil { - impl.logger.Errorw("getting error while fetching webhook event parsed data ", "err", err) + if err != nil && !util2.IsErrNoRows(err) { + impl.logger.Errorw("getting error while fetching webhook event parsed data ", "eventId", eventId, "uniqueId", uniqueId, "err", err) return nil, err + } else if util2.IsErrNoRows(err) { + impl.logger.Warnw("webhook event parsed data not found", "eventId", eventId, "uniqueId", uniqueId) + return nil, ErrWebhookEventParsedDataNotFound } - return webhookEventParsedData, nil } @@ -332,14 +334,20 @@ func (impl WebhookEventServiceImpl) NotifyForAutoCi(material *CiPipelineMaterial return err } -func (impl WebhookEventServiceImpl) HandleMaterialWebhookMappingIntoDb(ciPipelineMaterialId int, webhookParsedDataId int, conditionMatched bool, filterResults []*sql.CiPipelineMaterialWebhookDataMappingFilterResult) error { - impl.logger.Debug("Handling Material webhook mapping into DB") - +func (impl WebhookEventServiceImpl) getCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialId int, webhookParsedDataId int) (*sql.CiPipelineMaterialWebhookDataMapping, bool, error) { + var isNewMapping bool mapping, err := impl.webhookEventDataMappingRepository.GetCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialId, webhookParsedDataId) - if err != nil { - impl.logger.Errorw("err in getting ci-pipeline vs webhook data mapping", "err", err) - return err + if err != nil && !util2.IsErrNoRows(err) { + impl.logger.Errorw("err in getting ci-pipeline vs webhook data mapping", "ciPipelineMaterialId", ciPipelineMaterialId, "webhookParsedDataId", webhookParsedDataId, "err", err) + return mapping, isNewMapping, err + } else if util2.IsErrNoRows(err) { + isNewMapping = true } + return mapping, isNewMapping, nil +} + +func (impl WebhookEventServiceImpl) HandleMaterialWebhookMappingIntoDb(ciPipelineMaterialId int, webhookParsedDataId int, conditionMatched bool, filterResults []*sql.CiPipelineMaterialWebhookDataMappingFilterResult) error { + impl.logger.Debugw("Handling Material webhook mapping into DB", "ciPipelineMaterialId", ciPipelineMaterialId, "webhookParsedDataId", webhookParsedDataId) ciPipelineMaterialWebhookDataMapping := &sql.CiPipelineMaterialWebhookDataMapping{ CiPipelineMaterialId: ciPipelineMaterialId, @@ -349,8 +357,12 @@ func (impl WebhookEventServiceImpl) HandleMaterialWebhookMappingIntoDb(ciPipelin UpdatedOn: time.Now(), } - isNewMapping := mapping == nil - + // get mapping + mapping, isNewMapping, err := impl.getCiPipelineMaterialWebhookDataMapping(ciPipelineMaterialId, webhookParsedDataId) + if err != nil { + impl.logger.Errorw("err in getting ci-pipeline vs webhook data mapping", "ciPipelineMaterialId", ciPipelineMaterialId, "webhookParsedDataId", webhookParsedDataId, "err", err) + return err + } if isNewMapping { // insert into DB impl.logger.Debug("Saving mapping into DB") diff --git a/git-sensor/pkg/git/WebhookEventService_test.go b/git-sensor/pkg/git/WebhookEventService_test.go new file mode 100644 index 000000000..994dc2900 --- /dev/null +++ b/git-sensor/pkg/git/WebhookEventService_test.go @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2020-2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package git + +import ( + "fmt" + "github.com/devtron-labs/git-sensor/internals/logger" + "github.com/devtron-labs/git-sensor/internals/sql" + "github.com/devtron-labs/git-sensor/internals/sql/mocks" + "github.com/go-pg/pg" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "os" + "testing" + "time" +) + +func TestWebhookEventServiceImpl_GetWebhookParsedEventDataByEventIdAndUniqueId(t *testing.T) { + + type args struct { + eventId int + uniqueId string + } + + tests := []struct { + name string + args args + want *sql.WebhookEventParsedData + mockExecution func(*mocks.WebhookEventParsedDataRepository) + wantErr assert.ErrorAssertionFunc + }{ + { + name: "WebhookEventParsedData_Found", + args: args{ + eventId: 1, + uniqueId: "uniqueId", + }, + want: &sql.WebhookEventParsedData{ + Id: 1, + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + CreatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + }, + wantErr: assert.NoError, + mockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(&sql.WebhookEventParsedData{ + Id: 1, + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + CreatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + }, nil).Once() + }, + }, + { + name: "WebhookEventParsedData_NotFound", + args: args{ + eventId: 1, + uniqueId: "uniqueId", + }, + want: nil, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, ErrWebhookEventParsedDataNotFound.Error(), msgAndArgs...) + }, + mockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(nil, pg.ErrNoRows).Once() + }, + }, + { + name: "Empty_UniqueId", + args: args{ + eventId: 1, + uniqueId: "", + }, + want: nil, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, ErrWebhookEventParsedDataNotFound.Error(), msgAndArgs...) + }, + mockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl.AssertNotCalled(t, "GetWebhookParsedEventDataByEventIdAndUniqueId", 2) + }, + }, + { + name: "PG_Timeout_Error", + args: args{ + eventId: 1, + uniqueId: "uniqueId", + }, + want: nil, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, os.ErrDeadlineExceeded.Error(), msgAndArgs...) + }, + mockExecution: func(mockImpl *mocks.WebhookEventParsedDataRepository) { + mockImpl. + On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(nil, os.ErrDeadlineExceeded).Once() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + impl := getWebhookEventServiceImpl(t, tt.mockExecution, nil) + got, err := impl.GetWebhookParsedEventDataByEventIdAndUniqueId(tt.args.eventId, tt.args.uniqueId) + if !tt.wantErr(t, err, fmt.Sprintf("GetWebhookParsedEventDataByEventIdAndUniqueId(%v, %v)", tt.args.eventId, tt.args.uniqueId)) { + return + } + assert.Equalf(t, tt.want, got, "GetWebhookParsedEventDataByEventIdAndUniqueId(%v, %v)", tt.args.eventId, tt.args.uniqueId) + }) + } +} + +func TestWebhookEventServiceImpl_getCiPipelineMaterialWebhookDataMapping(t *testing.T) { + type args struct { + ciPipelineMaterialId int + webhookParsedDataId int + } + tests := []struct { + name string + args args + want *sql.CiPipelineMaterialWebhookDataMapping + isNewEntry bool + mockExecution func(*mocks.WebhookEventDataMappingRepository) + wantErr assert.ErrorAssertionFunc + }{ + { + name: "WebhookEventDataMapping_Found", + args: args{ + ciPipelineMaterialId: 1, + webhookParsedDataId: 1, + }, + want: &sql.CiPipelineMaterialWebhookDataMapping{ + Id: 1, + CiPipelineMaterialId: 1, + WebhookDataId: 1, + ConditionMatched: true, + IsActive: true, + CreatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + }, + isNewEntry: false, + wantErr: assert.NoError, + mockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + On("GetCiPipelineMaterialWebhookDataMapping", 1, 1). + Return(&sql.CiPipelineMaterialWebhookDataMapping{ + Id: 1, + CiPipelineMaterialId: 1, + WebhookDataId: 1, + ConditionMatched: true, + IsActive: true, + CreatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2025, 4, 10, 0, 0, 0, 0, time.UTC), + }, nil).Once() + }, + }, + { + name: "WebhookEventDataMapping_NotFound", + args: args{ + ciPipelineMaterialId: 1, + webhookParsedDataId: 1, + }, + want: nil, + isNewEntry: true, + wantErr: assert.NoError, + mockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + On("GetCiPipelineMaterialWebhookDataMapping", 1, 1). + Return(nil, pg.ErrNoRows).Once() + }, + }, + { + name: "PG_Timeout_Error", + args: args{ + ciPipelineMaterialId: 1, + webhookParsedDataId: 1, + }, + want: nil, + isNewEntry: false, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, os.ErrDeadlineExceeded.Error(), msgAndArgs...) + }, + mockExecution: func(mockImpl *mocks.WebhookEventDataMappingRepository) { + mockImpl. + On("GetCiPipelineMaterialWebhookDataMapping", 1, 1). + Return(nil, os.ErrDeadlineExceeded).Once() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + impl := getWebhookEventServiceImpl(t, nil, tt.mockExecution) + got, isNewMapping, err := impl.getCiPipelineMaterialWebhookDataMapping(tt.args.ciPipelineMaterialId, tt.args.webhookParsedDataId) + if !tt.wantErr(t, err, fmt.Sprintf("getCiPipelineMaterialWebhookDataMapping(%v, %v)", tt.args.ciPipelineMaterialId, tt.args.webhookParsedDataId)) { + return + } + if !tt.wantErr(t, err, fmt.Sprintf("getCiPipelineMaterialWebhookDataMapping(%v, %v)", tt.args.ciPipelineMaterialId, tt.args.webhookParsedDataId)) { + return + } + assert.Equalf(t, tt.isNewEntry, isNewMapping, "getCiPipelineMaterialWebhookDataMapping(%v, %v)", tt.args.ciPipelineMaterialId, tt.args.webhookParsedDataId) + assert.Equalf(t, tt.want, got, "getCiPipelineMaterialWebhookDataMapping(%v, %v)", tt.args.ciPipelineMaterialId, tt.args.webhookParsedDataId) + }) + } +} + +func getWebhookEventServiceImpl( + t *testing.T, + webhookEventParsedDataMockExecution func(*mocks.WebhookEventParsedDataRepository), + webhookEventDataMappingMockExecution func(*mocks.WebhookEventDataMappingRepository), +) *WebhookEventServiceImpl { + sugaredLogger := getLogger() + webhookEventDataMappingRepository := getWebhookEventDataMappingRepository(t) + if webhookEventDataMappingMockExecution != nil { + webhookEventDataMappingMockExecution(webhookEventDataMappingRepository) + } + webhookEventParsedDataRepositoryMock := getWebhookEventParsedDataRepositoryMock(t) + if webhookEventParsedDataMockExecution != nil { + webhookEventParsedDataMockExecution(webhookEventParsedDataRepositoryMock) + } + return NewWebhookEventServiceImpl( + sugaredLogger, + nil, + webhookEventParsedDataRepositoryMock, + webhookEventDataMappingRepository, nil, nil, nil, nil, + ) +} + +func getLogger() *zap.SugaredLogger { + return logger.NewSugaredLogger() +} + +func getWebhookEventParsedDataRepositoryMock(t *testing.T) *mocks.WebhookEventParsedDataRepository { + return mocks.NewWebhookEventParsedDataRepository(t) +} + +func getWebhookEventDataMappingRepository(t *testing.T) *mocks.WebhookEventDataMappingRepository { + return mocks.NewWebhookEventDataMappingRepository(t) +} diff --git a/git-sensor/pkg/git/WebhookHandler.go b/git-sensor/pkg/git/WebhookHandler.go index 19af9cf60..ab4bf726e 100644 --- a/git-sensor/pkg/git/WebhookHandler.go +++ b/git-sensor/pkg/git/WebhookHandler.go @@ -17,8 +17,10 @@ package git import ( + "errors" "github.com/devtron-labs/git-sensor/internals/sql" "go.uber.org/zap" + "slices" "strings" "time" ) @@ -82,7 +84,7 @@ func (impl WebhookHandlerImpl) HandleWebhookEvent(webhookEvent *WebhookEvent) er for _, event := range events { if len(event.EventTypesCsv) > 0 { eventTypes := strings.Split(event.EventTypesCsv, ",") - if !contains(eventTypes, eventType) { + if !slices.Contains(eventTypes, eventType) { continue } } @@ -100,42 +102,43 @@ func (impl WebhookHandlerImpl) HandleWebhookEvent(webhookEvent *WebhookEvent) er webhookEventParsedData.EventId = eventId webhookEventParsedData.EventActionType = event.ActionType webhookEventParsedData.PayloadDataId = payloadId - - // fetch webhook parsed data from DB if unique id is not blank - webhookParsedEventGetData, err := impl.webhookEventService.GetWebhookParsedEventDataByEventIdAndUniqueId(eventId, webhookEventParsedData.UniqueId) - if err != nil { - impl.logger.Errorw("error in getting parsed webhook event data", "err", err) - return err + if dbErr := impl.upsertWebhookEventParsedData(eventId, webhookEventParsedData); dbErr != nil { + impl.logger.Errorw("error in upserting webhook event parsed data", "eventId", eventId, "err", dbErr) + // intentionally not returning error here, as we want to continue processing other events } - - // save or update in DB - if webhookParsedEventGetData != nil { - webhookEventParsedData.Id = webhookParsedEventGetData.Id - webhookEventParsedData.CreatedOn = webhookParsedEventGetData.CreatedOn - webhookEventParsedData.UpdatedOn = time.Now() - impl.webhookEventService.UpdateWebhookParsedEventData(webhookEventParsedData) - } else { - webhookEventParsedData.CreatedOn = time.Now() - impl.webhookEventService.SaveWebhookParsedEventData(webhookEventParsedData) - } - // match ci trigger condition and notify err = impl.webhookEventService.MatchCiTriggerConditionAndNotify(event, webhookEventParsedData, fullDataMap) if err != nil { impl.logger.Errorw("error in matching ci trigger condition for webhook after db save", "err", err) return err } - } - return nil } -func contains(s []string, str string) bool { - for _, v := range s { - if v == str { - return true +func (impl WebhookHandlerImpl) upsertWebhookEventParsedData(eventId int, webhookEventParsedData *sql.WebhookEventParsedData) error { + // fetch webhook parsed data from DB if unique id is not blank + webhookParsedEventGetData, err := impl.webhookEventService.GetWebhookParsedEventDataByEventIdAndUniqueId(eventId, webhookEventParsedData.UniqueId) + if err != nil && !errors.Is(err, ErrWebhookEventParsedDataNotFound) { + impl.logger.Errorw("error in getting parsed webhook event data", "eventId", eventId, "uniqueId", webhookEventParsedData.UniqueId, "err", err) + return err + } else if errors.Is(err, ErrWebhookEventParsedDataNotFound) { + impl.logger.Infow("webhook event parsed data not found in db, creating a new one", "eventId", eventId, "uniqueId", webhookEventParsedData.UniqueId) + // save in DB + webhookEventParsedData.CreatedOn = time.Now() + if err = impl.webhookEventService.SaveWebhookParsedEventData(webhookEventParsedData); err != nil { + impl.logger.Errorw("error in saving webhook event parsed data", "err", err) + return err + } + } else { + // update in DB + webhookEventParsedData.Id = webhookParsedEventGetData.Id + webhookEventParsedData.CreatedOn = webhookParsedEventGetData.CreatedOn + webhookEventParsedData.UpdatedOn = time.Now() + if err = impl.webhookEventService.UpdateWebhookParsedEventData(webhookEventParsedData); err != nil { + impl.logger.Errorw("error in updating webhook event parsed data", "err", err) + return err } } - return false + return nil } diff --git a/git-sensor/pkg/git/WebhookHandler_test.go b/git-sensor/pkg/git/WebhookHandler_test.go new file mode 100644 index 000000000..6434b1620 --- /dev/null +++ b/git-sensor/pkg/git/WebhookHandler_test.go @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2020-2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package git + +import ( + "fmt" + "github.com/devtron-labs/git-sensor/internals/sql" + mocks "github.com/devtron-labs/git-sensor/pkg/git/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "os" + "testing" + "time" +) + +func TestWebhookHandlerImpl_upsertWebhookEventParsedData(t *testing.T) { + type args struct { + eventId int + webhookEventParsedData *sql.WebhookEventParsedData + } + tests := []struct { + name string + args args + mockExecution func(service *mocks.WebhookEventService) + wantErr assert.ErrorAssertionFunc + }{ + { + name: "WebhookEventParsedData_Upsert_Success", + args: args{ + eventId: 1, + webhookEventParsedData: &sql.WebhookEventParsedData{ + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + }, + }, + mockExecution: func(service *mocks.WebhookEventService) { + service.On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(&sql.WebhookEventParsedData{ + Id: 1, + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + }, + CreatedOn: time.Date(2023, time.April, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2023, time.April, 10, 0, 0, 0, 0, time.UTC), + }, nil).Once() + service.On("UpdateWebhookParsedEventData", mock.AnythingOfType("*sql.WebhookEventParsedData")). + Return(nil).Once() + service.AssertNotCalled(t, "SaveWebhookParsedEventData", mock.Anything) + }, + wantErr: assert.NoError, + }, + { + name: "WebhookEventParsedData_Upsert_Error", + args: args{ + eventId: 1, + webhookEventParsedData: &sql.WebhookEventParsedData{ + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + }, + }, + mockExecution: func(service *mocks.WebhookEventService) { + service.On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(&sql.WebhookEventParsedData{ + Id: 1, + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + }, + CreatedOn: time.Date(2023, time.April, 10, 0, 0, 0, 0, time.UTC), + UpdatedOn: time.Date(2023, time.April, 10, 0, 0, 0, 0, time.UTC), + }, nil).Once() + service.On("UpdateWebhookParsedEventData", mock.AnythingOfType("*sql.WebhookEventParsedData")). + Return(os.ErrDeadlineExceeded).Once() + service.AssertNotCalled(t, "SaveWebhookParsedEventData", mock.Anything) + }, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, os.ErrDeadlineExceeded.Error(), msgAndArgs...) + }, + }, + { + name: "WebhookEventParsedData_Save_Success", + args: args{ + eventId: 1, + webhookEventParsedData: &sql.WebhookEventParsedData{ + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + }, + }, + mockExecution: func(service *mocks.WebhookEventService) { + service.On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(nil, ErrWebhookEventParsedDataNotFound).Once() + service.On("SaveWebhookParsedEventData", mock.AnythingOfType("*sql.WebhookEventParsedData")). + Return(nil).Once() + service.AssertNotCalled(t, "UpdateWebhookParsedEventData", mock.Anything) + }, + wantErr: assert.NoError, + }, + { + name: "WebhookEventParsedData_Save_Error", + args: args{ + eventId: 1, + webhookEventParsedData: &sql.WebhookEventParsedData{ + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + }, + }, + mockExecution: func(service *mocks.WebhookEventService) { + service.On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(nil, ErrWebhookEventParsedDataNotFound).Once() + service.On("SaveWebhookParsedEventData", mock.AnythingOfType("*sql.WebhookEventParsedData")). + Return(os.ErrDeadlineExceeded).Once() + service.AssertNotCalled(t, "UpdateWebhookParsedEventData", mock.Anything) + }, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, os.ErrDeadlineExceeded.Error(), msgAndArgs...) + }, + }, + { + name: "GetWebhookParsedEventDataByEventIdAndUniqueId_PG_Connection_Error", + args: args{ + eventId: 1, + webhookEventParsedData: &sql.WebhookEventParsedData{ + EventId: 1, + PayloadDataId: 1, + UniqueId: "uniqueId", + EventActionType: "PUSH", + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + CiEnvVariableData: map[string]string{ + "envKey1": "envValue1", + "envKey2": "envValue2", + }, + }, + }, + mockExecution: func(service *mocks.WebhookEventService) { + service.On("GetWebhookParsedEventDataByEventIdAndUniqueId", 1, "uniqueId"). + Return(nil, os.ErrDeadlineExceeded).Once() + service.AssertNotCalled(t, "SaveWebhookParsedEventData", mock.Anything) + service.AssertNotCalled(t, "UpdateWebhookParsedEventData", mock.Anything) + }, + wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + return assert.EqualError(t, err, os.ErrDeadlineExceeded.Error(), msgAndArgs...) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + impl := getNewWebhookHandlerImplImpl(t, tt.mockExecution) + tt.wantErr(t, impl.upsertWebhookEventParsedData(tt.args.eventId, tt.args.webhookEventParsedData), fmt.Sprintf("upsertWebhookEventParsedData(%v, %v)", tt.args.eventId, tt.args.webhookEventParsedData)) + }) + } +} + +func getNewWebhookHandlerImplImpl( + t *testing.T, + WebhookEventServiceMockExecution func(service *mocks.WebhookEventService), +) *WebhookHandlerImpl { + sugaredLogger := getLogger() + webhookEventService := getWebhookEventServiceMock(t) + if WebhookEventServiceMockExecution != nil { + WebhookEventServiceMockExecution(webhookEventService) + } + return NewWebhookHandlerImpl( + sugaredLogger, + webhookEventService, + nil, + ) +} + +func getWebhookEventServiceMock(t *testing.T) *mocks.WebhookEventService { + return mocks.NewWebhookEventService(t) +} diff --git a/git-sensor/pkg/git/mocks/WebhookEventService.go b/git-sensor/pkg/git/mocks/WebhookEventService.go new file mode 100644 index 000000000..98435275d --- /dev/null +++ b/git-sensor/pkg/git/mocks/WebhookEventService.go @@ -0,0 +1,171 @@ +// Code generated by mockery v2.42.0. DO NOT EDIT. + +package mocks + +import ( + sql "github.com/devtron-labs/git-sensor/internals/sql" + mock "github.com/stretchr/testify/mock" +) + +// WebhookEventService is an autogenerated mock type for the WebhookEventService type +type WebhookEventService struct { + mock.Mock +} + +// GetAllGitHostWebhookEventByGitHostId provides a mock function with given fields: gitHostId, gitHostName +func (_m *WebhookEventService) GetAllGitHostWebhookEventByGitHostId(gitHostId int, gitHostName string) ([]*sql.GitHostWebhookEvent, error) { + ret := _m.Called(gitHostId, gitHostName) + + if len(ret) == 0 { + panic("no return value specified for GetAllGitHostWebhookEventByGitHostId") + } + + var r0 []*sql.GitHostWebhookEvent + var r1 error + if rf, ok := ret.Get(0).(func(int, string) ([]*sql.GitHostWebhookEvent, error)); ok { + return rf(gitHostId, gitHostName) + } + if rf, ok := ret.Get(0).(func(int, string) []*sql.GitHostWebhookEvent); ok { + r0 = rf(gitHostId, gitHostName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.GitHostWebhookEvent) + } + } + + if rf, ok := ret.Get(1).(func(int, string) error); ok { + r1 = rf(gitHostId, gitHostName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAllGitHostWebhookEventByGitHostName provides a mock function with given fields: gitHostName +func (_m *WebhookEventService) GetAllGitHostWebhookEventByGitHostName(gitHostName string) ([]*sql.GitHostWebhookEvent, error) { + ret := _m.Called(gitHostName) + + if len(ret) == 0 { + panic("no return value specified for GetAllGitHostWebhookEventByGitHostName") + } + + var r0 []*sql.GitHostWebhookEvent + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]*sql.GitHostWebhookEvent, error)); ok { + return rf(gitHostName) + } + if rf, ok := ret.Get(0).(func(string) []*sql.GitHostWebhookEvent); ok { + r0 = rf(gitHostName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.GitHostWebhookEvent) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(gitHostName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetWebhookParsedEventDataByEventIdAndUniqueId provides a mock function with given fields: eventId, uniqueId +func (_m *WebhookEventService) GetWebhookParsedEventDataByEventIdAndUniqueId(eventId int, uniqueId string) (*sql.WebhookEventParsedData, error) { + ret := _m.Called(eventId, uniqueId) + + if len(ret) == 0 { + panic("no return value specified for GetWebhookParsedEventDataByEventIdAndUniqueId") + } + + var r0 *sql.WebhookEventParsedData + var r1 error + if rf, ok := ret.Get(0).(func(int, string) (*sql.WebhookEventParsedData, error)); ok { + return rf(eventId, uniqueId) + } + if rf, ok := ret.Get(0).(func(int, string) *sql.WebhookEventParsedData); ok { + r0 = rf(eventId, uniqueId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.WebhookEventParsedData) + } + } + + if rf, ok := ret.Get(1).(func(int, string) error); ok { + r1 = rf(eventId, uniqueId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MatchCiTriggerConditionAndNotify provides a mock function with given fields: event, webhookEventParsedData, fullDataMap +func (_m *WebhookEventService) MatchCiTriggerConditionAndNotify(event *sql.GitHostWebhookEvent, webhookEventParsedData *sql.WebhookEventParsedData, fullDataMap map[string]string) error { + ret := _m.Called(event, webhookEventParsedData, fullDataMap) + + if len(ret) == 0 { + panic("no return value specified for MatchCiTriggerConditionAndNotify") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.GitHostWebhookEvent, *sql.WebhookEventParsedData, map[string]string) error); ok { + r0 = rf(event, webhookEventParsedData, fullDataMap) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SaveWebhookParsedEventData provides a mock function with given fields: webhookEventParsedData +func (_m *WebhookEventService) SaveWebhookParsedEventData(webhookEventParsedData *sql.WebhookEventParsedData) error { + ret := _m.Called(webhookEventParsedData) + + if len(ret) == 0 { + panic("no return value specified for SaveWebhookParsedEventData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.WebhookEventParsedData) error); ok { + r0 = rf(webhookEventParsedData) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateWebhookParsedEventData provides a mock function with given fields: webhookEventParsedData +func (_m *WebhookEventService) UpdateWebhookParsedEventData(webhookEventParsedData *sql.WebhookEventParsedData) error { + ret := _m.Called(webhookEventParsedData) + + if len(ret) == 0 { + panic("no return value specified for UpdateWebhookParsedEventData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.WebhookEventParsedData) error); ok { + r0 = rf(webhookEventParsedData) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewWebhookEventService creates a new instance of WebhookEventService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewWebhookEventService(t interface { + mock.TestingT + Cleanup(func()) +}) *WebhookEventService { + mock := &WebhookEventService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/git-sensor/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/git-sensor/vendor/modules.txt b/git-sensor/vendor/modules.txt index 616be7797..d38fa4745 100644 --- a/git-sensor/vendor/modules.txt +++ b/git-sensor/vendor/modules.txt @@ -66,7 +66,7 @@ github.com/cyphar/filepath-securejoin # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/constants github.com/devtron-labs/common-lib/fetchAllEnv @@ -488,4 +488,4 @@ gopkg.in/yaml.v3 # mellium.im/sasl v0.3.2 ## explicit; go 1.20 mellium.im/sasl -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/image-scanner/go.mod b/image-scanner/go.mod index 0c7caee86..ca3b24447 100644 --- a/image-scanner/go.mod +++ b/image-scanner/go.mod @@ -74,4 +74,4 @@ require ( mellium.im/sasl v0.3.2 // indirect ) -replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/image-scanner/go.sum b/image-scanner/go.sum index 0c6997f78..222785b0a 100644 --- a/image-scanner/go.sum +++ b/image-scanner/go.sum @@ -280,8 +280,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= diff --git a/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/image-scanner/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/image-scanner/vendor/modules.txt b/image-scanner/vendor/modules.txt index b924dca8f..e0607f367 100644 --- a/image-scanner/vendor/modules.txt +++ b/image-scanner/vendor/modules.txt @@ -72,7 +72,7 @@ github.com/cespare/xxhash/v2 github.com/coreos/clair/api/v3/clairpb github.com/coreos/clair/database github.com/coreos/clair/ext/versionfmt -# github.com/devtron-labs/common-lib v0.19.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.19.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/constants github.com/devtron-labs/common-lib/fetchAllEnv @@ -440,4 +440,4 @@ google.golang.org/protobuf/types/known/wrapperspb # mellium.im/sasl v0.3.2 ## explicit; go 1.20 mellium.im/sasl -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/kubelink/go.mod b/kubelink/go.mod index 0467a8236..2945d0efd 100644 --- a/kubelink/go.mod +++ b/kubelink/go.mod @@ -177,7 +177,7 @@ require ( ) replace ( - github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea + github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.18.0 // https://github.com/kubernetes/kubernetes/issues/79384#issuecomment-505627280 k8s.io/api => k8s.io/api v0.29.0 diff --git a/kubelink/go.sum b/kubelink/go.sum index 78324d703..ccc6e2d80 100644 --- a/kubelink/go.sum +++ b/kubelink/go.sum @@ -79,8 +79,8 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic= diff --git a/kubelink/internals/middleware/instrument.go b/kubelink/internals/middleware/instrument.go index 2dfd5ef4c..e0c5062d1 100644 --- a/kubelink/internals/middleware/instrument.go +++ b/kubelink/internals/middleware/instrument.go @@ -27,9 +27,10 @@ import ( // metrics name constant const ( - KUBELINK_HTTP_DURATION_SECONDS = "kubelink_http_duration_seconds" - KUBELINK_HTTP_REQUESTS_TOTAL = "kubelink_http_requests_total" - KUBELINK_HTTP_REQUESTS_CURRENT = "kubelink_http_requests_current" + KUBELINK_HTTP_DURATION_SECONDS = "kubelink_http_duration_seconds" + KUBELINK_HTTP_REQUESTS_TOTAL = "kubelink_http_requests_total" + KUBELINK_HTTP_REQUESTS_CURRENT = "kubelink_http_requests_current" + KUBELINK_INFORMER_DATA_TRANSFORM_DURATION_SECONDS = "kubelink_informer_data_transform_duration_seconds" ) // metrics labels constants @@ -57,6 +58,17 @@ var currentRequestGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ Help: "no of request being served currently", }, []string{PATH, METHOD}) +var InformerDataTransformDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: KUBELINK_INFORMER_DATA_TRANSFORM_DURATION_SECONDS, + Help: "Duration of informer data transform request", +}, []string{CLUSTER_NAME, NAMESPACE, RELEASE_NAME}) + +const ( + CLUSTER_NAME = "clusterName" + NAMESPACE = "namespace" + RELEASE_NAME = "releaseName" +) + // prometheusMiddleware implements mux.MiddlewareFunc. func PrometheusMiddleware(next http.Handler) http.Handler { // prometheus.MustRegister(requestCounter) diff --git a/kubelink/pkg/k8sInformer/K8sInformer.go b/kubelink/pkg/k8sInformer/K8sInformer.go index e74357aeb..807eb6924 100644 --- a/kubelink/pkg/k8sInformer/K8sInformer.go +++ b/kubelink/pkg/k8sInformer/K8sInformer.go @@ -17,10 +17,6 @@ package k8sInformer import ( - "bytes" - "compress/gzip" - "encoding/base64" - "encoding/json" "errors" "fmt" "github.com/devtron-labs/common-lib/async" @@ -29,11 +25,10 @@ import ( globalConfig "github.com/devtron-labs/kubelink/config" "github.com/devtron-labs/kubelink/converter" client "github.com/devtron-labs/kubelink/grpc" + "github.com/devtron-labs/kubelink/internals/middleware" repository "github.com/devtron-labs/kubelink/pkg/cluster" "github.com/devtron-labs/kubelink/pkg/service/helmApplicationService/adapter" "go.uber.org/zap" - "helm.sh/helm/v3/pkg/release" - "io/ioutil" coreV1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubeinformers "k8s.io/client-go/informers" @@ -57,10 +52,6 @@ const ( ) type K8sInformer interface { - startInformer(clusterInfo bean.ClusterInfo) error - syncInformer(clusterId int) error - stopInformer(clusterName string, clusterId int) - startInformerAndPopulateCache(clusterId int) error GetAllReleaseByClusterId(clusterId int) []*client.DeployedAppDetail CheckReleaseExists(clusterId int32, releaseIdentifier string) bool GetClusterClientSet(clusterInfo bean.ClusterInfo) (*kubernetes.Clientset, error) @@ -129,7 +120,7 @@ func (impl *K8sInformerImpl) OnStateChange(clusterId int, action string) { switch action { case UPDATE: err := impl.syncInformer(clusterId) - if err != nil && err != errors.New(INFORMER_ALREADY_EXIST_MESSAGE) { + if err != nil && !errors.Is(err, InformerAlreadyExistError) { impl.logger.Errorw("error in updating informer for cluster", "id", clusterId, "err", err) return } @@ -139,11 +130,7 @@ func (impl *K8sInformerImpl) OnStateChange(clusterId int, action string) { impl.logger.Errorw("Error in fetching cluster by id", "cluster-id ", clusterId) return } - impl.stopInformer(deleteClusterInfo.ClusterName, deleteClusterInfo.Id) - if err != nil { - impl.logger.Errorw("error in updating informer for cluster", "id", clusterId, "err", err) - return - } + impl.stopInformer(deleteClusterInfo.Id) } } @@ -152,40 +139,6 @@ func (impl *K8sInformerImpl) RegisterListener(listener ClusterSecretUpdateListen impl.listeners = append(impl.listeners, listener) } -func decodeRelease(data string) (*release.Release, error) { - // base64 decode string - b64 := base64.StdEncoding - b, err := b64.DecodeString(data) - if err != nil { - return nil, err - } - - var magicGzip = []byte{0x1f, 0x8b, 0x08} - - // For backwards compatibility with releases that were stored before - // compression was introduced we skip decompression if the - // gzip magic header is not found - if len(b) > 3 && bytes.Equal(b[0:3], magicGzip) { - r, err := gzip.NewReader(bytes.NewReader(b)) - if err != nil { - return nil, err - } - defer r.Close() - b2, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - b = b2 - } - - var rls release.Release - // unmarshal release object bytes - if err := json.Unmarshal(b, &rls); err != nil { - return nil, err - } - return &rls, nil -} - func (impl *K8sInformerImpl) BuildInformerForAllClusters(clusterInfos []*bean.ClusterInfo) error { if len(clusterInfos) == 0 { clusterInfo := &bean.ClusterInfo{ @@ -245,7 +198,7 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { // for default cluster adding an extra informer, this informer will add informer on new clusters if clusterInfo.ClusterName == DEFAULT_CLUSTER { - impl.logger.Debugw("Starting informer, reading new cluster request for default cluster") + impl.logger.Debugw("starting informer, reading new cluster request for default cluster") labelOptions := kubeinformers.WithTweakListOptions(func(opts *metav1.ListOptions) { //kubectl get secret --field-selector type==cluster.request/modify --all-namespaces opts.FieldSelector = "type==cluster.request/modify" @@ -253,9 +206,10 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(clusterClient, 15*time.Minute, labelOptions) stopper := make(chan struct{}) secretInformer := informerFactory.Core().V1().Secrets() - secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + _, err = secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { - impl.logger.Debugw("Event received in cluster secret Add informer", "time", time.Now()) + startTime := time.Now() + impl.logger.Debugw("CLUSTER_ADD_INFORMER: cluster secret add event received", "obj", obj, "time", time.Now()) if secretObject, ok := obj.(*coreV1.Secret); ok { if secretObject.Type != CLUSTER_MODIFY_EVENT_SECRET_TYPE { return @@ -263,26 +217,32 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { data := secretObject.Data action := data["action"] id := string(data["cluster_id"]) - id_int, _ := strconv.Atoi(id) - + var idInt int + idInt, err = strconv.Atoi(id) + if err != nil { + impl.logger.Errorw("error in converting cluster id to int", "clusterId", id, "err", err) + return + } if string(action) == ADD { - err = impl.startInformerAndPopulateCache(id_int) - if err != nil && err != errors.New(INFORMER_ALREADY_EXIST_MESSAGE) { - impl.logger.Debugw("error in adding informer for cluster", "id", id_int, "err", err) + err = impl.startInformerAndPopulateCache(idInt) + if err != nil && !errors.Is(err, InformerAlreadyExistError) { + impl.logger.Errorw("error in adding informer for cluster", "id", idInt, "err", err) return } } if string(action) == UPDATE { - err = impl.syncInformer(id_int) - if err != nil && err != errors.New(INFORMER_ALREADY_EXIST_MESSAGE) { - impl.logger.Debugw("error in updating informer for cluster", "id", clusterInfo.ClusterId, "name", clusterInfo.ClusterName, "err", err) + err = impl.syncInformer(idInt) + if err != nil && !errors.Is(err, InformerAlreadyExistError) { + impl.logger.Errorw("error in updating informer for cluster", "id", clusterInfo.ClusterId, "name", clusterInfo.ClusterName, "err", err) return } } + impl.logger.Infow("CLUSTER_ADD_INFORMER: registered informer for cluster", "clusterId", idInt, "timeTaken", time.Since(startTime)) } }, UpdateFunc: func(oldObj, newObj interface{}) { - impl.logger.Debugw("Event received in cluster secret update informer", "time", time.Now()) + startTime := time.Now() + impl.logger.Debugw("CLUSTER_UPDATE_INFORMER: cluster secret update event received", "oldObj", oldObj, "newObj", newObj, "time", time.Now()) if secretObject, ok := newObj.(*coreV1.Secret); ok { if secretObject.Type != CLUSTER_MODIFY_EVENT_SECRET_TYPE { return @@ -290,23 +250,29 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { data := secretObject.Data action := data["action"] id := string(data["cluster_id"]) - id_int, _ := strconv.Atoi(id) - + var idInt int + idInt, err = strconv.Atoi(id) + if err != nil { + impl.logger.Errorw("error in converting cluster id to int", "clusterId", id, "err", err) + return + } if string(action) == ADD { - err = impl.startInformerAndPopulateCache(clusterInfo.ClusterId) - if err != nil && err != errors.New(INFORMER_ALREADY_EXIST_MESSAGE) { - impl.logger.Errorw("error in adding informer for cluster", "id", id_int, "err", err) + err = impl.startInformerAndPopulateCache(idInt) + if err != nil && !errors.Is(err, InformerAlreadyExistError) { + impl.logger.Errorw("error in adding informer for cluster", "clusterId", idInt, "err", err) return } } if string(action) == UPDATE { - impl.OnStateChange(id_int, string(action)) - impl.informOtherListeners(id_int, string(action)) + impl.OnStateChange(idInt, string(action)) + impl.informOtherListeners(idInt, string(action)) } + impl.logger.Infow("CLUSTER_UPDATE_INFORMER: registered informer for cluster", "clusterId", idInt, "timeTaken", time.Since(startTime)) } }, DeleteFunc: func(obj interface{}) { - impl.logger.Debugw("Event received in secret delete informer", "time", time.Now()) + startTime := time.Now() + impl.logger.Debugw("CLUSTER_DELETE_INFORMER: secret delete event received", "obj", obj, "time", time.Now()) if secretObject, ok := obj.(*coreV1.Secret); ok { if secretObject.Type != CLUSTER_MODIFY_EVENT_SECRET_TYPE { return @@ -314,15 +280,23 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { data := secretObject.Data action := data["action"] id := string(data["cluster_id"]) - id_int, _ := strconv.Atoi(id) - + idInt, err := strconv.Atoi(id) + if err != nil { + impl.logger.Errorw("error in converting cluster id to int", "clusterId", id, "err", err) + return + } if string(action) == DELETE { - impl.OnStateChange(id_int, string(action)) - impl.informOtherListeners(id_int, string(action)) + impl.OnStateChange(idInt, string(action)) + impl.informOtherListeners(idInt, string(action)) } + impl.logger.Infow("CLUSTER_DELETE_INFORMER: registered informer for cluster", "clusterId", idInt, "timeTaken", time.Since(startTime)) } }, }) + if err != nil { + impl.logger.Errorw("error in adding event handler for cluster secret informer", "err", err) + return err + } informerFactory.Start(stopper) //impl.informerStopper[clusterInfo.ClusterName+"_second_informer"] = stopper @@ -330,7 +304,7 @@ func (impl *K8sInformerImpl) startInformer(clusterInfo bean.ClusterInfo) error { // these informers will be used to populate helm release cache err = impl.startInformerAndPopulateCache(clusterInfo.ClusterId) - if err != nil && err != errors.New(INFORMER_ALREADY_EXIST_MESSAGE) { + if err != nil && !errors.Is(err, InformerAlreadyExistError) { impl.logger.Errorw("error in creating informer for new cluster", "err", err) return err } @@ -348,31 +322,53 @@ func (impl *K8sInformerImpl) syncInformer(clusterId int) error { clusterInfo, err := impl.clusterRepository.FindById(clusterId) if err != nil { - impl.logger.Errorw("error in fetching cluster info by id", "err", err) + impl.logger.Errorw("error in fetching cluster info by id", "clusterId", clusterId, "err", err) return err } //before creating new informer for cluster, close existing one - impl.logger.Debugw("stopping informer for cluster - ", "cluster-name", clusterInfo.ClusterName, "cluster-id", clusterInfo.Id) - impl.stopInformer(clusterInfo.ClusterName, clusterInfo.Id) - impl.logger.Debugw("informer stopped", "cluster-name", clusterInfo.ClusterName, "cluster-id", clusterInfo.Id) + impl.logger.Debugw("stopping informer for cluster - ", "clusterName", clusterInfo.ClusterName, "clusterId", clusterInfo.Id) + impl.stopInformer(clusterInfo.Id) + impl.logger.Debugw("informer stopped", "clusterName", clusterInfo.ClusterName, "clusterId", clusterInfo.Id) //create new informer for cluster with new config err = impl.startInformerAndPopulateCache(clusterId) if err != nil { - impl.logger.Errorw("error in starting informer for ", "cluster name", clusterInfo.ClusterName) + impl.logger.Errorw("error in starting informer for", "clusterName", clusterInfo.ClusterName) return err } return nil } -func (impl *K8sInformerImpl) stopInformer(clusterName string, clusterId int) { - stopper := impl.informerStopper[clusterId] - if stopper != nil { +func (impl *K8sInformerImpl) stopInformer(clusterId int) { + if stopper, ok := impl.informerStopper[clusterId]; ok && stopper != nil { close(stopper) delete(impl.informerStopper, clusterId) } return } +func (impl *K8sInformerImpl) transformHelmRelease(clusterModel *repository.Cluster, obj any) (*coreV1.Secret, error) { + startTime := time.Now() + if secretObject, ok := obj.(*coreV1.Secret); ok && secretObject.Type == HELM_RELEASE_SECRET_TYPE { + releaseDTO, err := decodeHelmReleaseData(string(secretObject.Data["release"])) + if err != nil { + impl.logger.Errorw("TRANSFORM_HELM_RELEASE: error in decoding helm release", "clusterId", clusterModel.Id, "timeTaken", time.Since(startTime), "err", err) + return nil, err + } + appDetail := adapter.ParseDeployedAppDetail(int32(clusterModel.Id), clusterModel.ClusterName, releaseDTO) + transformedSecretData, err := parseSecretDataForDeployedAppDetail(appDetail) + if err != nil { + impl.logger.Errorw("TRANSFORM_HELM_RELEASE: error in parsing secret data for deployed app detail", "clusterId", clusterModel.Id, "appDetail", appDetail, "timeTaken", time.Since(startTime), "err", err) + return nil, err + } + secretObject.Data = transformedSecretData + impl.logger.Debugw("TRANSFORM_HELM_RELEASE: successfully decoded helm release", "clusterId", clusterModel.Id, "appDetail", appDetail, "timeTaken", time.Since(startTime)) + middleware.InformerDataTransformDuration.WithLabelValues(clusterModel.ClusterName, releaseDTO.Namespace, releaseDTO.Name).Observe(time.Since(startTime).Seconds()) + return secretObject, nil + } + impl.logger.Warnw("TRANSFORM_HELM_RELEASE: not a helm release secret", "clusterId", clusterModel.Id, "obj", obj) + return nil, errors.New("error: not a helm release secret") +} + func (impl *K8sInformerImpl) startInformerAndPopulateCache(clusterId int) error { clusterModel, err := impl.clusterRepository.FindById(clusterId) @@ -383,7 +379,7 @@ func (impl *K8sInformerImpl) startInformerAndPopulateCache(clusterId int) error if _, ok := impl.informerStopper[clusterId]; ok { impl.logger.Debugw(fmt.Sprintf("informer for %s already exist", clusterModel.ClusterName)) - return errors.New(INFORMER_ALREADY_EXIST_MESSAGE) + return InformerAlreadyExistError } impl.logger.Infow("starting informer for cluster - ", "cluster-id ", clusterModel.Id, "cluster-name ", clusterModel.ClusterName) @@ -415,68 +411,107 @@ func (impl *K8sInformerImpl) startInformerAndPopulateCache(clusterId int) error opts.LabelSelector = "status!=superseded" opts.FieldSelector = "type==helm.sh/release.v1" }) - informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(clusterClient, 15*time.Minute, labelOptions) + transformerFunc := kubeinformers.WithTransform(func(obj any) (any, error) { + return impl.transformHelmRelease(clusterModel, obj) + }) + informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(clusterClient, 15*time.Minute, labelOptions, transformerFunc) stopper := make(chan struct{}) secretInformer := informerFactory.Core().V1().Secrets() - secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - impl.logger.Debugw("Event received in Helm secret add informer", "time", time.Now()) + _, err = secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + startTime := time.Now() + impl.logger.Debugw("RELEASE_ADD_INFORMER: helm secret add event received", "clusterId", clusterModel.Id, "time", time.Now()) if secretObject, ok := obj.(*coreV1.Secret); ok { - - if secretObject.Type != HELM_RELEASE_SECRET_TYPE { + if secretObject == nil { + impl.logger.Errorw("RELEASE_ADD_INFORMER: secret object is nil! unexpected...", "clusterId", clusterModel.Id) return } - releaseDTO, err := decodeRelease(string(secretObject.Data["release"])) + impl.logger.Debugw("RELEASE_ADD_INFORMER: secret object found", "clusterId", clusterModel.Id, "secretObject", secretObject.Data) + appDetail, err := getDeployedAppDetailFromSecretData(secretObject.Data) if err != nil { - impl.logger.Error("error in decoding release") + impl.logger.Errorw("RELEASE_ADD_INFORMER: error in getting deployed app detail from secret data", "clusterId", clusterModel.Id, "err", err) + return } - appDetail := adapter.ParseDeployedAppDetail(int32(clusterModel.Id), clusterModel.ClusterName, releaseDTO) + if appDetail == nil { + impl.logger.Errorw("RELEASE_ADD_INFORMER: app detail is nil! unexpected...", "clusterId", clusterModel.Id) + return + } + impl.logger.Debugw("RELEASE_ADD_INFORMER: app detail found", "clusterId", clusterModel.Id, "appDetail", appDetail) impl.mutex.Lock() defer impl.mutex.Unlock() - impl.HelmListClusterMap[clusterId][impl.getUniqueReleaseKey(&ReleaseDto{releaseDTO}, clusterModel.Id)] = appDetail + impl.HelmListClusterMap[clusterId][impl.getUniqueReleaseKey(NewDeployedAppDetailDto(appDetail))] = appDetail + impl.logger.Infow("RELEASE_ADD_INFORMER: added app detail in cache", "clusterId", clusterModel.Id, "namespace", appDetail.EnvironmentDetail.Namespace, "releaseName", appDetail.AppName, "timeTaken", time.Since(startTime)) + } else { + impl.logger.Errorw("RELEASE_ADD_INFORMER: not a secret object", "clusterId", clusterModel.Id, "obj", obj) } }, UpdateFunc: func(oldObj, newObj interface{}) { - impl.logger.Debugw("Event received in Helm secret update informer", "time", time.Now()) - if secretObject, ok := oldObj.(*coreV1.Secret); ok { - if secretObject.Type != HELM_RELEASE_SECRET_TYPE { + startTime := time.Now() + impl.logger.Debugw("RELEASE_UPDATE_INFORMER: helm secret update event received", "clusterId", clusterModel.Id, "time", time.Now()) + if secretObject, ok := newObj.(*coreV1.Secret); ok { + if secretObject == nil { + impl.logger.Errorw("secret object is nil! unexpected...", "clusterId", clusterModel.Id) return } - releaseDTO, err := decodeRelease(string(secretObject.Data["release"])) + impl.logger.Debugw("RELEASE_UPDATE_INFORMER: secret object found", "clusterId", clusterModel.Id, "secretObject", secretObject.Data) + appDetail, err := getDeployedAppDetailFromSecretData(secretObject.Data) if err != nil { - impl.logger.Error("error in decoding release") + impl.logger.Errorw("RELEASE_UPDATE_INFORMER: error in getting deployed app detail from secret data", "clusterId", clusterModel.Id, "err", err) + return + } + if appDetail == nil { + impl.logger.Errorw("RELEASE_UPDATE_INFORMER: app detail is nil! unexpected...", "clusterId", clusterModel.Id) + return } - appDetail := adapter.ParseDeployedAppDetail(int32(clusterModel.Id), clusterModel.ClusterName, releaseDTO) + impl.logger.Debugw("RELEASE_UPDATE_INFORMER: app detail found", "clusterId", clusterModel.Id, "appDetail", appDetail) impl.mutex.Lock() defer impl.mutex.Unlock() - // adding cluster id with release name and namespace because there can be case when two cluster or two namespaces have release with same name - impl.HelmListClusterMap[clusterId][impl.getUniqueReleaseKey(&ReleaseDto{releaseDTO}, clusterModel.Id)] = appDetail + impl.HelmListClusterMap[clusterId][impl.getUniqueReleaseKey(NewDeployedAppDetailDto(appDetail))] = appDetail + impl.logger.Infow("RELEASE_UPDATE_INFORMER: updated app detail in cache", "clusterId", clusterModel.Id, "namespace", appDetail.EnvironmentDetail.Namespace, "releaseName", appDetail.AppName, "timeTaken", time.Since(startTime)) + } else { + impl.logger.Errorw("RELEASE_UPDATE_INFORMER: not a secret object", "clusterId", clusterModel.Id, "obj", newObj) } }, DeleteFunc: func(obj interface{}) { - impl.logger.Debugw("Event received in Helm secret delete informer", "time", time.Now()) + startTime := time.Now() + impl.logger.Debugw("RELEASE_DELETE_INFORMER: helm secret delete event received", "clusterId", clusterModel.Id, "time", time.Now()) if secretObject, ok := obj.(*coreV1.Secret); ok { - if secretObject.Type != HELM_RELEASE_SECRET_TYPE { + if secretObject == nil { + impl.logger.Errorw("RELEASE_DELETE_INFORMER: secret object is nil! unexpected...", "clusterId", clusterModel.Id) return } - releaseDTO, err := decodeRelease(string(secretObject.Data["release"])) + impl.logger.Debugw("RELEASE_DELETE_INFORMER: secret object found", "clusterId", clusterModel.Id, "secretObject", secretObject.Data) + appDetail, err := getDeployedAppDetailFromSecretData(secretObject.Data) if err != nil { - impl.logger.Error("error in decoding release") + impl.logger.Errorw("RELEASE_DELETE_INFORMER: error in getting deployed app detail from secret data", "clusterId", clusterModel.Id, "err", err) + return + } + if appDetail == nil { + impl.logger.Errorw("RELEASE_DELETE_INFORMER: app detail is nil! unexpected...", "clusterId", clusterModel.Id) + return } + impl.logger.Debugw("RELEASE_DELETE_INFORMER: app detail found", "clusterId", clusterModel.Id, "appDetail", appDetail) impl.mutex.Lock() defer impl.mutex.Unlock() - delete(impl.HelmListClusterMap[clusterId], impl.getUniqueReleaseKey(&ReleaseDto{releaseDTO}, clusterModel.Id)) + delete(impl.HelmListClusterMap[clusterId], impl.getUniqueReleaseKey(NewDeployedAppDetailDto(appDetail))) + impl.logger.Infow("RELEASE_DELETE_INFORMER: deleted app detail in cache", "clusterId", clusterModel.Id, "namespace", appDetail.EnvironmentDetail.Namespace, "releaseName", appDetail.AppName, "timeTaken", time.Since(startTime)) + } else { + impl.logger.Errorw("RELEASE_DELETE_INFORMER: not a secret object", "clusterId", clusterModel.Id, "obj", obj) } }, }) + if err != nil { + impl.logger.Errorw("error in adding event handler for helm secret informer", "clusterId", clusterId, "err", err) + return err + } informerFactory.Start(stopper) - impl.logger.Infow("informer started for cluster: ", "cluster_id", clusterModel.Id, "cluster_name", clusterModel.ClusterName) + impl.logger.Infow("informer started for cluster", "clusterId", clusterModel.Id, "clusterName", clusterModel.ClusterName) impl.informerStopper[clusterId] = stopper return nil } -func (impl *K8sInformerImpl) getUniqueReleaseKey(release *ReleaseDto, clusterId int) string { - return release.getUniqueReleaseIdentifier() + "_" + strconv.Itoa(clusterId) +func (impl *K8sInformerImpl) getUniqueReleaseKey(appDetailDto *DeployedAppDetailDto) string { + return appDetailDto.getUniqueReleaseIdentifier() } func (impl *K8sInformerImpl) GetAllReleaseByClusterId(clusterId int) []*client.DeployedAppDetail { diff --git a/kubelink/pkg/k8sInformer/bean.go b/kubelink/pkg/k8sInformer/bean.go new file mode 100644 index 000000000..6948084ac --- /dev/null +++ b/kubelink/pkg/k8sInformer/bean.go @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package k8sInformer + +import ( + "fmt" + client "github.com/devtron-labs/kubelink/grpc" +) + +const ( + secretKeyAppDetailKey = "appDetail" +) + +type DeployedAppDetailDto struct { + *client.DeployedAppDetail +} + +func NewDeployedAppDetailDto(appDetail *client.DeployedAppDetail) *DeployedAppDetailDto { + return &DeployedAppDetailDto{DeployedAppDetail: appDetail} +} + +func (r *DeployedAppDetailDto) getUniqueReleaseIdentifier() string { + if r == nil || r.EnvironmentDetail == nil { + return "" + } + // adding cluster id with release name and namespace because there can be case when two cluster or two namespaces have release with same name + return fmt.Sprintf("%s_%s_%d", r.EnvironmentDetail.Namespace, r.AppName, r.EnvironmentDetail.ClusterId) +} diff --git a/kubelink/pkg/k8sInformer/constants.go b/kubelink/pkg/k8sInformer/error.go similarity index 75% rename from kubelink/pkg/k8sInformer/constants.go rename to kubelink/pkg/k8sInformer/error.go index 704040897..c5eabd5b3 100644 --- a/kubelink/pkg/k8sInformer/constants.go +++ b/kubelink/pkg/k8sInformer/error.go @@ -18,17 +18,11 @@ package k8sInformer import ( "errors" - "helm.sh/helm/v3/pkg/release" ) -type ReleaseDto struct { - *release.Release -} - -func (r *ReleaseDto) getUniqueReleaseIdentifier() string { - return r.Namespace + "_" + r.Name -} - var ( + // ErrorCacheMissReleaseNotFound is returned when a release is not found in the cache ErrorCacheMissReleaseNotFound = errors.New("release not found in cache") + // InformerAlreadyExistError is returned when an informer already exists + InformerAlreadyExistError = errors.New(INFORMER_ALREADY_EXIST_MESSAGE) ) diff --git a/kubelink/pkg/k8sInformer/util.go b/kubelink/pkg/k8sInformer/util.go new file mode 100644 index 000000000..7cc1e7783 --- /dev/null +++ b/kubelink/pkg/k8sInformer/util.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package k8sInformer + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "errors" + client "github.com/devtron-labs/kubelink/grpc" + "github.com/devtron-labs/kubelink/pkg/service/helmApplicationService/release" + "io" +) + +func parseSecretDataForDeployedAppDetail(appDetail *client.DeployedAppDetail) (map[string][]byte, error) { + appDetailBytes, err := json.Marshal(appDetail) + if err != nil { + return nil, err + } + data := make(map[string][]byte) + data[secretKeyAppDetailKey] = appDetailBytes + return data, nil +} + +func getDeployedAppDetailFromSecretData(data map[string][]byte) (*client.DeployedAppDetail, error) { + if appDetailBytes, ok := data[secretKeyAppDetailKey]; ok { + var appDetail client.DeployedAppDetail + err := json.Unmarshal(appDetailBytes, &appDetail) + if err != nil { + return nil, err + } + return &appDetail, nil + } + return nil, errors.New("app detail not found in secret data") +} + +func decodeHelmReleaseData(data string) (*release.Release, error) { + // base64 decode string + b64 := base64.StdEncoding + b, err := b64.DecodeString(data) + if err != nil { + return nil, err + } + + var magicGzip = []byte{0x1f, 0x8b, 0x08} + + // For backwards compatibility with releases that were stored before + // compression was introduced we skip decompression if the + // gzip magic header is not found + if len(b) > 3 && bytes.Equal(b[0:3], magicGzip) { + r, err := gzip.NewReader(bytes.NewReader(b)) + if err != nil { + return nil, err + } + defer r.Close() + b2, err := io.ReadAll(r) + if err != nil { + return nil, err + } + b = b2 + } + + var rls release.Release + // unmarshal release object bytes + if err := json.Unmarshal(b, &rls); err != nil { + return nil, err + } + return &rls, nil +} diff --git a/kubelink/pkg/service/helmApplicationService/adapter/adapter.go b/kubelink/pkg/service/helmApplicationService/adapter/adapter.go index bd8190302..5c887d78b 100644 --- a/kubelink/pkg/service/helmApplicationService/adapter/adapter.go +++ b/kubelink/pkg/service/helmApplicationService/adapter/adapter.go @@ -2,11 +2,13 @@ package adapter import ( "github.com/devtron-labs/common-lib/helmLib/registry" - "github.com/devtron-labs/common-lib/utils/remoteConnection/bean" + remoteConnection "github.com/devtron-labs/common-lib/utils/remoteConnection/bean" client "github.com/devtron-labs/kubelink/grpc" + "github.com/devtron-labs/kubelink/pkg/service/helmApplicationService/release" "github.com/devtron-labs/kubelink/pkg/util" "google.golang.org/protobuf/types/known/timestamppb" - "helm.sh/helm/v3/pkg/release" + helmChart "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" ) func NewRegistryConfig(credential *client.RegistryCredential) (*registry.Configuration, error) { @@ -36,15 +38,15 @@ func NewRegistryConfig(credential *client.RegistryCredential) (*registry.Configu connectionConfig := credential.RemoteConnectionConfig if connectionConfig != nil { - registryConfig.RemoteConnectionConfig = &bean.RemoteConnectionConfigBean{} + registryConfig.RemoteConnectionConfig = &remoteConnection.RemoteConnectionConfigBean{} switch connectionConfig.RemoteConnectionMethod { case client.RemoteConnectionMethod_DIRECT: - registryConfig.RemoteConnectionConfig.ConnectionMethod = bean.RemoteConnectionMethodDirect + registryConfig.RemoteConnectionConfig.ConnectionMethod = remoteConnection.RemoteConnectionMethodDirect case client.RemoteConnectionMethod_PROXY: - registryConfig.RemoteConnectionConfig.ConnectionMethod = bean.RemoteConnectionMethodProxy + registryConfig.RemoteConnectionConfig.ConnectionMethod = remoteConnection.RemoteConnectionMethodProxy registryConfig.RemoteConnectionConfig.ProxyConfig = ConvertConfigToProxyConfig(connectionConfig) case client.RemoteConnectionMethod_SSH: - registryConfig.RemoteConnectionConfig.ConnectionMethod = bean.RemoteConnectionMethodSSH + registryConfig.RemoteConnectionConfig.ConnectionMethod = remoteConnection.RemoteConnectionMethodSSH registryConfig.RemoteConnectionConfig.SSHTunnelConfig = ConvertConfigToSSHTunnelConfig(connectionConfig) } } @@ -52,20 +54,20 @@ func NewRegistryConfig(credential *client.RegistryCredential) (*registry.Configu return registryConfig, nil } -func ConvertConfigToProxyConfig(config *client.RemoteConnectionConfig) *bean.ProxyConfig { - var proxyConfig *bean.ProxyConfig +func ConvertConfigToProxyConfig(config *client.RemoteConnectionConfig) *remoteConnection.ProxyConfig { + var proxyConfig *remoteConnection.ProxyConfig if config.ProxyConfig != nil { - proxyConfig = &bean.ProxyConfig{ + proxyConfig = &remoteConnection.ProxyConfig{ ProxyUrl: config.ProxyConfig.ProxyUrl, } } return proxyConfig } -func ConvertConfigToSSHTunnelConfig(config *client.RemoteConnectionConfig) *bean.SSHTunnelConfig { - var sshConfig *bean.SSHTunnelConfig +func ConvertConfigToSSHTunnelConfig(config *client.RemoteConnectionConfig) *remoteConnection.SSHTunnelConfig { + var sshConfig *remoteConnection.SSHTunnelConfig if config.SSHTunnelConfig != nil { - sshConfig = &bean.SSHTunnelConfig{ + sshConfig = &remoteConnection.SSHTunnelConfig{ SSHUsername: config.SSHTunnelConfig.SSHUsername, SSHPassword: config.SSHTunnelConfig.SSHPassword, SSHAuthKey: config.SSHTunnelConfig.SSHAuthKey, @@ -119,3 +121,46 @@ func GetAppDetailRequestFromGetResourceTreeRequest(req *client.GetResourceTreeRe CacheConfig: req.CacheConfig, } } + +func NewRelease(helmRelease *helmRelease.Release) *release.Release { + if helmRelease == nil { + return nil + } + return &release.Release{ + Name: helmRelease.Name, + Namespace: helmRelease.Namespace, + Info: NewReleaseInfo(helmRelease.Info), + Chart: NewChart(helmRelease.Chart), + } +} + +func NewReleaseInfo(helmReleaseInfo *helmRelease.Info) *release.Info { + if helmReleaseInfo == nil { + return nil + } + return &release.Info{ + LastDeployed: helmReleaseInfo.LastDeployed, + Status: helmReleaseInfo.Status, + } +} + +func NewChart(helmChart *helmChart.Chart) *release.Chart { + if helmChart == nil { + return nil + } + return &release.Chart{ + Metadata: NewChartMetadata(helmChart.Metadata), + } +} + +func NewChartMetadata(helmChartMetadata *helmChart.Metadata) *release.Metadata { + if helmChartMetadata == nil { + return nil + } + return &release.Metadata{ + Name: helmChartMetadata.Name, + Version: helmChartMetadata.Version, + Icon: helmChartMetadata.Icon, + Home: helmChartMetadata.Home, + } +} diff --git a/kubelink/pkg/service/helmApplicationService/helmAppService.go b/kubelink/pkg/service/helmApplicationService/helmAppService.go index c92de2cb4..ea7622fd0 100644 --- a/kubelink/pkg/service/helmApplicationService/helmAppService.go +++ b/kubelink/pkg/service/helmApplicationService/helmAppService.go @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package helmApplicationService import ( @@ -22,16 +23,16 @@ import ( "encoding/json" "errors" "fmt" - registry2 "github.com/devtron-labs/common-lib/helmLib/registry" + "github.com/devtron-labs/common-lib/helmLib/registry" "github.com/devtron-labs/common-lib/utils/k8s/commonBean" "github.com/devtron-labs/common-lib/utils/k8sObjectsUtil" "github.com/devtron-labs/kubelink/converter" - error2 "github.com/devtron-labs/kubelink/error" + customErr "github.com/devtron-labs/kubelink/error" repository "github.com/devtron-labs/kubelink/pkg/cluster" "github.com/devtron-labs/kubelink/pkg/service/commonHelmService" "github.com/devtron-labs/kubelink/pkg/service/helmApplicationService/adapter" "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/registry" + helmRegistry "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/storage/driver" "net/url" "path" @@ -57,7 +58,7 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/repo" "io/ioutil" - errors2 "k8s.io/apimachinery/pkg/api/errors" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -120,7 +121,7 @@ type HelmAppServiceImpl struct { pubsubClient *pubsub_lib.PubSubClientServiceImpl clusterRepository repository.ClusterRepository converter converter.ClusterBeanConverter - registrySettings registry2.SettingsFactory + registrySettings registry.SettingsFactory common commonHelmService.CommonHelmService resourceTreeService commonHelmService.ResourceTreeService } @@ -128,7 +129,7 @@ type HelmAppServiceImpl struct { func NewHelmAppServiceImpl(logger *zap.SugaredLogger, k8sService commonHelmService.K8sService, k8sInformer k8sInformer.K8sInformer, helmReleaseConfig *globalConfig.HelmReleaseConfig, k8sUtil k8sUtils.K8sService, converter converter.ClusterBeanConverter, - clusterRepository repository.ClusterRepository, common commonHelmService.CommonHelmService, registrySettings registry2.SettingsFactory, + clusterRepository repository.ClusterRepository, common commonHelmService.CommonHelmService, registrySettings registry.SettingsFactory, resourceTreeService commonHelmService.ResourceTreeService) (*HelmAppServiceImpl, error) { var pubsubClient *pubsub_lib.PubSubClientServiceImpl @@ -177,7 +178,7 @@ func (impl *HelmAppServiceImpl) GetRandomString() string { func (impl *HelmAppServiceImpl) getDeployedAppDetails(config *client.ClusterConfig) (deployedApps []*client.DeployedAppDetail, err error) { if impl.helmReleaseConfig.IsHelmReleaseCachingEnabled() { - impl.logger.Infow("Fetching helm release using Cache") + impl.logger.Infow("fetching helm release using Cache") return impl.K8sInformer.GetAllReleaseByClusterId(int(config.GetClusterId())), nil } @@ -199,14 +200,14 @@ func (impl *HelmAppServiceImpl) getDeployedAppDetails(config *client.ClusterConf } impl.logger.Debug("fetching all release list from helm") - releases, err := helmAppClient.ListAllReleases() + helmReleases, err := helmAppClient.ListAllReleases() if err != nil { impl.logger.Errorw("error in getting releases list ", "clusterId", config.ClusterId, "err", err) return deployedApps, err } - for _, item := range releases { - deployedApps = append(deployedApps, adapter.NewDeployedAppDetail(config, item)) + for _, helmRelease := range helmReleases { + deployedApps = append(deployedApps, adapter.NewDeployedAppDetail(config, adapter.NewRelease(helmRelease))) } return deployedApps, nil } @@ -225,18 +226,18 @@ func (impl *HelmAppServiceImpl) GetApplicationListForCluster(config *client.Clus return deployedApp } -func (impl HelmAppServiceImpl) GetResourceTreeForExternalResources(ctx context.Context, req *client.ExternalResourceTreeRequest) (*bean.ResourceTreeResponse, error) { +func (impl *HelmAppServiceImpl) GetResourceTreeForExternalResources(ctx context.Context, req *client.ExternalResourceTreeRequest) (*bean.ResourceTreeResponse, error) { return impl.common.GetResourceTreeForExternalResources(ctx, req) } -func (impl HelmAppServiceImpl) BuildAppDetail(ctx context.Context, req *client.AppDetailRequest) (*bean.AppDetail, error) { +func (impl *HelmAppServiceImpl) BuildAppDetail(ctx context.Context, req *client.AppDetailRequest) (*bean.AppDetail, error) { helmRelease, err := impl.common.GetHelmRelease(req.ClusterConfig, req.Namespace, req.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) if errors.Is(err, driver.ErrReleaseNotFound) { return &bean.AppDetail{ReleaseExists: false}, err } - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -301,7 +302,7 @@ func (impl *HelmAppServiceImpl) FetchApplicationStatus(req *client.AppDetailRequ helmRelease, err := impl.common.GetHelmRelease(req.ClusterConfig, req.Namespace, req.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -325,12 +326,12 @@ func (impl *HelmAppServiceImpl) FetchApplicationStatus(req *client.AppDetailRequ // FetchApplicationStatusV2 gets the application status of helm release using cache if preferred which can be used anywhere as it gives unfiltered status // unlike its counterpart FetchApplicationStatus which filters down status into healthy or non-healthy since it is only used for deployment status calculation. -func (impl HelmAppServiceImpl) FetchApplicationStatusV2(ctx context.Context, req *client.AppDetailRequest) (*client.AppStatus, error) { +func (impl *HelmAppServiceImpl) FetchApplicationStatusV2(ctx context.Context, req *client.AppDetailRequest) (*client.AppStatus, error) { helmAppStatus := &client.AppStatus{} helmRelease, err := impl.common.GetHelmRelease(req.ClusterConfig, req.Namespace, req.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -350,7 +351,7 @@ func (impl *HelmAppServiceImpl) GetHelmAppValues(req *client.AppDetailRequest) ( helmRelease, err := impl.common.GetHelmRelease(req.ClusterConfig, req.Namespace, req.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -368,7 +369,7 @@ func (impl *HelmAppServiceImpl) GetHelmAppValues(req *client.AppDetailRequest) ( return nil, err } - appDetail := adapter.ParseDeployedAppDetail(req.ClusterConfig.ClusterId, req.ClusterConfig.ClusterName, helmRelease) + appDetail := adapter.ParseDeployedAppDetail(req.ClusterConfig.ClusterId, req.ClusterConfig.ClusterName, adapter.NewRelease(helmRelease)) releaseInfo.DeployedAppDetail = appDetail return releaseInfo, nil @@ -469,7 +470,7 @@ func (impl *HelmAppServiceImpl) GetDeploymentHistory(req *client.AppDetailReques helmReleases, err := impl.getHelmReleaseHistory(req.ClusterConfig, req.Namespace, req.ReleaseName, impl.helmReleaseConfig.MaxCountForHelmRelease) if err != nil { impl.logger.Errorw("Error in getting helm release history ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -516,7 +517,7 @@ func (impl *HelmAppServiceImpl) GetDesiredManifest(req *client.ObjectRequest) (* helmRelease, err := impl.common.GetHelmRelease(req.ClusterConfig, req.ReleaseNamespace, req.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -557,7 +558,7 @@ func (impl *HelmAppServiceImpl) UninstallRelease(releaseIdentifier *client.Relea err = helmClient.UninstallReleaseByName(releaseIdentifier.ReleaseName) if err != nil { impl.logger.Errorw("Error in uninstall release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -582,7 +583,7 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli if err != nil { return nil, err } - registryClient, err := registry.NewClient() + registryClient, err := helmRegistry.NewClient() if err != nil { impl.logger.Errorw(HELM_CLIENT_ERROR, "err", err) return nil, err @@ -591,7 +592,7 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli helmRelease, err := impl.common.GetHelmRelease(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -610,7 +611,7 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli _, err = helmClientObj.UpgradeRelease(context.Background(), helmRelease.Chart, updateChartSpec) if err != nil { impl.logger.Errorw("Error in upgrade release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -644,7 +645,7 @@ func (impl *HelmAppServiceImpl) GetDeploymentDetail(request *client.DeploymentDe helmReleases, err := impl.getHelmReleaseHistory(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName, impl.helmReleaseConfig.MaxCountForHelmRelease) if err != nil { impl.logger.Errorw("Error in getting helm release history ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -742,7 +743,7 @@ func (impl *HelmAppServiceImpl) installRelease(ctx context.Context, request *cli rel, err := helmClientObj.InstallChart(context.Background(), chartSpec) if err != nil { impl.logger.Errorw("Error in install release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -815,7 +816,7 @@ func (impl *HelmAppServiceImpl) GetNotes(ctx context.Context, request *client.In release, err := helmClientObj.GetNotes(chartSpec, helmTemplateOptions) if err != nil { impl.logger.Errorw("Error in fetching Notes ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -1198,7 +1199,7 @@ func (impl *HelmAppServiceImpl) getManifestData(restConfig *rest.Config, release if err != nil { impl.logger.Errorw("Error in getting live manifest ", "err", err) - statusError, _ := err.(*errors2.StatusError) + statusError, _ := err.(*k8sErrors.StatusError) desiredOrLiveManifest = &bean.DesiredOrLiveManifest{ // using deep copy as it replaces item in manifest in loop Manifest: desiredManifest.DeepCopy(), @@ -1315,7 +1316,7 @@ func (impl *HelmAppServiceImpl) InstallReleaseWithCustomChart(ctx context.Contex _, err = helmClientObj.InstallChart(ctx, chartSpec) if err != nil { impl.logger.Errorw("Error in install chart", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -1385,7 +1386,7 @@ func (impl *HelmAppServiceImpl) UpgradeReleaseWithCustomChart(ctx context.Contex _, err := helmClientObj.InstallChart(ctx, installChartSpec) if err != nil { impl.logger.Errorw("Error in install release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -1400,7 +1401,7 @@ func (impl *HelmAppServiceImpl) UpgradeReleaseWithCustomChart(ctx context.Contex } } else if err != nil { impl.logger.Errorw("Error in upgrade release with chart info", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -1411,14 +1412,14 @@ func (impl *HelmAppServiceImpl) UpgradeReleaseWithCustomChart(ctx context.Contex } func (impl *HelmAppServiceImpl) ValidateOCIRegistryLogin(ctx context.Context, OCIRegistryRequest *client.RegistryCredential) (*client.OCIRegistryResponse, error) { - registryClient, err := registry.NewClient() + registryClient, err := helmRegistry.NewClient() if err != nil { impl.logger.Errorw(HELM_CLIENT_ERROR, "err", err) return nil, err } var caFilePath string - if OCIRegistryRequest.Connection == registry2.SECURE_WITH_CERT { - caFilePath, err = registry2.CreateCertificateFile(OCIRegistryRequest.RegistryName, OCIRegistryRequest.RegistryCertificate) + if OCIRegistryRequest.Connection == registry.SECURE_WITH_CERT { + caFilePath, err = registry.CreateCertificateFile(OCIRegistryRequest.RegistryName, OCIRegistryRequest.RegistryCertificate) if err != nil { impl.logger.Errorw("error in creating certificate file path", "registryName", OCIRegistryRequest.RegistryName, "err", err) return nil, err @@ -1427,7 +1428,7 @@ func (impl *HelmAppServiceImpl) ValidateOCIRegistryLogin(ctx context.Context, OC registryConfig, err := adapter.NewRegistryConfig(OCIRegistryRequest) defer func() { if registryConfig != nil { - err := registry2.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) + err := registry.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) if err != nil { impl.logger.Errorw("error in deleting certificate folder", "registryName", registryConfig.RegistryId, "err", err) } @@ -1438,7 +1439,7 @@ func (impl *HelmAppServiceImpl) ValidateOCIRegistryLogin(ctx context.Context, OC return nil, err } registryConfig.RegistryCAFilePath = caFilePath - registryClient, err = registry2.GetLoggedInClient(registryClient, registryConfig) + registryClient, err = registry.GetLoggedInClient(registryClient, registryConfig) if err != nil { impl.logger.Errorw("error in registry login", "registryName", OCIRegistryRequest.RegistryName, "err", err) return nil, err @@ -1453,7 +1454,7 @@ func (impl *HelmAppServiceImpl) PushHelmChartToOCIRegistryRepo(ctx context.Conte registryConfig, err := adapter.NewRegistryConfig(OCIRegistryRequest.RegistryCredential) defer func() { if registryConfig != nil { - err := registry2.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) + err := registry.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) if err != nil { impl.logger.Errorw("error in deleting certificate folder", "registryName", registryConfig.RegistryId, "err", err) } @@ -1478,7 +1479,7 @@ func (impl *HelmAppServiceImpl) PushHelmChartToOCIRegistryRepo(ctx context.Conte registryPushResponse.IsLoggedIn = true - var pushOpts []registry.PushOption + var pushOpts []helmRegistry.PushOption provRef := fmt.Sprintf("%s.prov", OCIRegistryRequest.Chart) if _, err := os.Stat(provRef); err == nil { provBytes, err := ioutil.ReadFile(provRef) @@ -1486,11 +1487,11 @@ func (impl *HelmAppServiceImpl) PushHelmChartToOCIRegistryRepo(ctx context.Conte impl.logger.Errorw("Error in extracting prov bytes", "err", err) return registryPushResponse, err } - pushOpts = append(pushOpts, registry.PushOptProvData(provBytes)) + pushOpts = append(pushOpts, helmRegistry.PushOptProvData(provBytes)) } var ref string - withStrictMode := registry.PushOptStrictMode(true) + withStrictMode := helmRegistry.PushOptStrictMode(true) repoURL := path.Join(OCIRegistryRequest.RegistryCredential.RegistryUrl, OCIRegistryRequest.RegistryCredential.RepoName) if OCIRegistryRequest.ChartName == "" || OCIRegistryRequest.ChartVersion == "" { @@ -1511,7 +1512,7 @@ func (impl *HelmAppServiceImpl) PushHelmChartToOCIRegistryRepo(ctx context.Conte meta.Metadata.Version) } else { // disable strict mode for configuring chartName in repo - withStrictMode = registry.PushOptStrictMode(false) + withStrictMode = helmRegistry.PushOptStrictMode(false) trimmedURL := TrimSchemeFromURL(repoURL) if err != nil { impl.logger.Errorw("err in getting repo url without scheme", "repoURL", repoURL, "err", err) @@ -1534,6 +1535,7 @@ func (impl *HelmAppServiceImpl) PushHelmChartToOCIRegistryRepo(ctx context.Conte } return registryPushResponse, err } + func TrimSchemeFromURL(registryUrl string) string { if !strings.Contains(strings.ToLower(registryUrl), "https") && !strings.Contains(strings.ToLower(registryUrl), "http") { registryUrl = fmt.Sprintf("//%s", registryUrl) @@ -1546,6 +1548,7 @@ func TrimSchemeFromURL(registryUrl string) string { urlWithoutScheme = strings.TrimPrefix(urlWithoutScheme, "/") return urlWithoutScheme } + func (impl *HelmAppServiceImpl) GetNatsMessageForHelmInstallError(ctx context.Context, helmInstallMessage commonHelmService.HelmReleaseStatusConfig, releaseIdentifier *client.ReleaseIdentifier, installationErr error) (string, error) { helmInstallMessage.Message = installationErr.Error() isReleaseInstalled, err := impl.IsReleaseInstalled(ctx, releaseIdentifier) @@ -1587,7 +1590,7 @@ func (impl *HelmAppServiceImpl) GetReleaseDetails(ctx context.Context, releaseId helmRelease, err := impl.common.GetHelmRelease(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) - internalErr := error2.ConvertHelmErrorToInternalError(err) + internalErr := customErr.ConvertHelmErrorToInternalError(err) if internalErr != nil { err = internalErr } @@ -1597,7 +1600,7 @@ func (impl *HelmAppServiceImpl) GetReleaseDetails(ctx context.Context, releaseId impl.logger.Errorw("requested helm release does not exist") return nil, commonHelmService.ErrorReleaseNotFoundOnCluster } - return adapter.ParseDeployedAppDetail(releaseIdentifier.ClusterConfig.ClusterId, releaseIdentifier.ClusterConfig.ClusterName, helmRelease), nil + return adapter.ParseDeployedAppDetail(releaseIdentifier.ClusterConfig.ClusterId, releaseIdentifier.ClusterConfig.ClusterName, adapter.NewRelease(helmRelease)), nil } impl.logger.Errorw("error in fetching release details by id", "releaseIdentifier", releaseIdentifier, "err", err) return nil, err @@ -1629,6 +1632,7 @@ func (impl *HelmAppServiceImpl) BuildResourceTreeUsingParentObjects(ctx context. } return resourceTreeResponse, nil } + func (impl *HelmAppServiceImpl) resourceNodeAdapter(nodes []*commonBean.ResourceNode) []*client.ResourceNode { var resourceNodes []*client.ResourceNode for _, node := range nodes { @@ -1727,11 +1731,11 @@ func podMetadataAdapter(podmetadatas []*commonBean.PodMetadata) []*client.PodMet return podMetadatas } -func (impl *HelmAppServiceImpl) setupRegistryClient(request *client.InstallReleaseRequest) (*registry.Client, func(), error) { - var settings *registry2.Settings +func (impl *HelmAppServiceImpl) setupRegistryClient(request *client.InstallReleaseRequest) (*helmRegistry.Client, func(), error) { + var settings *registry.Settings cleanup := func() {} registryCredential := request.RegistryCredential - var registryClient *registry.Client + var registryClient *helmRegistry.Client if request.RegistryCredential != nil { registryConfig, err := adapter.NewRegistryConfig(registryCredential) @@ -1742,7 +1746,7 @@ func (impl *HelmAppServiceImpl) setupRegistryClient(request *client.InstallRelea cleanup = func() { if registryConfig != nil { - err := registry2.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) + err := registry.DeleteCertificateFolder(registryConfig.RegistryCAFilePath) if err != nil { impl.logger.Errorw("error in deleting certificate folder", "registryName", registryConfig.RegistryId, "err", err) } @@ -1768,7 +1772,7 @@ func (impl *HelmAppServiceImpl) setupRegistryClient(request *client.InstallRelea return registryClient, cleanup, nil } -func (impl *HelmAppServiceImpl) getChartSpec(helmClientObj helmClient.Client, request *client.InstallReleaseRequest, releaseIdentifier *client.ReleaseIdentifier, registryClient *registry.Client) (*helmClient.ChartSpec, error) { +func (impl *HelmAppServiceImpl) getChartSpec(helmClientObj helmClient.Client, request *client.InstallReleaseRequest, releaseIdentifier *client.ReleaseIdentifier, registryClient *helmRegistry.Client) (*helmClient.ChartSpec, error) { var chartName, username, password string var allowInsecureConnection bool var err error diff --git a/kubelink/pkg/service/helmApplicationService/release/bean.go b/kubelink/pkg/service/helmApplicationService/release/bean.go new file mode 100644 index 000000000..6e2b52226 --- /dev/null +++ b/kubelink/pkg/service/helmApplicationService/release/bean.go @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package release + +import ( + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" +) + +// Release describes a deployment of a chart, together with the chart +// and the variables used to deploy that chart. +type Release struct { + // Name is the name of the release + Name string `json:"name,omitempty"` + // Info provides information about a release + Info *Info `json:"info,omitempty"` + // Chart is the chart that was released. + Chart *Chart `json:"chart,omitempty"` + // Namespace is the kubernetes namespace of the release. + Namespace string `json:"namespace,omitempty"` +} + +// Info describes release information. +type Info struct { + // LastDeployed is when the release was last deployed. + LastDeployed time.Time `json:"last_deployed,omitempty"` + // Status is the current state of the release + Status release.Status `json:"status,omitempty"` +} + +// Chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +type Chart struct { + // Metadata is the contents of the Chartfile. + Metadata *Metadata `json:"metadata"` +} + +// Metadata for a Chart file. This models the structure of a Chart.yaml file. +type Metadata struct { + // The name of the chart. Required. + Name string `json:"name,omitempty"` + // The URL to a relevant project page, git repo, or contact person + Home string `json:"home,omitempty"` + // A SemVer 2 conformant version string of the chart. Required. + Version string `json:"version,omitempty"` + // The URL to an icon file. + Icon string `json:"icon,omitempty"` +} + +// Name returns the name of the chart. +func (ch *Chart) Name() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.Name +} diff --git a/kubelink/pkg/util/HelmServiceUtil.go b/kubelink/pkg/util/HelmServiceUtil.go index 646cd49e8..90dd87057 100644 --- a/kubelink/pkg/util/HelmServiceUtil.go +++ b/kubelink/pkg/util/HelmServiceUtil.go @@ -21,37 +21,39 @@ import ( "fmt" "github.com/devtron-labs/common-lib/utils/k8s/commonBean" "github.com/devtron-labs/common-lib/utils/k8s/health" - "helm.sh/helm/v3/pkg/release" + "github.com/devtron-labs/kubelink/pkg/service/helmApplicationService/release" + helmRelease "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" ) -// GetAppId returns AppID by logic cluster_id|namespace|release_name +// GetAppId returns AppID by logic +// - format: cluster_id|namespace|release_name func GetAppId(clusterId int32, release *release.Release) string { return fmt.Sprintf("%d|%s|%s", clusterId, release.Namespace, release.Name) } -func GetMessageFromReleaseStatus(releaseStatus release.Status) string { +func GetMessageFromReleaseStatus(releaseStatus helmRelease.Status) string { switch releaseStatus { - case release.StatusUnknown: - return "The release is in an uncertain state" - case release.StatusDeployed: - return "The release has been pushed to Kubernetes" - case release.StatusUninstalled: - return "The release has been uninstalled from Kubernetes" - case release.StatusSuperseded: - return "The release object is outdated and a newer one exists" - case release.StatusFailed: - return "The release was not successfully deployed" - case release.StatusUninstalling: - return "The release uninstall operation is underway" - case release.StatusPendingInstall: - return "The release install operation is underway" - case release.StatusPendingUpgrade: - return "The release upgrade operation is underway" - case release.StatusPendingRollback: - return "The release rollback operation is underway" + case helmRelease.StatusUnknown: + return "The helmRelease is in an uncertain state" + case helmRelease.StatusDeployed: + return "The helmRelease has been pushed to Kubernetes" + case helmRelease.StatusUninstalled: + return "The helmRelease has been uninstalled from Kubernetes" + case helmRelease.StatusSuperseded: + return "The helmRelease object is outdated and a newer one exists" + case helmRelease.StatusFailed: + return "The helmRelease was not successfully deployed" + case helmRelease.StatusUninstalling: + return "The helmRelease uninstall operation is underway" + case helmRelease.StatusPendingInstall: + return "The helmRelease install operation is underway" + case helmRelease.StatusPendingUpgrade: + return "The helmRelease upgrade operation is underway" + case helmRelease.StatusPendingRollback: + return "The helmRelease rollback operation is underway" default: - fmt.Println("un handled release status", releaseStatus) + fmt.Println("un handled helmRelease status", releaseStatus) } return "" diff --git a/kubelink/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/kubelink/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/kubelink/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/kubelink/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/kubelink/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/kubelink/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/kubelink/vendor/modules.txt b/kubelink/vendor/modules.txt index f602b95a5..dff4c8731 100644 --- a/kubelink/vendor/modules.txt +++ b/kubelink/vendor/modules.txt @@ -127,7 +127,7 @@ github.com/cyphar/filepath-securejoin # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/async github.com/devtron-labs/common-lib/constants @@ -1354,7 +1354,7 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.3.0 ## explicit; go 1.12 sigs.k8s.io/yaml -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 # go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.18.0 # k8s.io/api => k8s.io/api v0.29.0 # k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.29.0 diff --git a/kubelink/wire_gen.go b/kubelink/wire_gen.go index 022ee8acc..1b8faef62 100644 --- a/kubelink/wire_gen.go +++ b/kubelink/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject diff --git a/kubewatch/go.mod b/kubewatch/go.mod index 145a09333..b1d2b6617 100644 --- a/kubewatch/go.mod +++ b/kubewatch/go.mod @@ -245,4 +245,4 @@ replace ( k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.29.7 ) -replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/kubewatch/go.sum b/kubewatch/go.sum index b9211517a..72bc8fea2 100644 --- a/kubewatch/go.sum +++ b/kubewatch/go.sum @@ -719,8 +719,8 @@ github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= diff --git a/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/kubewatch/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/kubewatch/vendor/modules.txt b/kubewatch/vendor/modules.txt index 0e03fd429..7015b56fe 100644 --- a/kubewatch/vendor/modules.txt +++ b/kubewatch/vendor/modules.txt @@ -215,7 +215,7 @@ github.com/cyphar/filepath-securejoin # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/async github.com/devtron-labs/common-lib/constants @@ -1760,4 +1760,4 @@ upper.io/db.v3/postgresql # k8s.io/mount-utils => k8s.io/mount-utils v0.29.7 # k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.29.7 # k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.29.7 -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/lens/go.mod b/lens/go.mod index e1694aeb0..d20e4d1a9 100644 --- a/lens/go.mod +++ b/lens/go.mod @@ -59,4 +59,4 @@ require ( github.com/onsi/gomega v1.18.1 // indirect ) -replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +replace github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 diff --git a/lens/go.sum b/lens/go.sum index c576fc93c..4cd5eeaea 100644 --- a/lens/go.sum +++ b/lens/go.sum @@ -22,8 +22,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea h1:76Q2QQCCU/2bwuW0uEEyJpQPlYnm0QqhYmgH7rA8AzU= -github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 h1:MYBgJsIjg4kuSgO/SWxL/JzZi2NUCDqp53Cg5ZZ72xc= +github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946/go.mod h1:ceFKgQ2qm40PR95g5Xp2EClq7nDBKFTcglJ0JdsgClA= github.com/devtron-labs/protos v0.0.3-0.20240130061723-7b2e12ab0abb h1:CkfQQgZc950/hTPqtQSiHV2RmZgkBLGCzwR02FZYjAU= github.com/devtron-labs/protos v0.0.3-0.20240130061723-7b2e12ab0abb/go.mod h1:pjLjgoa1GzbkOkvbMyP4SAKsaiK7eG6GoQCNauG03JA= github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= diff --git a/lens/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go b/lens/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go index ad3cbbda0..17ccda061 100644 --- a/lens/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go +++ b/lens/vendor/github.com/devtron-labs/common-lib/utils/CommonUtils.go @@ -17,13 +17,9 @@ package utils import ( - "errors" "fmt" "github.com/devtron-labs/common-lib/git-manager/util" "github.com/devtron-labs/common-lib/utils/bean" - "github.com/go-pg/pg" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "log" "math/rand" "os" @@ -96,53 +92,6 @@ func BuildDockerImagePath(dockerInfo bean.DockerRegistryInfo) (string, error) { return dest, nil } -func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { - return func(event *pg.QueryProcessedEvent) { - query, err := event.FormattedQuery() - if err != nil { - log.Println("Error formatting query", "err", err) - return - } - ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ - StartTime: event.StartTime, - Error: event.Error, - Query: query, - }) - } -} - -func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { - queryDuration := time.Since(event.StartTime) - var queryError bool - pgError := event.Error - if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) { - queryError = true - } - // Expose prom metrics - if cfg.ExportPromMetrics { - var status string - if queryError { - status = "FAIL" - } else { - status = "SUCCESS" - } - PgQueryDuration.WithLabelValues(status, cfg.ServiceName).Observe(queryDuration.Seconds()) - } - - // Log pg query if enabled - logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold - logFailureQuery := queryError && cfg.LogAllFailureQueries - if logFailureQuery { - log.Println("PG_QUERY_FAIL - query time", "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) - } - if logThresholdQueries { - log.Println("PG_QUERY_SLOW - query time", "duration", queryDuration.Seconds(), "query", event.Query) - } - if cfg.LogAllQuery { - log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) - } -} - func GetSelfK8sUID() string { return os.Getenv(DEVTRON_SELF_POD_UID) } @@ -151,11 +100,6 @@ func GetSelfK8sPodName() string { return os.Getenv(DEVTRON_SELF_POD_NAME) } -var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "pg_query_duration_seconds", - Help: "Duration of PG queries", -}, []string{"status", "serviceName"}) - func ConvertTargetPlatformStringToObject(targetPlatformString string) []*bean.TargetPlatform { targetPlatforms := ConvertTargetPlatformStringToList(targetPlatformString) targetPlatformObject := []*bean.TargetPlatform{} diff --git a/lens/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go b/lens/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go new file mode 100644 index 000000000..3bdec67be --- /dev/null +++ b/lens/vendor/github.com/devtron-labs/common-lib/utils/SqlUtil.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "github.com/devtron-labs/common-lib/utils/bean" + "github.com/go-pg/pg" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "io" + "log" + "net" + "os" + "time" +) + +const ( + PgNetworkErrorLogPrefix string = "PG_NETWORK_ERROR" + PgQueryFailLogPrefix string = "PG_QUERY_FAIL" + PgQuerySlowLogPrefix string = "PG_QUERY_SLOW" +) + +const ( + FAIL string = "FAIL" + SUCCESS string = "SUCCESS" +) + +type ErrorType string + +func (e ErrorType) String() string { + return string(e) +} + +const ( + NetworkErrorType ErrorType = "NETWORK_ERROR" + SyntaxErrorType ErrorType = "SYNTAX_ERROR" + TimeoutErrorType ErrorType = "TIMEOUT_ERROR" + NoErrorType ErrorType = "NA" +) + +func GetPGPostQueryProcessor(cfg bean.PgQueryMonitoringConfig) func(event *pg.QueryProcessedEvent) { + return func(event *pg.QueryProcessedEvent) { + query, err := event.FormattedQuery() + if err != nil { + log.Println("Error formatting query", "err", err) + return + } + ExecutePGQueryProcessor(cfg, bean.PgQueryEvent{ + StartTime: event.StartTime, + Error: event.Error, + Query: query, + FuncName: event.Func, + }) + } +} + +func ExecutePGQueryProcessor(cfg bean.PgQueryMonitoringConfig, event bean.PgQueryEvent) { + queryDuration := time.Since(event.StartTime) + var queryError bool + pgError := event.Error + if pgError != nil && !errors.Is(pgError, pg.ErrNoRows) && !isIntegrityViolationError(pgError) { + queryError = true + } + // Expose prom metrics + if cfg.ExportPromMetrics { + var status string + if queryError { + status = FAIL + } else { + status = SUCCESS + } + PgQueryDuration.WithLabelValues(status, cfg.ServiceName, event.FuncName, getErrorType(pgError).String()).Observe(queryDuration.Seconds()) + } + + // Log pg query if enabled + logThresholdQueries := cfg.LogSlowQuery && queryDuration.Milliseconds() > cfg.QueryDurationThreshold + logNetworkFailure := queryError && cfg.LogAllFailureQueries && isNetworkError(pgError) + if logNetworkFailure { + log.Println(fmt.Sprintf("%s - query time", PgNetworkErrorLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + logFailureQuery := queryError && cfg.LogAllFailureQueries && !isNetworkError(pgError) + if logFailureQuery { + log.Println(fmt.Sprintf("%s - query time", PgQueryFailLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query, "pgError", pgError) + } + if logThresholdQueries { + log.Println(fmt.Sprintf("%s - query time", PgQuerySlowLogPrefix), "duration", queryDuration.Seconds(), "query", event.Query) + } + if cfg.LogAllQuery { + log.Println("query time", "duration", queryDuration.Seconds(), "query", event.Query) + } +} + +var PgQueryDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "pg_query_duration_seconds", + Help: "Duration of PG queries", +}, []string{"status", "serviceName", "functionName", "errorType"}) + +func getErrorType(err error) ErrorType { + if err == nil { + return NoErrorType + } else if errors.Is(err, os.ErrDeadlineExceeded) { + return TimeoutErrorType + } else if isNetworkError(err) { + return NetworkErrorType + } + return SyntaxErrorType +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} + +func isIntegrityViolationError(err error) bool { + pgErr, ok := err.(pg.Error) + if !ok { + return false + } + return pgErr.IntegrityViolation() +} diff --git a/lens/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go b/lens/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go index 50b122e49..ea16a2f72 100644 --- a/lens/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go +++ b/lens/vendor/github.com/devtron-labs/common-lib/utils/bean/bean.go @@ -83,6 +83,7 @@ type PgQueryEvent struct { StartTime time.Time Error error Query string + FuncName string } type TargetPlatform struct { diff --git a/lens/vendor/modules.txt b/lens/vendor/modules.txt index 3ec236f08..12324bcd5 100644 --- a/lens/vendor/modules.txt +++ b/lens/vendor/modules.txt @@ -7,7 +7,7 @@ github.com/caarlos0/env # github.com/cespare/xxhash/v2 v2.2.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2 -# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib v0.0.0 => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946 ## explicit; go 1.21 github.com/devtron-labs/common-lib/constants github.com/devtron-labs/common-lib/fetchAllEnv @@ -289,4 +289,4 @@ google.golang.org/protobuf/types/known/timestamppb # mellium.im/sasl v0.3.2 ## explicit; go 1.20 mellium.im/sasl -# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250411055655-be36c40e11ea +# github.com/devtron-labs/common-lib => github.com/devtron-labs/devtron-services/common-lib v0.0.0-20250429231810-7b844d4e0946