diff --git a/cmd/adhoc.go b/cmd/adhoc.go index cd22bb6e7..d84b5c6fd 100644 --- a/cmd/adhoc.go +++ b/cmd/adhoc.go @@ -7,11 +7,8 @@ import ( "github.com/runatlantis/atlantis/server/legacy" "github.com/runatlantis/atlantis/server/logging" adhoc "github.com/runatlantis/atlantis/server/neptune/adhoc" - adhocHelpers "github.com/runatlantis/atlantis/server/neptune/adhoc/adhocexecutionhelpers" adhocconfig "github.com/runatlantis/atlantis/server/neptune/adhoc/config" neptune "github.com/runatlantis/atlantis/server/neptune/temporalworker/config" - "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github" - "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform" ) type Adhoc struct{} @@ -58,23 +55,21 @@ func (a *Adhoc) NewServer(userConfig legacy.UserConfig, config legacy.Config) (S DownloadURL: userConfig.TFDownloadURL, LogFilters: globalCfg.TerraformLogFilter, }, - DataDir: userConfig.DataDir, - TemporalCfg: globalCfg.Temporal, - GithubCfg: globalCfg.Github, - App: appConfig, - CtxLogger: ctxLogger, - StatsNamespace: userConfig.StatsNamespace, - Metrics: globalCfg.Metrics, - AdhocExecutionParams: adhocHelpers.AdhocTerraformWorkflowExecutionParams{ - Revision: "", - TerraformRoots: []terraform.Root{}, - GithubRepo: github.Repo{}, - }, + DataDir: userConfig.DataDir, + TemporalCfg: globalCfg.Temporal, + GithubCfg: globalCfg.Github, + App: appConfig, + CtxLogger: ctxLogger, + StatsNamespace: userConfig.StatsNamespace, + Metrics: globalCfg.Metrics, GithubHostname: userConfig.GithubHostname, GithubAppID: userConfig.GithubAppID, GithubAppKeyFile: userConfig.GithubAppKeyFile, GithubAppSlug: userConfig.GithubAppSlug, GlobalCfg: globalCfg, + GithubUser: userConfig.GithubUser, + GithubToken: userConfig.GithubToken, + JobConfig: globalCfg.PersistenceConfig.Jobs, } return adhoc.NewServer(cfg) } diff --git a/server/neptune/adhoc/adhocexecutionhelpers/adhoc_execution_params.go b/server/neptune/adhoc/adhocexecutionhelpers/adhoc_execution_params.go index ba22b0afa..cae5502de 100644 --- a/server/neptune/adhoc/adhocexecutionhelpers/adhoc_execution_params.go +++ b/server/neptune/adhoc/adhocexecutionhelpers/adhoc_execution_params.go @@ -4,12 +4,12 @@ import ( "context" "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/neptune/adhoc/adhocgithubhelpers" "github.com/runatlantis/atlantis/server/neptune/gateway/config" root_config "github.com/runatlantis/atlantis/server/neptune/gateway/config" "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github" "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform" internal_gh "github.com/runatlantis/atlantis/server/vcs/provider/github" + "github.com/runatlantis/atlantis/server/vcs/provider/github/converter" ) type AdhocTerraformWorkflowExecutionParams struct { @@ -19,11 +19,29 @@ type AdhocTerraformWorkflowExecutionParams struct { // Note that deploymentID is used in NewWorkflowStore(), but we don't care about that in adhoc mode so can leave it blank } -func ConstructAdhocExecParamsWithRootCfgBuilderAndRepoRetriever(ctx context.Context, repoName string, revision string, githubRetriever *adhocgithubhelpers.AdhocGithubRetriever, rootCfgBuilder *root_config.Builder) (AdhocTerraformWorkflowExecutionParams, error) { +func ConstructAdhocExecParams( + ctx context.Context, + repoName string, + PRNum int, + pullFetcher *internal_gh.PRFetcher, + pullConverter converter.PullConverter, + installationRetriever *internal_gh.InstallationRetriever, + rootCfgBuilder *root_config.Builder) (AdhocTerraformWorkflowExecutionParams, error) { + orgName := "lyft" + installationToken, err := installationRetriever.FindOrganizationInstallation(ctx, orgName) + if err != nil { + return AdhocTerraformWorkflowExecutionParams{}, errors.Wrap(err, "finding organization installation") + } + // TODO: in the future, could potentially pass in the owner instead of hardcoding lyft - repo, err := githubRetriever.GetRepository(ctx, "lyft", repoName) + ghCommit, err := pullFetcher.Fetch(ctx, installationToken.Token, orgName, repoName, PRNum) if err != nil { - return AdhocTerraformWorkflowExecutionParams{}, errors.Wrap(err, "getting repo") + return AdhocTerraformWorkflowExecutionParams{}, errors.Wrap(err, "fetching commit") + } + + actualCommit, err := pullConverter.Convert(ghCommit) + if err != nil { + return AdhocTerraformWorkflowExecutionParams{}, errors.Wrap(err, "converting commit") } opts := config.BuilderOptions{ @@ -33,7 +51,12 @@ func ConstructAdhocExecParamsWithRootCfgBuilderAndRepoRetriever(ctx context.Cont }, } - rootCfgs, err := rootCfgBuilder.Build(ctx, &root_config.RepoCommit{}, repo.Credentials.InstallationToken, opts) + rootCfgs, err := rootCfgBuilder.Build(ctx, &root_config.RepoCommit{ + Repo: actualCommit.HeadRepo, + Branch: actualCommit.HeadBranch, + Sha: actualCommit.HeadCommit, + OptionalPRNum: actualCommit.Num, + }, installationToken.Token, opts) if err != nil { return AdhocTerraformWorkflowExecutionParams{}, errors.Wrap(err, "building root cfgs") } @@ -41,8 +64,14 @@ func ConstructAdhocExecParamsWithRootCfgBuilderAndRepoRetriever(ctx context.Cont roots := getRootsFromMergedProjectCfgs(rootCfgs) return AdhocTerraformWorkflowExecutionParams{ - Revision: revision, - GithubRepo: repo, + Revision: actualCommit.HeadCommit, + GithubRepo: github.Repo{ + Owner: orgName, + Name: repoName, + URL: actualCommit.HeadRepo.CloneURL, + DefaultBranch: actualCommit.HeadRepo.DefaultBranch, + Credentials: github.AppCredentials{InstallationToken: installationToken.Token}, + }, TerraformRoots: roots, }, nil } diff --git a/server/neptune/adhoc/adhocgithubhelpers/adhoc_github_helpers.go b/server/neptune/adhoc/adhocgithubhelpers/adhoc_github_helpers.go deleted file mode 100644 index fb0854c57..000000000 --- a/server/neptune/adhoc/adhocgithubhelpers/adhoc_github_helpers.go +++ /dev/null @@ -1,50 +0,0 @@ -package adhocgithubhelpers - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/models" - "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github" - internal "github.com/runatlantis/atlantis/server/vcs/provider/github" -) - -type repoRetriever interface { - Get(ctx context.Context, installationToken int64, owner, repo string) (models.Repo, error) -} - -type installationRetriever interface { - FindOrganizationInstallation(ctx context.Context, org string) (internal.Installation, error) -} - -type AdhocGithubRetriever struct { - RepoRetriever repoRetriever - InstallationRetriever installationRetriever -} - -func (r *AdhocGithubRetriever) GetRepository(ctx context.Context, owner string, repoName string) (github.Repo, error) { - installation, err := r.InstallationRetriever.FindOrganizationInstallation(ctx, owner) - if err != nil { - return github.Repo{}, errors.Wrap(err, "finding installation") - } - - repo, err := r.RepoRetriever.Get(ctx, installation.Token, owner, repoName) - if err != nil { - return github.Repo{}, errors.Wrap(err, "getting repo") - } - - if len(repo.DefaultBranch) == 0 { - return github.Repo{}, fmt.Errorf("default branch was nil, this is a bug on github's side") - } - - return github.Repo{ - Owner: repo.Owner, - Name: repo.Name, - URL: repo.CloneURL, - DefaultBranch: repo.DefaultBranch, - Credentials: github.AppCredentials{ - InstallationToken: installation.Token, - }, - }, nil -} diff --git a/server/neptune/adhoc/config/config.go b/server/neptune/adhoc/config/config.go index c017e37ec..064495121 100644 --- a/server/neptune/adhoc/config/config.go +++ b/server/neptune/adhoc/config/config.go @@ -4,7 +4,6 @@ import ( "github.com/palantir/go-githubapp/githubapp" "github.com/runatlantis/atlantis/server/config/valid" "github.com/runatlantis/atlantis/server/logging" - adhoc "github.com/runatlantis/atlantis/server/neptune/adhoc/adhocexecutionhelpers" neptune "github.com/runatlantis/atlantis/server/neptune/temporalworker/config" ) @@ -18,17 +17,19 @@ type Config struct { TerraformCfg neptune.TerraformConfig Metrics valid.Metrics + JobConfig valid.StoreConfig StatsNamespace string - DataDir string - CtxLogger logging.Logger - App githubapp.Config - AdhocExecutionParams adhoc.AdhocTerraformWorkflowExecutionParams + DataDir string + CtxLogger logging.Logger + App githubapp.Config GithubAppID int64 GithubAppKeyFile string GithubAppSlug string GithubHostname string + GithubUser string + GithubToken string GlobalCfg valid.GlobalCfg } diff --git a/server/neptune/adhoc/server.go b/server/neptune/adhoc/server.go index ed3d21529..171bc0ce5 100644 --- a/server/neptune/adhoc/server.go +++ b/server/neptune/adhoc/server.go @@ -15,10 +15,11 @@ import ( "github.com/palantir/go-githubapp/githubapp" "github.com/runatlantis/atlantis/server/legacy/events/vcs" - "github.com/runatlantis/atlantis/server/neptune/lyft/feature" "github.com/runatlantis/atlantis/server/neptune/sync/crons" ghClient "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github" + "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform" "github.com/runatlantis/atlantis/server/vcs/provider/github" + "github.com/runatlantis/atlantis/server/vcs/provider/github/converter" assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/gorilla/mux" @@ -26,7 +27,6 @@ import ( "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" adhoc "github.com/runatlantis/atlantis/server/neptune/adhoc/adhocexecutionhelpers" - adhocGithubHelpers "github.com/runatlantis/atlantis/server/neptune/adhoc/adhocgithubhelpers" adhocconfig "github.com/runatlantis/atlantis/server/neptune/adhoc/config" root_config "github.com/runatlantis/atlantis/server/neptune/gateway/config" "github.com/runatlantis/atlantis/server/neptune/gateway/deploy" @@ -38,28 +38,31 @@ import ( "github.com/runatlantis/atlantis/server/neptune/workflows" "github.com/runatlantis/atlantis/server/neptune/workflows/activities" "github.com/runatlantis/atlantis/server/static" - github_converter "github.com/runatlantis/atlantis/server/vcs/provider/github/converter" "github.com/uber-go/tally/v4" "github.com/urfave/negroni" + "go.temporal.io/sdk/client" "go.temporal.io/sdk/interceptor" "go.temporal.io/sdk/worker" ) type Server struct { - Logger logging.Logger - CronScheduler *internalSync.CronScheduler - Crons []*internalSync.Cron - HTTPServerProxy *neptune_http.ServerProxy - Port int - StatsScope tally.Scope - StatsCloser io.Closer - TemporalClient *temporal.ClientWrapper - TerraformActivities *activities.Terraform - GithubActivities *activities.Github - AdhocExecutionParams adhoc.AdhocTerraformWorkflowExecutionParams - TerraformTaskQueue string - RootConfigBuilder *root_config.Builder - GithubRetriever *adhocGithubHelpers.AdhocGithubRetriever + Logger logging.Logger + CronScheduler *internalSync.CronScheduler + Crons []*internalSync.Cron + HTTPServerProxy *neptune_http.ServerProxy + Port int + StatsScope tally.Scope + StatsCloser io.Closer + TemporalClient *temporal.ClientWrapper + TerraformActivities *activities.Terraform + GithubActivities *activities.Github + TerraformTaskQueue string + RootConfigBuilder *root_config.Builder + Repo string + PRNum int + InstallationRetriever *github.InstallationRetriever + PullFetcher *github.PRFetcher + PullConverter converter.PullConverter } func NewServer(config *adhocconfig.Config) (*Server, error) { @@ -126,33 +129,14 @@ func NewServer(config *adhocconfig.Config) (*Server, error) { return nil, errors.Wrap(err, "client creator") } - repoConfig := feature.RepoConfig{ - Owner: config.FeatureConfig.FFOwner, - Repo: config.FeatureConfig.FFRepo, - Branch: config.FeatureConfig.FFBranch, - Path: config.FeatureConfig.FFPath, - } installationFetcher := &github.InstallationRetriever{ ClientCreator: clientCreator, } - fileFetcher := &github.SingleFileContentsFetcher{ - ClientCreator: clientCreator, - } - retriever := &feature.CustomGithubInstallationRetriever{ - InstallationFetcher: installationFetcher, - FileContentsFetcher: fileFetcher, - Cfg: repoConfig, - } - featureAllocator, err := feature.NewGHSourcedAllocator(retriever, config.CtxLogger) - if err != nil { - return nil, errors.Wrap(err, "initializing feature allocator") - } - githubActivities, err := activities.NewGithub( clientCreator, config.GithubCfg.TemporalAppInstallationID, config.DataDir, - featureAllocator, + nil, ) if err != nil { return nil, errors.Wrap(err, "initializing github activities") @@ -199,16 +183,17 @@ func NewServer(config *adhocconfig.Config) (*Server, error) { Scope: scope.SubScope("event.filters.root"), } - repoConverter := github_converter.RepoConverter{} - repoRetriever := &github.RepoRetriever{ + pullFetcher := &github.PRFetcher{ ClientCreator: clientCreator, - RepoConverter: repoConverter, } - // This exists to convert a repo name to a repo object - githubRetriever := &adhocGithubHelpers.AdhocGithubRetriever{ - RepoRetriever: repoRetriever, - InstallationRetriever: installationFetcher, + repoConverter := converter.RepoConverter{ + GithubUser: config.GithubUser, + GithubToken: config.GithubToken, + } + + pullConverter := converter.PullConverter{ + RepoConverter: repoConverter, } server := Server{ @@ -220,21 +205,53 @@ func NewServer(config *adhocconfig.Config) (*Server, error) { Frequency: 1 * time.Minute, }, }, - HTTPServerProxy: httpServerProxy, - Port: config.ServerCfg.Port, - StatsScope: scope, - StatsCloser: statsCloser, - TemporalClient: temporalClient, - TerraformActivities: terraformActivities, - TerraformTaskQueue: config.TemporalCfg.TerraformTaskQueue, - GithubActivities: githubActivities, - AdhocExecutionParams: config.AdhocExecutionParams, - RootConfigBuilder: rootConfigBuilder, - GithubRetriever: githubRetriever, + HTTPServerProxy: httpServerProxy, + Port: config.ServerCfg.Port, + StatsScope: scope, + StatsCloser: statsCloser, + TemporalClient: temporalClient, + TerraformActivities: terraformActivities, + TerraformTaskQueue: config.TemporalCfg.TerraformTaskQueue, + GithubActivities: githubActivities, + RootConfigBuilder: rootConfigBuilder, + Repo: config.GlobalCfg.AdhocMode.Repo, + PRNum: config.GlobalCfg.AdhocMode.PRNum, + InstallationRetriever: installationFetcher, + PullFetcher: pullFetcher, + PullConverter: pullConverter, } return &server, nil } +// This function constructs the request we want to send to the temporal client, +// then executes the Terraform workflow. Note normally this workflow is executed +// when a request is made to the server, but we are manually executing it here, +// since we don't care about requests in adhoc mode. +func (s Server) manuallyExecuteTerraformWorkflow(repo ghClient.Repo, revision string, root terraform.Root) (interface{}, error) { + deploymentIDStr := revision + "-" + root.Name + "-" + repo.Name + request := workflows.TerraformRequest{ + Revision: revision, + WorkflowMode: terraform.Adhoc, + Root: root, + Repo: repo, + DeploymentID: deploymentIDStr, + } + options := client.StartWorkflowOptions{ + TaskQueue: s.TerraformTaskQueue, + SearchAttributes: map[string]interface{}{ + "atlantis_repository": repo.GetFullName(), + "atlantis_root": root.Name, + }, + } + + res, err := s.TemporalClient.ExecuteWorkflow(context.Background(), options, workflows.Terraform, request) + if err != nil { + s.Logger.Error(err.Error()) + return nil, err + } + return res, nil +} + func (s Server) Start() error { defer s.shutdown() @@ -258,6 +275,24 @@ func (s Server) Start() error { s.Logger.InfoContext(ctx, "Shutting down terraform worker, resource clean up may still be occurring in the background") }() + wg.Add(1) + go func() { + defer wg.Done() + + adhocExecutionParams, err := adhoc.ConstructAdhocExecParams(ctx, s.Repo, s.PRNum, s.PullFetcher, s.PullConverter, s.InstallationRetriever, s.RootConfigBuilder) + if err != nil { + s.Logger.Error(err.Error()) + return + } + + for _, root := range adhocExecutionParams.TerraformRoots { + _, err := s.manuallyExecuteTerraformWorkflow(adhocExecutionParams.GithubRepo, adhocExecutionParams.Revision, root) + if err != nil { + s.Logger.Error(err.Error()) + } + } + }() + // Ensure server gracefully drains connections when stopped. stop := make(chan os.Signal, 1) // Stop on SIGINTs and SIGTERMs. diff --git a/server/neptune/exec/exec.go b/server/neptune/exec/exec.go index 2c640860e..fd078bf27 100644 --- a/server/neptune/exec/exec.go +++ b/server/neptune/exec/exec.go @@ -2,13 +2,14 @@ package exec import ( "context" - "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/logging" - key "github.com/runatlantis/atlantis/server/neptune/context" "os" "os/exec" "syscall" "time" + + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/logging" + key "github.com/runatlantis/atlantis/server/neptune/context" ) type Cmd struct { @@ -48,7 +49,7 @@ func (c *Cmd) RunWithNewProcessGroup(ctx context.Context) error { err := c.Wait() if ctx.Err() != nil { - return errors.Wrap(ctx.Err(), "waiting for process") + return errors.Wrap(ctx.Err(), "context error, waiting for process") } if err != nil { return errors.Wrap(err, "waiting for process") diff --git a/server/neptune/gateway/config/root_config_builder.go b/server/neptune/gateway/config/root_config_builder.go index 30108fc00..b63c1af91 100644 --- a/server/neptune/gateway/config/root_config_builder.go +++ b/server/neptune/gateway/config/root_config_builder.go @@ -54,7 +54,8 @@ func (s *ModifiedRootsStrategy) FindMatches(ctx context.Context, config valid.Re Sha: repo.RepoCommit.Sha, }) if err != nil { - return nil, errors.Wrapf(err, "finding modified files: %s", modifiedFiles) + debugStr := fmt.Sprintf("sha: %s, prNum: %d, dir %s", repo.RepoCommit.Sha, repo.RepoCommit.OptionalPRNum, repo.Dir) + return nil, errors.Wrapf(err, "finding modified files: %s, debug str: %s", modifiedFiles, debugStr) } matchingRoots, err := s.RootFinder.FindRoots(ctx, config, repo.Dir, modifiedFiles) @@ -131,6 +132,7 @@ func (b *Builder) build(ctx context.Context, commit *RepoCommit, installationTok RepoCommit: commit, Dir: repoDir, } + b.Logger.Info(fmt.Sprintf("localRepo is: full repo name: %s, commit sha: %s, commit branch: %s, commit repo name: %s, repodir: %s", commit.Repo.FullName, commit.Sha, commit.Branch, commit.Repo.Name, repoDir)) // Run pre-workflow hooks err = b.HooksRunner.Run(ctx, localRepo.Repo, localRepo.Dir) @@ -152,6 +154,7 @@ func (b *Builder) build(ctx context.Context, commit *RepoCommit, installationTok return nil, errors.Wrap(err, "getting matching roots") } + b.Logger.Info(fmt.Sprintf("merging roots for %s", localRepo.Repo.FullName)) for _, mr := range matchingRoots { mergedRootCfg := b.GlobalCfg.MergeProjectCfg(localRepo.Repo.ID(), mr, repoCfg) mergedRootCfgs = append(mergedRootCfgs, &mergedRootCfg) diff --git a/server/neptune/workflows/internal/terraform/workflow.go b/server/neptune/workflows/internal/terraform/workflow.go index eff184f32..0c547c4d1 100644 --- a/server/neptune/workflows/internal/terraform/workflow.go +++ b/server/neptune/workflows/internal/terraform/workflow.go @@ -346,6 +346,7 @@ func (r *Runner) run(ctx workflow.Context) (Response, error) { if err != nil { return Response{}, r.toExternalError(err, "fetching root") } + defer func() { r.executeCleanup(ctx, cleanup) }() diff --git a/server/neptune/workflows/internal/terraform/workflow_test.go b/server/neptune/workflows/internal/terraform/workflow_test.go index 450310cda..f0fbc97b7 100644 --- a/server/neptune/workflows/internal/terraform/workflow_test.go +++ b/server/neptune/workflows/internal/terraform/workflow_test.go @@ -623,7 +623,7 @@ func TestSuccess_PRMode(t *testing.T) { }, resp.States) } -func TestSuccess_AdminMode(t *testing.T) { +func TestSuccess_AdhocMode(t *testing.T) { var suite testsuite.WorkflowTestSuite env := suite.NewTestWorkflowEnvironment() ga := &githubActivities{} diff --git a/server/vcs/provider/github/repo_fetcher.go b/server/vcs/provider/github/repo_fetcher.go index 5b487ee65..514e59e4b 100644 --- a/server/vcs/provider/github/repo_fetcher.go +++ b/server/vcs/provider/github/repo_fetcher.go @@ -62,12 +62,14 @@ func (g *RepoFetcher) Fetch(ctx context.Context, repo models.Repo, branch string authURL := fmt.Sprintf("://x-access-token:%s", ghToken) repo.CloneURL = strings.Replace(repo.CloneURL, "://:", authURL, 1) repo.SanitizedCloneURL = strings.Replace(repo.SanitizedCloneURL, "://:", "://x-access-token:", 1) + g.Logger.Info(fmt.Sprintf("about to clone inside RepoFetcher Fetch with params: repo: %v. branch: %s, sha: %s", repo, branch, sha)) path, cleanup, err := g.clone(ctx, repo, branch, sha, options) if err != nil { g.Scope.Counter(metrics.ExecutionErrorMetric).Inc(1) return path, cleanup, err } g.Scope.Counter(metrics.ExecutionSuccessMetric).Inc(1) + g.Logger.Info(fmt.Sprintf("cloned repo %s to path %s", repo.Name, path)) return path, cleanup, err } @@ -91,7 +93,8 @@ func (g *RepoFetcher) clone(ctx context.Context, repo models.Repo, branch string } _, err := g.run(ctx, cloneCmd, destinationPath) if err != nil { - return "", nil, errors.Wrap(err, "failed to clone directory") + debugStr := fmt.Sprintf("destination path is %s, repo is %v, sha is %v", destinationPath, repo, sha) + return "", nil, errors.Wrap(err, "failed to clone directory, debug info: "+debugStr) } // Return immediately if commit at HEAD of clone matches request commit @@ -137,12 +140,13 @@ func (g *RepoFetcher) run(ctx context.Context, args []string, destinationPath st cmd.Stderr = &b err := cmd.RunWithNewProcessGroup(ctx) if err != nil { - return nil, errors.Wrap(err, "running command in separate process group") + return nil, errors.Wrap(err, "running command in separate process group, command is "+cmd.String()) } return b.Bytes(), nil } func (g *RepoFetcher) Cleanup(ctx context.Context, filePath string) { + g.Logger.Info(fmt.Sprintf("cleaning up cloned repo at path %s", filePath)) if err := os.RemoveAll(filePath); err != nil { g.Logger.ErrorContext(ctx, "failed deleting cloned repo", map[string]interface{}{ "err": err,