-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathfedorasourceprovider.go
More file actions
370 lines (309 loc) · 12.5 KB
/
fedorasourceprovider.go
File metadata and controls
370 lines (309 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package sourceproviders
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"time"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders/fedorasource"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/git"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/retry"
)
// FedoraSourcesProviderImpl implements [ComponentSourceProvider] for Git repositories.
type FedoraSourcesProviderImpl struct {
fs opctx.FS
dryRunnable opctx.DryRunnable
gitProvider git.GitProvider
downloader fedorasource.FedoraSourceDownloader
distroGitBaseURI string
distroGitBranch string
lookasideBaseURI string
snapshotTime string
retryConfig retry.Config
}
var _ ComponentSourceProvider = (*FedoraSourcesProviderImpl)(nil)
func NewFedoraSourcesProviderImpl(
fs opctx.FS,
dryRunnable opctx.DryRunnable,
gitProvider git.GitProvider,
downloader fedorasource.FedoraSourceDownloader,
distro ResolvedDistro,
retryCfg retry.Config,
) (*FedoraSourcesProviderImpl, error) {
if fs == nil {
return nil, errors.New("filesystem cannot be nil")
}
if dryRunnable == nil {
return nil, errors.New("dryRunnable cannot be nil")
}
if gitProvider == nil {
return nil, errors.New("git provider cannot be nil")
}
if downloader == nil {
return nil, errors.New("downloader cannot be nil")
}
if distro.Definition.DistGitBaseURI == "" {
return nil, errors.New("resolved distro must specify a dist-git base URI")
}
if distro.Version.DistGitBranch == "" {
return nil, errors.New("resolved distro must specify a dist-git branch")
}
if distro.Definition.LookasideBaseURI == "" {
return nil, errors.New("resolved distro must specify a lookaside base URI")
}
return &FedoraSourcesProviderImpl{
fs: fs,
dryRunnable: dryRunnable,
gitProvider: gitProvider,
downloader: downloader,
distroGitBaseURI: distro.Definition.DistGitBaseURI,
distroGitBranch: distro.Version.DistGitBranch,
lookasideBaseURI: distro.Definition.LookasideBaseURI,
snapshotTime: distro.Ref.Snapshot,
retryConfig: retryCfg,
}, nil
}
func (g *FedoraSourcesProviderImpl) GetComponent(
ctx context.Context, component components.Component, destDirPath string, opts ...FetchComponentOption,
) (err error) {
resolved := resolveFetchComponentOptions(opts)
componentName := component.GetName()
if componentName == "" {
return errors.New("component name cannot be empty")
}
upstreamNameToUse := componentName
// Figure out if there's an override for the upstream name. This can happen when the derived
// component name differs from the name used in the upstream distro.
if upstreamNameOverride := component.GetConfig().Spec.UpstreamName; upstreamNameOverride != "" {
upstreamNameToUse = upstreamNameOverride
}
if destDirPath == "" {
return errors.New("destination path cannot be empty")
}
gitRepoURL, err := fedorasource.BuildDistGitURL(g.distroGitBaseURI, upstreamNameToUse)
if err != nil {
return fmt.Errorf("failed to build dist-git URL for %#q:\n%w", upstreamNameToUse, err)
}
slog.Info("Getting component from git repo",
"component", componentName,
"upstreamComponent", upstreamNameToUse,
"branch", g.distroGitBranch,
"upstreamCommit", component.GetConfig().Spec.UpstreamCommit,
"snapshot", g.snapshotTime)
// Clone to a temp directory first, then copy files to destination.
tempDir, err := fileutils.MkdirTempInTempDir(g.fs, "azldev-clone-")
if err != nil {
return fmt.Errorf("failed to create temp directory for clone:\n%w", err)
}
defer fileutils.RemoveAllAndUpdateErrorIfNil(g.fs, tempDir, &err)
// Clone the repository to temp directory with retry for transient network failures.
err = retry.Do(ctx, g.retryConfig, func() error {
// Clean up temp directory contents from any prior failed clone attempt.
_ = g.fs.RemoveAll(tempDir)
_ = fileutils.MkdirAll(g.fs, tempDir)
return g.gitProvider.Clone(ctx, gitRepoURL, tempDir, git.WithGitBranch(g.distroGitBranch))
})
if err != nil {
return fmt.Errorf("failed to clone git repository %#q:\n%w", gitRepoURL, err)
}
// Collect filenames from source-files config so the lookaside extractor can skip them.
// These files were already fetched by FetchFiles and take precedence over upstream versions.
sourceFiles := component.GetConfig().SourceFiles
skipFileNames := make([]string, len(sourceFiles))
for i := range sourceFiles {
skipFileNames[i] = sourceFiles[i].Filename
}
// Process the cloned repo: checkout target commit, extract sources, copy to destination.
return g.processClonedRepo(ctx, component.GetConfig().Spec.UpstreamCommit,
tempDir, upstreamNameToUse, componentName, destDirPath, skipFileNames, resolved)
}
// processClonedRepo handles the post-clone steps: checking out the target commit,
// extracting lookaside sources, renaming spec files, and copying to the destination.
func (g *FedoraSourcesProviderImpl) processClonedRepo(
ctx context.Context,
upstreamCommit string,
tempDir, upstreamName, componentName, destDirPath string,
skipFilenames []string,
opts FetchComponentOptions,
) error {
// Checkout the appropriate commit based on component/distro config
if err := g.checkoutTargetCommit(ctx, upstreamCommit, tempDir); err != nil {
return fmt.Errorf("failed to checkout target commit:\n%w", err)
}
// Delete the .git directory so it's not copied to destination, unless the caller
// requested that it be preserved (e.g., for synthetic history generation).
if !opts.PreserveGitDir {
if err := g.fs.RemoveAll(filepath.Join(tempDir, ".git")); err != nil {
return fmt.Errorf("failed to remove .git directory from cloned repository at %#q:\n%w",
tempDir, err)
}
}
// Extract sources from repo (downloads lookaside files into the temp dir).
// Files in skipFilenames are not downloaded — they were already fetched by FetchFiles.
// Skip this step entirely when SkipLookaside is set (e.g., during rendering).
if !opts.SkipLookaside {
err := g.downloader.ExtractSourcesFromRepo(
ctx, tempDir, upstreamName, g.lookasideBaseURI, skipFilenames,
)
if err != nil {
return fmt.Errorf("failed to extract sources from git repository:\n%w", err)
}
}
// If the upstream name differs from the component name, rename the spec in temp dir.
if err := g.renameSpecIfNeeded(tempDir, upstreamName, componentName); err != nil {
return err
}
// Copy files from temp dir to destination, skipping files that already exist.
// This preserves any files downloaded by FetchFiles, giving them precedence.
copyOptions := fileutils.CopyDirOptions{
CopyFileOptions: fileutils.CopyFileOptions{
PreserveFileMode: true,
},
FileFilter: fileutils.SkipExistingFiles,
}
if err := fileutils.CopyDirRecursive(g.dryRunnable, g.fs, tempDir, destDirPath, copyOptions); err != nil {
return fmt.Errorf("failed to copy files to destination:\n%w", err)
}
return nil
}
// renameSpecIfNeeded renames the spec file in the given directory if the upstream name
// differs from the desired component name.
func (g *FedoraSourcesProviderImpl) renameSpecIfNeeded(dir, upstreamName, componentName string) error {
if upstreamName == componentName {
return nil
}
downloadedSpecPath := filepath.Join(dir, upstreamName+".spec")
desiredSpecPath := filepath.Join(dir, componentName+".spec")
err := g.fs.Rename(downloadedSpecPath, desiredSpecPath)
if err != nil {
return fmt.Errorf("failed to rename fetched spec file from %#q to %#q:\n%w",
downloadedSpecPath, desiredSpecPath, err)
}
return nil
}
// checkoutTargetCommit resolves the effective commit via [resolveEffectiveCommitHash]
// and checks it out in the cloned repository.
func (g *FedoraSourcesProviderImpl) checkoutTargetCommit(
ctx context.Context,
upstreamCommit string,
repoDir string,
) error {
commitHash, err := g.resolveEffectiveCommitHash(ctx, repoDir, upstreamCommit, slog.LevelInfo)
if err != nil {
return err
}
if err := g.gitProvider.Checkout(ctx, repoDir, commitHash); err != nil {
return fmt.Errorf("failed to checkout commit %#q:\n%w", commitHash, err)
}
return nil
}
// ResolveIdentity implements [SourceIdentityProvider] by resolving the upstream
// commit hash for the component. All resolution priority logic is in
// [resolveEffectiveCommitHash], called via [resolveCommit].
func (g *FedoraSourcesProviderImpl) ResolveIdentity(
ctx context.Context,
component components.Component,
) (string, error) {
if component.GetName() == "" {
return "", errors.New("component name cannot be empty")
}
upstreamName := component.GetConfig().Spec.UpstreamName
if upstreamName == "" {
upstreamName = component.GetName()
}
gitRepoURL, err := fedorasource.BuildDistGitURL(g.distroGitBaseURI, upstreamName)
if err != nil {
return "", fmt.Errorf("failed to build dist-git URL for %#q:\n%w", upstreamName, err)
}
return g.resolveCommit(ctx, gitRepoURL, upstreamName, component.GetConfig().Spec.UpstreamCommit)
}
// resolveCommit determines the effective commit via [resolveEffectiveCommitHash].
// For pinned commits (case 1), it returns immediately without cloning. For snapshot
// and HEAD cases, it performs a metadata-only clone to resolve the commit hash.
func (g *FedoraSourcesProviderImpl) resolveCommit(
ctx context.Context, gitRepoURL string, upstreamName string, upstreamCommit string,
) (string, error) {
// Case 1: Explicit upstream commit hash specified per-component
if upstreamCommit != "" {
return g.resolveEffectiveCommitHash(ctx, "", upstreamCommit, slog.LevelDebug)
}
// Cases 2 & 3: need a metadata-only clone to resolve snapshot or HEAD commit.
tempDir, err := fileutils.MkdirTempInTempDir(g.fs, "azldev-identity-snapshot-")
if err != nil {
return "", fmt.Errorf("creating temp directory for snapshot clone:\n%w", err)
}
defer func() {
if removeErr := g.fs.RemoveAll(tempDir); removeErr != nil {
slog.Debug("Failed to clean up snapshot clone temp directory",
"path", tempDir, "error", removeErr)
}
}()
// Clone a single branch to resolve the snapshot commit. We use a full
// (non-shallow) clone because not all git servers support --shallow-since
// (e.g., Pagure returns "the remote end hung up unexpectedly").
err = retry.Do(ctx, g.retryConfig, func() error {
_ = g.fs.RemoveAll(tempDir)
_ = fileutils.MkdirAll(g.fs, tempDir)
return g.gitProvider.Clone(ctx, gitRepoURL, tempDir,
git.WithGitBranch(g.distroGitBranch),
git.WithMetadataOnly(),
git.WithQuiet(),
)
})
if err != nil {
return "", fmt.Errorf("partial clone for identity of %#q:\n%w", upstreamName, err)
}
commitHash, err := g.resolveEffectiveCommitHash(ctx, tempDir, "", slog.LevelDebug)
if err != nil {
return "", fmt.Errorf("resolving commit for %#q:\n%w", upstreamName, err)
}
return commitHash, nil
}
// resolveEffectiveCommitHash is the single source of truth for which commit a
// component should use from a cloned repository.
//
// Priority:
// 1. Explicit upstream commit hash (pinned per-component).
// 2. Snapshot time — commit immediately before the snapshot date.
// 3. Default — current HEAD.
func (g *FedoraSourcesProviderImpl) resolveEffectiveCommitHash(
ctx context.Context,
repoDir string,
upstreamCommit string,
logLevel slog.Level,
) (string, error) {
// Case 1: Explicit upstream commit hash specified per-component.
if upstreamCommit != "" {
slog.Log(ctx, logLevel, "Using explicit upstream commit hash", "commitHash", upstreamCommit)
return upstreamCommit, nil
}
// Case 2: Provider has a snapshot time configured from the resolved distro.
if g.snapshotTime != "" {
snapshotDateTime, err := time.Parse(time.RFC3339, g.snapshotTime)
if err != nil {
return "", fmt.Errorf("invalid snapshot time %#q:\n%w", g.snapshotTime, err)
}
commitHash, err := g.gitProvider.GetCommitHashBeforeDate(ctx, repoDir, snapshotDateTime)
if err != nil {
return "", fmt.Errorf("resolving commit for snapshot time %s:\n%w",
snapshotDateTime.Format(time.RFC3339), err)
}
slog.Log(ctx, logLevel, "Using upstream distro snapshot time",
"snapshotDateTime", snapshotDateTime.Format(time.RFC3339),
"commitHash", commitHash)
return commitHash, nil
}
// Case 3: Default — use current HEAD.
commitHash, err := g.gitProvider.GetCurrentCommit(ctx, repoDir)
if err != nil {
return "", fmt.Errorf("resolving current HEAD commit:\n%w", err)
}
slog.Log(ctx, logLevel, "Using current HEAD", "commitHash", commitHash)
return commitHash, nil
}