mirror of
https://github.com/harness/drone.git
synced 2025-05-09 05:00:06 +08:00
feat: [CODE-2857]: Add checks API to spaces (#3084)
* Fix stmt reassignment * Add get descendent ids helper to space store * Add checks API to spaces
This commit is contained in:
parent
ab9d78dc7b
commit
18da27f968
63
app/api/controller/check/check_recent_space.go
Normal file
63
app/api/controller/check/check_recent_space.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2023 Harness, 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 check
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/auth"
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
"github.com/harness/gitness/types/enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListRecentChecksSpace return an array of status check UIDs that have been run recently.
|
||||||
|
func (c *Controller) ListRecentChecksSpace(
|
||||||
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
spaceRef string,
|
||||||
|
recursive bool,
|
||||||
|
opts types.CheckRecentOptions,
|
||||||
|
) ([]string, error) {
|
||||||
|
space, err := c.getSpaceCheckAccess(ctx, session, spaceRef, enum.PermissionSpaceEdit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to acquire access to space: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Since == 0 {
|
||||||
|
opts.Since = time.Now().Add(-30 * 24 * time.Hour).UnixMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
var spaceIDs []int64
|
||||||
|
if recursive {
|
||||||
|
spaceIDs, err = c.spaceStore.GetDescendantsIDs(ctx, space.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get space descendants ids: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spaceIDs = append(spaceIDs, space.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIdentifiers, err := c.checkStore.ListRecentSpace(ctx, spaceIDs, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"failed to list status check results for space=%s: %w",
|
||||||
|
space.Identifier, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkIdentifiers, nil
|
||||||
|
}
|
@ -33,6 +33,7 @@ type Controller struct {
|
|||||||
tx dbtx.Transactor
|
tx dbtx.Transactor
|
||||||
authorizer authz.Authorizer
|
authorizer authz.Authorizer
|
||||||
repoStore store.RepoStore
|
repoStore store.RepoStore
|
||||||
|
spaceStore store.SpaceStore
|
||||||
checkStore store.CheckStore
|
checkStore store.CheckStore
|
||||||
git git.Interface
|
git git.Interface
|
||||||
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error
|
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error
|
||||||
@ -42,6 +43,7 @@ func NewController(
|
|||||||
tx dbtx.Transactor,
|
tx dbtx.Transactor,
|
||||||
authorizer authz.Authorizer,
|
authorizer authz.Authorizer,
|
||||||
repoStore store.RepoStore,
|
repoStore store.RepoStore,
|
||||||
|
spaceStore store.SpaceStore,
|
||||||
checkStore store.CheckStore,
|
checkStore store.CheckStore,
|
||||||
git git.Interface,
|
git git.Interface,
|
||||||
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error,
|
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error,
|
||||||
@ -50,6 +52,7 @@ func NewController(
|
|||||||
tx: tx,
|
tx: tx,
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
repoStore: repoStore,
|
repoStore: repoStore,
|
||||||
|
spaceStore: spaceStore,
|
||||||
checkStore: checkStore,
|
checkStore: checkStore,
|
||||||
git: git,
|
git: git,
|
||||||
sanitizers: sanitizers,
|
sanitizers: sanitizers,
|
||||||
@ -74,3 +77,24 @@ func (c *Controller) getRepoCheckAccess(ctx context.Context,
|
|||||||
|
|
||||||
return repo, nil
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getSpaceCheckAccess(
|
||||||
|
ctx context.Context,
|
||||||
|
session *auth.Session,
|
||||||
|
spaceRef string,
|
||||||
|
permission enum.Permission,
|
||||||
|
) (*types.Space, error) {
|
||||||
|
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent space not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := &types.Scope{SpacePath: space.Path}
|
||||||
|
resource := &types.Resource{Type: enum.ResourceTypeRepo}
|
||||||
|
err = apiauth.Check(ctx, c.authorizer, session, scope, resource, permission)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("auth check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return space, nil
|
||||||
|
}
|
||||||
|
@ -35,6 +35,7 @@ func ProvideController(
|
|||||||
tx dbtx.Transactor,
|
tx dbtx.Transactor,
|
||||||
authorizer authz.Authorizer,
|
authorizer authz.Authorizer,
|
||||||
repoStore store.RepoStore,
|
repoStore store.RepoStore,
|
||||||
|
spaceStore store.SpaceStore,
|
||||||
checkStore store.CheckStore,
|
checkStore store.CheckStore,
|
||||||
rpcClient git.Interface,
|
rpcClient git.Interface,
|
||||||
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error,
|
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error,
|
||||||
@ -43,6 +44,7 @@ func ProvideController(
|
|||||||
tx,
|
tx,
|
||||||
authorizer,
|
authorizer,
|
||||||
repoStore,
|
repoStore,
|
||||||
|
spaceStore,
|
||||||
checkStore,
|
checkStore,
|
||||||
rpcClient,
|
rpcClient,
|
||||||
sanitizers,
|
sanitizers,
|
||||||
|
59
app/api/handler/check/check_recent_space.go
Normal file
59
app/api/handler/check/check_recent_space.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2023 Harness, 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 pullreq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/app/api/controller/check"
|
||||||
|
"github.com/harness/gitness/app/api/render"
|
||||||
|
"github.com/harness/gitness/app/api/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleCheckListRecentSpace is an HTTP handler for listing recently executed status checks for a space.
|
||||||
|
func HandleCheckListRecentSpace(checkCtrl *check.Controller) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
session, _ := request.AuthSessionFrom(ctx)
|
||||||
|
|
||||||
|
spaceRef, err := request.GetSpaceRefFromPath(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := request.ParseCheckRecentOptions(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recursive, err := request.ParseRecursiveFromQuery(r)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIdentifiers, err := checkCtrl.ListRecentChecksSpace(
|
||||||
|
ctx, session, spaceRef, recursive, opts,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
render.TranslatedUserError(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, http.StatusOK, checkIdentifiers)
|
||||||
|
}
|
||||||
|
}
|
@ -222,7 +222,7 @@ func setupRoutesV1WithAuth(r chi.Router,
|
|||||||
capabilitiesCtrl *capabilities.Controller,
|
capabilitiesCtrl *capabilities.Controller,
|
||||||
) {
|
) {
|
||||||
setupAccountWithAuth(r, userCtrl, config)
|
setupAccountWithAuth(r, userCtrl, config)
|
||||||
setupSpaces(r, appCtx, spaceCtrl, userGroupCtrl, webhookCtrl)
|
setupSpaces(r, appCtx, spaceCtrl, userGroupCtrl, webhookCtrl, checkCtrl)
|
||||||
setupRepos(r, repoCtrl, repoSettingsCtrl, pipelineCtrl, executionCtrl, triggerCtrl,
|
setupRepos(r, repoCtrl, repoSettingsCtrl, pipelineCtrl, executionCtrl, triggerCtrl,
|
||||||
logCtrl, pullreqCtrl, webhookCtrl, checkCtrl, uploadCtrl)
|
logCtrl, pullreqCtrl, webhookCtrl, checkCtrl, uploadCtrl)
|
||||||
setupConnectors(r, connectorCtrl)
|
setupConnectors(r, connectorCtrl)
|
||||||
@ -248,7 +248,7 @@ func setupSpaces(
|
|||||||
spaceCtrl *space.Controller,
|
spaceCtrl *space.Controller,
|
||||||
userGroupCtrl *usergroup.Controller,
|
userGroupCtrl *usergroup.Controller,
|
||||||
webhookCtrl *webhook.Controller,
|
webhookCtrl *webhook.Controller,
|
||||||
|
checkCtrl *check.Controller,
|
||||||
) {
|
) {
|
||||||
r.Route("/spaces", func(r chi.Router) {
|
r.Route("/spaces", func(r chi.Router) {
|
||||||
// Create takes path and parentId via body, not uri
|
// Create takes path and parentId via body, not uri
|
||||||
@ -294,6 +294,8 @@ func setupSpaces(
|
|||||||
SetupSpaceLabels(r, spaceCtrl)
|
SetupSpaceLabels(r, spaceCtrl)
|
||||||
SetupWebhookSpace(r, webhookCtrl)
|
SetupWebhookSpace(r, webhookCtrl)
|
||||||
SetupRulesSpace(r, spaceCtrl)
|
SetupRulesSpace(r, spaceCtrl)
|
||||||
|
|
||||||
|
r.Get("/checks", handlercheck.HandleCheckListRecentSpace(checkCtrl))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -196,6 +196,9 @@ type (
|
|||||||
// GetDescendantsData returns a list of space parent data for spaces that are descendants of the space.
|
// GetDescendantsData returns a list of space parent data for spaces that are descendants of the space.
|
||||||
GetDescendantsData(ctx context.Context, spaceID int64) ([]types.SpaceParentData, error)
|
GetDescendantsData(ctx context.Context, spaceID int64) ([]types.SpaceParentData, error)
|
||||||
|
|
||||||
|
// GetDescendantsIDs returns a list of space ids for spaces that are descendants of the specified space.
|
||||||
|
GetDescendantsIDs(ctx context.Context, spaceID int64) ([]int64, error)
|
||||||
|
|
||||||
// Create creates a new space
|
// Create creates a new space
|
||||||
Create(ctx context.Context, space *types.Space) error
|
Create(ctx context.Context, space *types.Space) error
|
||||||
|
|
||||||
@ -646,7 +649,19 @@ type (
|
|||||||
List(ctx context.Context, repoID int64, commitSHA string, opts types.CheckListOptions) ([]types.Check, error)
|
List(ctx context.Context, repoID int64, commitSHA string, opts types.CheckListOptions) ([]types.Check, error)
|
||||||
|
|
||||||
// ListRecent returns a list of recently executed status checks in a repository.
|
// ListRecent returns a list of recently executed status checks in a repository.
|
||||||
ListRecent(ctx context.Context, repoID int64, opts types.CheckRecentOptions) ([]string, error)
|
ListRecent(
|
||||||
|
ctx context.Context,
|
||||||
|
repoID int64,
|
||||||
|
opts types.CheckRecentOptions,
|
||||||
|
) ([]string, error)
|
||||||
|
|
||||||
|
// ListRecentSpace returns a list of recently executed status checks in
|
||||||
|
// repositories in spaces with specified space IDs.
|
||||||
|
ListRecentSpace(
|
||||||
|
ctx context.Context,
|
||||||
|
spaceIDs []int64,
|
||||||
|
opts types.CheckRecentOptions,
|
||||||
|
) ([]string, error)
|
||||||
|
|
||||||
// ListResults returns a list of status check results for a specific commit in a repo.
|
// ListResults returns a list of status check results for a specific commit in a repo.
|
||||||
ListResults(ctx context.Context, repoID int64, commitSHA string) ([]types.CheckResult, error)
|
ListResults(ctx context.Context, repoID int64, commitSHA string) ([]types.CheckResult, error)
|
||||||
|
@ -248,16 +248,42 @@ func (s *CheckStore) List(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListRecent returns a list of recently executed status checks in a repository.
|
// ListRecent returns a list of recently executed status checks in a repository.
|
||||||
func (s *CheckStore) ListRecent(ctx context.Context,
|
func (s *CheckStore) ListRecent(
|
||||||
|
ctx context.Context,
|
||||||
repoID int64,
|
repoID int64,
|
||||||
opts types.CheckRecentOptions,
|
opts types.CheckRecentOptions,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
stmt := database.Builder.
|
stmt := database.Builder.
|
||||||
Select("distinct check_uid").
|
Select("distinct check_uid").
|
||||||
From("checks").
|
From("checks").
|
||||||
Where("check_repo_id = ?", repoID).
|
Where("check_created > ?", opts.Since).
|
||||||
Where("check_created > ?", opts.Since)
|
Where("check_repo_id = ?", repoID)
|
||||||
|
|
||||||
|
return s.listRecent(ctx, stmt, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRecentSpace returns a list of recently executed status checks in
|
||||||
|
// repositories in spaces with specified space IDs.
|
||||||
|
func (s *CheckStore) ListRecentSpace(
|
||||||
|
ctx context.Context,
|
||||||
|
spaceIDs []int64,
|
||||||
|
opts types.CheckRecentOptions,
|
||||||
|
) ([]string, error) {
|
||||||
|
stmt := database.Builder.
|
||||||
|
Select("distinct check_uid").
|
||||||
|
From("checks").
|
||||||
|
Join("repositories ON checks.check_repo_id = repositories.repo_id").
|
||||||
|
Where("check_created > ?", opts.Since).
|
||||||
|
Where(squirrel.Eq{"repositories.repo_parent_id": spaceIDs})
|
||||||
|
|
||||||
|
return s.listRecent(ctx, stmt, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckStore) listRecent(
|
||||||
|
ctx context.Context,
|
||||||
|
stmt squirrel.SelectBuilder,
|
||||||
|
opts types.CheckRecentOptions,
|
||||||
|
) ([]string, error) {
|
||||||
stmt = s.applyOpts(stmt, opts.Query)
|
stmt = s.applyOpts(stmt, opts.Query)
|
||||||
|
|
||||||
stmt = stmt.OrderBy("check_uid")
|
stmt = stmt.OrderBy("check_uid")
|
||||||
@ -267,10 +293,9 @@ func (s *CheckStore) ListRecent(ctx context.Context,
|
|||||||
return nil, fmt.Errorf("failed to convert list recent status checks query to sql: %w", err)
|
return nil, fmt.Errorf("failed to convert list recent status checks query to sql: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := make([]string, 0)
|
|
||||||
|
|
||||||
db := dbtx.GetAccessor(ctx, s.db)
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
|
dst := make([]string, 0)
|
||||||
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
||||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to execute list recent status checks query")
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to execute list recent status checks query")
|
||||||
}
|
}
|
||||||
|
@ -562,15 +562,14 @@ func (s *RepoStore) countAll(
|
|||||||
parentID int64,
|
parentID int64,
|
||||||
filter *types.RepoFilter,
|
filter *types.RepoFilter,
|
||||||
) (int64, error) {
|
) (int64, error) {
|
||||||
query := spaceDescendantsQuery + `
|
|
||||||
SELECT space_descendant_id
|
|
||||||
FROM space_descendants`
|
|
||||||
|
|
||||||
db := dbtx.GetAccessor(ctx, s.db)
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
var spaceIDs []int64
|
spaceIDs, err := getSpaceDescendantsIDs(ctx, db, parentID)
|
||||||
if err := db.SelectContext(ctx, &spaceIDs, query, parentID); err != nil {
|
if err != nil {
|
||||||
return 0, database.ProcessSQLErrorf(ctx, err, "failed to retrieve spaces")
|
return 0, fmt.Errorf(
|
||||||
|
"failed to get space descendants ids for %d: %w",
|
||||||
|
parentID, err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt := database.Builder.
|
stmt := database.Builder.
|
||||||
@ -691,15 +690,14 @@ func (s *RepoStore) listAll(
|
|||||||
parentID int64,
|
parentID int64,
|
||||||
filter *types.RepoFilter,
|
filter *types.RepoFilter,
|
||||||
) ([]*types.Repository, error) {
|
) ([]*types.Repository, error) {
|
||||||
query := spaceDescendantsQuery + `
|
|
||||||
SELECT space_descendant_id
|
|
||||||
FROM space_descendants`
|
|
||||||
|
|
||||||
db := dbtx.GetAccessor(ctx, s.db)
|
db := dbtx.GetAccessor(ctx, s.db)
|
||||||
|
|
||||||
var spaceIDs []int64
|
spaceIDs, err := getSpaceDescendantsIDs(ctx, db, parentID)
|
||||||
if err := db.SelectContext(ctx, &spaceIDs, query, parentID); err != nil {
|
if err != nil {
|
||||||
return nil, database.ProcessSQLErrorf(ctx, err, "failed to retrieve spaces")
|
return nil, fmt.Errorf(
|
||||||
|
"failed to get space descendants ids for %d: %w",
|
||||||
|
parentID, err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt := database.Builder.
|
stmt := database.Builder.
|
||||||
|
@ -336,6 +336,24 @@ func (s *SpaceStore) GetDescendantsData(ctx context.Context, spaceID int64) ([]t
|
|||||||
return s.readParentsData(ctx, query, spaceID)
|
return s.readParentsData(ctx, query, spaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDescendantsIDs returns a list of space ids for spaces that are descendants of the specified space.
|
||||||
|
func (s *SpaceStore) GetDescendantsIDs(ctx context.Context, spaceID int64) ([]int64, error) {
|
||||||
|
return getSpaceDescendantsIDs(ctx, dbtx.GetAccessor(ctx, s.db), spaceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpaceDescendantsIDs(ctx context.Context, db dbtx.Accessor, spaceID int64) ([]int64, error) {
|
||||||
|
query := spaceDescendantsQuery + `
|
||||||
|
SELECT space_descendant_id
|
||||||
|
FROM space_descendants`
|
||||||
|
|
||||||
|
var ids []int64
|
||||||
|
if err := db.SelectContext(ctx, &ids, query, spaceID); err != nil {
|
||||||
|
return nil, database.ProcessSQLErrorf(ctx, err, "failed to retrieve spaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SpaceStore) readParentsData(
|
func (s *SpaceStore) readParentsData(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
query string,
|
query string,
|
||||||
|
@ -406,7 +406,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||||||
principalController := principal.ProvideController(principalStore, authorizer)
|
principalController := principal.ProvideController(principalStore, authorizer)
|
||||||
usergroupController := usergroup2.ProvideController(userGroupStore, spaceStore, authorizer, searchService)
|
usergroupController := usergroup2.ProvideController(userGroupStore, spaceStore, authorizer, searchService)
|
||||||
v := check2.ProvideCheckSanitizers()
|
v := check2.ProvideCheckSanitizers()
|
||||||
checkController := check2.ProvideController(transactor, authorizer, repoStore, checkStore, gitInterface, v)
|
checkController := check2.ProvideController(transactor, authorizer, repoStore, spaceStore, checkStore, gitInterface, v)
|
||||||
systemController := system.NewController(principalStore, config)
|
systemController := system.NewController(principalStore, config)
|
||||||
blobConfig, err := server.ProvideBlobStoreConfig(config)
|
blobConfig, err := server.ProvideBlobStoreConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user