github.com/argoproj/argo-cd/v3@v3.2.1/commitserver/commit/commit.go (about) 1 package commit 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "time" 9 10 "github.com/argoproj/argo-cd/v3/controller/hydrator" 11 12 log "github.com/sirupsen/logrus" 13 14 "github.com/argoproj/argo-cd/v3/commitserver/apiclient" 15 "github.com/argoproj/argo-cd/v3/commitserver/metrics" 16 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 17 "github.com/argoproj/argo-cd/v3/util/git" 18 "github.com/argoproj/argo-cd/v3/util/io" 19 "github.com/argoproj/argo-cd/v3/util/io/files" 20 ) 21 22 // Service is the service that handles commit requests. 23 type Service struct { 24 metricsServer *metrics.Server 25 repoClientFactory RepoClientFactory 26 } 27 28 // NewService returns a new instance of the commit service. 29 func NewService(gitCredsStore git.CredsStore, metricsServer *metrics.Server) *Service { 30 return &Service{ 31 metricsServer: metricsServer, 32 repoClientFactory: NewRepoClientFactory(gitCredsStore, metricsServer), 33 } 34 } 35 36 type hydratorMetadataFile struct { 37 RepoURL string `json:"repoURL,omitempty"` 38 DrySHA string `json:"drySha,omitempty"` 39 Commands []string `json:"commands,omitempty"` 40 Author string `json:"author,omitempty"` 41 Date string `json:"date,omitempty"` 42 // Subject is the subject line of the DRY commit message, i.e. `git show --format=%s`. 43 Subject string `json:"subject,omitempty"` 44 // Body is the body of the DRY commit message, excluding the subject line, i.e. `git show --format=%b`. 45 // Known Argocd- trailers with valid values are removed, but all other trailers are kept. 46 Body string `json:"body,omitempty"` 47 References []v1alpha1.RevisionReference `json:"references,omitempty"` 48 } 49 50 // TODO: make this configurable via ConfigMap. 51 var manifestHydrationReadmeTemplate = `# Manifest Hydration 52 53 To hydrate the manifests in this repository, run the following commands: 54 55 ` + "```shell" + ` 56 git clone {{ .RepoURL }} 57 # cd into the cloned directory 58 git checkout {{ .DrySHA }} 59 {{ range $command := .Commands -}} 60 {{ $command }} 61 {{ end -}}` + "```" + ` 62 {{ if .References -}} 63 64 ## References 65 66 {{ range $ref := .References -}} 67 {{ if $ref.Commit -}} 68 * [{{ $ref.Commit.SHA | mustRegexFind "[0-9a-f]+" | trunc 7 }}]({{ $ref.Commit.RepoURL }}): {{ $ref.Commit.Subject }} ({{ $ref.Commit.Author }}) 69 {{ end -}} 70 {{ end -}} 71 {{ end -}}` 72 73 // CommitHydratedManifests handles a commit request. It clones the repository, checks out the sync branch, checks out 74 // the target branch, clears the repository contents, writes the manifests to the repository, commits the changes, and 75 // pushes the changes. It returns the hydrated revision SHA and an error if one occurred. 76 func (s *Service) CommitHydratedManifests(_ context.Context, r *apiclient.CommitHydratedManifestsRequest) (*apiclient.CommitHydratedManifestsResponse, error) { 77 // This method is intentionally short. It's a wrapper around handleCommitRequest that adds metrics and logging. 78 // Keep logic here minimal and put most of the logic in handleCommitRequest. 79 startTime := time.Now() 80 81 // We validate for a nil repo in handleCommitRequest, but we need to check for a nil repo here to get the repo URL 82 // for metrics. 83 var repoURL string 84 if r.Repo != nil { 85 repoURL = r.Repo.Repo 86 } 87 88 var err error 89 s.metricsServer.IncPendingCommitRequest(repoURL) 90 defer func() { 91 s.metricsServer.DecPendingCommitRequest(repoURL) 92 commitResponseType := metrics.CommitResponseTypeSuccess 93 if err != nil { 94 commitResponseType = metrics.CommitResponseTypeFailure 95 } 96 s.metricsServer.IncCommitRequest(repoURL, commitResponseType) 97 s.metricsServer.ObserveCommitRequestDuration(repoURL, commitResponseType, time.Since(startTime)) 98 }() 99 100 logCtx := log.WithFields(log.Fields{"branch": r.TargetBranch, "drySHA": r.DrySha}) 101 102 out, sha, err := s.handleCommitRequest(logCtx, r) 103 if err != nil { 104 logCtx.WithError(err).WithField("output", out).Error("failed to handle commit request") 105 106 // No need to wrap this error, sufficient context is build in handleCommitRequest. 107 return &apiclient.CommitHydratedManifestsResponse{}, err 108 } 109 110 logCtx.Info("Successfully handled commit request") 111 return &apiclient.CommitHydratedManifestsResponse{ 112 HydratedSha: sha, 113 }, nil 114 } 115 116 // handleCommitRequest handles the commit request. It clones the repository, checks out the sync branch, checks out the 117 // target branch, clears the repository contents, writes the manifests to the repository, commits the changes, and pushes 118 // the changes. It returns the output of the git commands and an error if one occurred. 119 func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydratedManifestsRequest) (string, string, error) { 120 if r.Repo == nil { 121 return "", "", errors.New("repo is required") 122 } 123 if r.Repo.Repo == "" { 124 return "", "", errors.New("repo URL is required") 125 } 126 if r.TargetBranch == "" { 127 return "", "", errors.New("target branch is required") 128 } 129 if r.SyncBranch == "" { 130 return "", "", errors.New("sync branch is required") 131 } 132 133 logCtx = logCtx.WithField("repo", r.Repo.Repo) 134 logCtx.Debug("Initiating git client") 135 gitClient, dirPath, cleanup, err := s.initGitClient(logCtx, r) 136 if err != nil { 137 return "", "", fmt.Errorf("failed to init git client: %w", err) 138 } 139 defer cleanup() 140 141 root, err := os.OpenRoot(dirPath) 142 if err != nil { 143 return "", "", fmt.Errorf("failed to open root dir: %w", err) 144 } 145 defer io.Close(root) 146 147 logCtx.Debugf("Checking out sync branch %s", r.SyncBranch) 148 var out string 149 out, err = gitClient.CheckoutOrOrphan(r.SyncBranch, false) 150 if err != nil { 151 return out, "", fmt.Errorf("failed to checkout sync branch: %w", err) 152 } 153 154 logCtx.Debugf("Checking out target branch %s", r.TargetBranch) 155 out, err = gitClient.CheckoutOrNew(r.TargetBranch, r.SyncBranch, false) 156 if err != nil { 157 return out, "", fmt.Errorf("failed to checkout target branch: %w", err) 158 } 159 160 logCtx.Debug("Clearing and preparing paths") 161 var pathsToClear []string 162 // range over the paths configured and skip those application 163 // paths that are referencing to root path 164 for _, p := range r.Paths { 165 if hydrator.IsRootPath(p.Path) { 166 // skip adding paths that are referencing root directory 167 logCtx.Debugf("Path %s is referencing root directory, ignoring the path", p.Path) 168 continue 169 } 170 pathsToClear = append(pathsToClear, p.Path) 171 } 172 173 if len(pathsToClear) > 0 { 174 logCtx.Debugf("Clearing paths: %v", pathsToClear) 175 out, err := gitClient.RemoveContents(pathsToClear) 176 if err != nil { 177 return out, "", fmt.Errorf("failed to clear paths %v: %w", pathsToClear, err) 178 } 179 } 180 181 logCtx.Debug("Writing manifests") 182 err = WriteForPaths(root, r.Repo.Repo, r.DrySha, r.DryCommitMetadata, r.Paths) 183 if err != nil { 184 return "", "", fmt.Errorf("failed to write manifests: %w", err) 185 } 186 187 logCtx.Debug("Committing and pushing changes") 188 out, err = gitClient.CommitAndPush(r.TargetBranch, r.CommitMessage) 189 if err != nil { 190 return out, "", fmt.Errorf("failed to commit and push: %w", err) 191 } 192 193 logCtx.Debug("Getting commit SHA") 194 sha, err := gitClient.CommitSHA() 195 if err != nil { 196 return "", "", fmt.Errorf("failed to get commit SHA: %w", err) 197 } 198 199 return "", sha, nil 200 } 201 202 // initGitClient initializes a git client for the given repository and returns the client, the path to the directory where 203 // the repository is cloned, a cleanup function that should be called when the directory is no longer needed, and an error 204 // if one occurred. 205 func (s *Service) initGitClient(logCtx *log.Entry, r *apiclient.CommitHydratedManifestsRequest) (git.Client, string, func(), error) { 206 dirPath, err := files.CreateTempDir("/tmp/_commit-service") 207 if err != nil { 208 return nil, "", nil, fmt.Errorf("failed to create temp dir: %w", err) 209 } 210 // Call cleanupOrLog in this function if an error occurs to ensure the temp dir is cleaned up. 211 cleanupOrLog := func() { 212 err := os.RemoveAll(dirPath) 213 if err != nil { 214 logCtx.WithError(err).Error("failed to cleanup temp dir") 215 } 216 } 217 218 gitClient, err := s.repoClientFactory.NewClient(r.Repo, dirPath) 219 if err != nil { 220 cleanupOrLog() 221 return nil, "", nil, fmt.Errorf("failed to create git client: %w", err) 222 } 223 224 logCtx.Debugf("Initializing repo %s", r.Repo.Repo) 225 err = gitClient.Init() 226 if err != nil { 227 cleanupOrLog() 228 return nil, "", nil, fmt.Errorf("failed to init git client: %w", err) 229 } 230 231 logCtx.Debugf("Fetching repo %s", r.Repo.Repo) 232 err = gitClient.Fetch("") 233 if err != nil { 234 cleanupOrLog() 235 return nil, "", nil, fmt.Errorf("failed to clone repo: %w", err) 236 } 237 238 // FIXME: make it work for GHE 239 // logCtx.Debugf("Getting user info for repo credentials") 240 // gitCreds := r.Repo.GetGitCreds(s.gitCredsStore) 241 // startTime := time.Now() 242 // authorName, authorEmail, err := gitCreds.GetUserInfo(ctx) 243 // s.metricsServer.ObserveUserInfoRequestDuration(r.Repo.Repo, getCredentialType(r.Repo), time.Since(startTime)) 244 // if err != nil { 245 // cleanupOrLog() 246 // return nil, "", nil, fmt.Errorf("failed to get github app info: %w", err) 247 // } 248 var authorName, authorEmail string 249 250 if authorName == "" { 251 authorName = "Argo CD" 252 } 253 if authorEmail == "" { 254 logCtx.Warnf("Author email not available, using 'argo-cd@example.com'.") 255 authorEmail = "argo-cd@example.com" 256 } 257 258 logCtx.Debugf("Setting author %s <%s>", authorName, authorEmail) 259 _, err = gitClient.SetAuthor(authorName, authorEmail) 260 if err != nil { 261 cleanupOrLog() 262 return nil, "", nil, fmt.Errorf("failed to set author: %w", err) 263 } 264 265 return gitClient, dirPath, cleanupOrLog, nil 266 }