Skip to content

Commit f0bd1bf

Browse files
Antonio Salinasreubeno
authored andcommitted
fix(source preparation): Source Files Download Ordering (#501)
1 parent c43f48c commit f0bd1bf

File tree

17 files changed

+827
-373
lines changed

17 files changed

+827
-373
lines changed

internal/app/azldev/cmds/component/build.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,14 @@ func BuildComponent(
181181
workDirFactory *workdir.Factory,
182182
options *ComponentBuildOptions,
183183
) (results ComponentBuildResults, err error) {
184-
sourceManager, err := sourceproviders.NewSourceManager(env)
184+
// Resolve the effective distro for this component before creating the source manager.
185+
distro, err := sourceproviders.ResolveDistro(env, component)
186+
if err != nil {
187+
return ComponentBuildResults{},
188+
fmt.Errorf("failed to resolve distro for component %q:\n%w", component.GetName(), err)
189+
}
190+
191+
sourceManager, err := sourceproviders.NewSourceManager(env, distro)
185192
if err != nil {
186193
return ComponentBuildResults{},
187194
fmt.Errorf("failed to setup source retrieval manager for component %q:\n%w", component.GetName(), err)

internal/app/azldev/cmds/component/diffsources.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ func DiffComponentSources(env *azldev.Env, options *DiffSourcesOptions) (interfa
8686
event := env.StartEvent("Diffing sources", "component", component.GetName())
8787
defer event.End()
8888

89-
sourceManager, err := sourceproviders.NewSourceManager(env)
89+
// Resolve the effective distro for this component before creating the source manager.
90+
distro, err := sourceproviders.ResolveDistro(env, component)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to resolve distro for component %q:\n%w", component.GetName(), err)
93+
}
94+
95+
sourceManager, err := sourceproviders.NewSourceManager(env, distro)
9096
if err != nil {
9197
return nil, fmt.Errorf("failed to create source manager:\n%w", err)
9298
}

internal/app/azldev/cmds/component/preparesources.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,14 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
9797

9898
var sourceManager sourceproviders.SourceManager
9999

100+
// Resolve the effective distro for this component before creating the source manager.
101+
distro, err := sourceproviders.ResolveDistro(env, component)
102+
if err != nil {
103+
return fmt.Errorf("failed to resolve distro for component %#q:\n%w", component.GetName(), err)
104+
}
105+
100106
// Create source manager to handle all source fetching, both local and upstream.
101-
sourceManager, err = sourceproviders.NewSourceManager(env)
107+
sourceManager, err = sourceproviders.NewSourceManager(env, distro)
102108
if err != nil {
103109
return fmt.Errorf("failed to create source manager:\n%w", err)
104110
}

internal/projectconfig/component.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
)
3939

4040
// Origin describes where a source file comes from and how to retrieve it.
41+
// When omitted from a source file reference, the file will be resolved via the lookaside cache.
4142
type Origin struct {
4243
// Type indicates how the source file should be acquired.
4344
Type OriginType `toml:"type" json:"type" jsonschema:"required,enum=download,title=Origin type,description=Type of origin for this source file"`
@@ -59,8 +60,8 @@ type SourceFileReference struct {
5960
// Type of hash used by Hash (e.g., "sha256", "sha512").
6061
HashType fileutils.HashType `toml:"hash-type,omitempty" json:"hashType,omitempty"`
6162

62-
// Type of origin for this source file (e.g., URI, custom).
63-
Origin Origin `toml:"origin" json:"origin"`
63+
// Origin for this source file. When omitted, the file is resolved via the lookaside cache.
64+
Origin Origin `toml:"origin,omitempty" json:"origin,omitempty"`
6465
}
6566

6667
// Defines a component group. Component groups are logical groupings of components (see [ComponentConfig]).

internal/projectconfig/distro.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type DistroDefinition struct {
5353
// Published artifact information
5454
PackageRepositories []PackageRepository `toml:"repos,omitempty" json:"repos,omitempty" jsonschema:"title=Package Repositories,description=List of package repository definitions"`
5555

56+
// When true, source file downloads will not fall back to configured origins if the lookaside cache fails.
57+
DisableOrigins bool `toml:"disable-origins,omitempty" json:"disableOrigins,omitempty" jsonschema:"title=Disable origins,description=When true only allow source files from the lookaside cache and do not fall back to configured origins"`
58+
5659
// Versions: maps version => definition
5760
Versions map[string]DistroVersionDefinition `toml:"versions,omitempty" json:"versions,omitempty" jsonschema:"title=Versions,description=Mapping of distro version definitions"`
5861
}

internal/providers/sourceproviders/fedorasource/fedorasource.go

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import (
2222

2323
type FedoraSourceDownloader interface {
2424
// ExtractSourcesFromRepo processes a git repository by downloading any required
25-
// lookaside cache files into the repository directory.
26-
ExtractSourcesFromRepo(ctx context.Context, repoDir string, packageName string, lookasideBaseURI string) error
25+
// lookaside cache files into the repository directory. Files whose names appear
26+
// in skipFilenames are not downloaded (e.g., files already fetched separately).
27+
ExtractSourcesFromRepo(
28+
ctx context.Context, repoDir string, packageName string,
29+
lookasideBaseURI string, skipFilenames []string,
30+
) error
2731
}
2832

2933
// FedoraSourceDownloaderImpl is an implementation of GitRepoExtractor.
@@ -107,7 +111,7 @@ func NewFedoraRepoExtractorImpl(
107111
// ExtractSourcesFromRepo processes the git repository by downloading any required
108112
// lookaside cache files into the repository directory.
109113
func (g *FedoraSourceDownloaderImpl) ExtractSourcesFromRepo(
110-
ctx context.Context, repoDir string, packageName string, lookasideBaseURI string,
114+
ctx context.Context, repoDir string, packageName string, lookasideBaseURI string, skipFileNames []string,
111115
) error {
112116
if repoDir == "" {
113117
return errors.New("repository directory cannot be empty")
@@ -117,10 +121,6 @@ func (g *FedoraSourceDownloaderImpl) ExtractSourcesFromRepo(
117121
return errors.New("lookaside base URI cannot be empty")
118122
}
119123

120-
if err := verifyFedoraLookasideBaseURI(lookasideBaseURI); err != nil {
121-
return err
122-
}
123-
124124
repoDirExists, err := fileutils.Exists(g.fileSystem, repoDir)
125125
if err != nil {
126126
return fmt.Errorf("failed to check if repository directory exists at %#q:\n%w", repoDir, err)
@@ -152,7 +152,12 @@ func (g *FedoraSourceDownloaderImpl) ExtractSourcesFromRepo(
152152
return fmt.Errorf("failed to parse sources file at %#q:\n%w", sourcesFilePath, err)
153153
}
154154

155-
err = g.downloadAndVerifySources(ctx, sourceFiles, repoDir)
155+
skipSet := make(map[string]bool, len(skipFileNames))
156+
for _, name := range skipFileNames {
157+
skipSet[name] = true
158+
}
159+
160+
err = g.downloadAndVerifySources(ctx, sourceFiles, repoDir, skipSet)
156161
if err != nil {
157162
return fmt.Errorf("failed to download sources:\n%w", err)
158163
}
@@ -164,9 +169,17 @@ func (g *FedoraSourceDownloaderImpl) downloadAndVerifySources(
164169
ctx context.Context,
165170
sourceFiles []sourceFileInfo,
166171
repoDir string,
172+
skipSet map[string]bool,
167173
) error {
168174
sourcesTotal := len(sourceFiles)
169175
for sourceIndex, sourceFile := range sourceFiles {
176+
if skipSet[sourceFile.fileName] {
177+
slog.Debug("File already provided, skipping lookaside download",
178+
"fileName", sourceFile.fileName)
179+
180+
continue
181+
}
182+
170183
destFilePath := filepath.Join(repoDir, sourceFile.fileName)
171184

172185
exists, err := fileutils.Exists(g.fileSystem, destFilePath)
@@ -268,11 +281,10 @@ func parseSourcesFile(content string, packageName string, lookasideBaseURI strin
268281
hashType = "MD5"
269282
}
270283

271-
sourceURI := lookasideBaseURI
272-
sourceURI = strings.ReplaceAll(sourceURI, "$pkg", packageName)
273-
sourceURI = strings.ReplaceAll(sourceURI, "$filename", fileName)
274-
sourceURI = strings.ReplaceAll(sourceURI, "$hashtype", strings.ToLower(hashType))
275-
sourceURI = strings.ReplaceAll(sourceURI, "$hash", hash)
284+
sourceURI, err := BuildLookasideURL(lookasideBaseURI, packageName, fileName, hashType, hash)
285+
if err != nil {
286+
return nil, fmt.Errorf("failed to build lookaside URL at line %d:\n%w", lineNum+1, err)
287+
}
276288

277289
sourceFiles = append(sourceFiles, sourceFileInfo{
278290
fileName: fileName,
@@ -285,14 +297,42 @@ func parseSourcesFile(content string, packageName string, lookasideBaseURI strin
285297
return sourceFiles, nil
286298
}
287299

288-
func verifyFedoraLookasideBaseURI(lookasideBaseURI string) error {
289-
// Check for placeholder variables in the lookaside URI
290-
requiredPlaceholders := []string{"$pkg", "$filename", "$hashtype", "$hash"}
291-
for _, placeholder := range requiredPlaceholders {
292-
if !strings.Contains(lookasideBaseURI, placeholder) {
293-
return fmt.Errorf("lookaside base URI is missing required placeholder: %s", placeholder)
300+
// Lookaside URI template placeholders supported by [BuildLookasideURL].
301+
const (
302+
// PlaceholderPkg is replaced with the package name.
303+
PlaceholderPkg = "$pkg"
304+
// PlaceholderFilename is replaced with the source file name.
305+
PlaceholderFilename = "$filename"
306+
// PlaceholderHashType is replaced with the lowercase hash algorithm (e.g., "sha512").
307+
PlaceholderHashType = "$hashtype"
308+
// PlaceholderHash is replaced with the hash value.
309+
PlaceholderHash = "$hash"
310+
)
311+
312+
// BuildLookasideURL constructs a lookaside cache URL by substituting placeholders in the
313+
// URI template with the provided values. Supported placeholders are [PlaceholderPkg],
314+
// [PlaceholderFilename], [PlaceholderHashType], and [PlaceholderHash].
315+
// Placeholders not present in the template are simply ignored.
316+
//
317+
// Returns an error if any of the provided values contain a placeholder string, as this
318+
// would cause ambiguous substitution results depending on replacement order.
319+
func BuildLookasideURL(template, packageName, fileName, hashType, hash string) (string, error) {
320+
// allPlaceholders lists all supported lookaside URI template placeholders.
321+
allPlaceholders := []string{PlaceholderPkg, PlaceholderFilename, PlaceholderHashType, PlaceholderHash}
322+
323+
for _, v := range []string{packageName, fileName, hashType, hash} {
324+
for _, p := range allPlaceholders {
325+
if strings.Contains(v, p) {
326+
return "", fmt.Errorf("value %#q contains placeholder %s, which would cause ambiguous substitution", v, p)
327+
}
294328
}
295329
}
296330

297-
return nil
331+
uri := template
332+
uri = strings.ReplaceAll(uri, PlaceholderPkg, packageName)
333+
uri = strings.ReplaceAll(uri, PlaceholderFilename, fileName)
334+
uri = strings.ReplaceAll(uri, PlaceholderHashType, strings.ToLower(hashType))
335+
uri = strings.ReplaceAll(uri, PlaceholderHash, hash)
336+
337+
return uri, nil
298338
}

internal/providers/sourceproviders/fedorasource/fedorasource_test.go

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func TestExtractSourcesFromRepo(t *testing.T) {
110110
return afero.WriteFile(ctx.FS(), destPath, []byte("test patch content"), testFilePerms)
111111
})
112112

113-
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI)
113+
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI, nil)
114114
require.NoError(t, err)
115115
}
116116

@@ -123,13 +123,13 @@ func TestExtractSourcesFromRepoValidation(t *testing.T) {
123123
require.NoError(t, err)
124124

125125
t.Run("empty repo dir", func(t *testing.T) {
126-
err := extractor.ExtractSourcesFromRepo(context.Background(), "", testPackageName, testLookasideURI)
126+
err := extractor.ExtractSourcesFromRepo(context.Background(), "", testPackageName, testLookasideURI, nil)
127127
require.Error(t, err)
128128
assert.Contains(t, err.Error(), "repository directory cannot be empty")
129129
})
130130

131131
t.Run("empty lookaside URI", func(t *testing.T) {
132-
err := extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, "")
132+
err := extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, "", nil)
133133
require.Error(t, err)
134134
assert.Contains(t, err.Error(), "lookaside base URI cannot be empty")
135135
})
@@ -138,7 +138,9 @@ func TestExtractSourcesFromRepoValidation(t *testing.T) {
138138
require.NoError(t, ctx.FS().MkdirAll(testEmptyRepoDir, 0o755))
139139

140140
// Missing sources file is valid - it means no external sources to download
141-
err := extractor.ExtractSourcesFromRepo(context.Background(), testEmptyRepoDir, testPackageName, testLookasideURI)
141+
err := extractor.ExtractSourcesFromRepo(
142+
context.Background(), testEmptyRepoDir, testPackageName, testLookasideURI, nil,
143+
)
142144
require.NoError(t, err)
143145
})
144146
}
@@ -158,7 +160,7 @@ func TestExtractSourcesFromRepoDownloadFailure(t *testing.T) {
158160
mockDownloader.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).
159161
Return(downloadErr)
160162

161-
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI)
163+
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI, nil)
162164
require.Error(t, err)
163165
require.ErrorIs(t, err, downloadErr)
164166
assert.Contains(t, err.Error(), "failed to download sources")
@@ -182,7 +184,7 @@ func TestExtractSourcesFromRepoHashMismatch(t *testing.T) {
182184
return afero.WriteFile(ctx.FS(), destPath, []byte("wrong content"), testFilePerms)
183185
})
184186

185-
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI)
187+
err = extractor.ExtractSourcesFromRepo(context.Background(), testRepoDir, testPackageName, testLookasideURI, nil)
186188
require.Error(t, err)
187189
assert.Contains(t, err.Error(), "hash mismatch")
188190
}
@@ -267,28 +269,75 @@ func TestParseSourcesFile(t *testing.T) {
267269
})
268270
}
269271

270-
func TestVerifyFedoraLookasideBaseURI(t *testing.T) {
271-
t.Run("valid URI with all placeholders", func(t *testing.T) {
272-
err := verifyFedoraLookasideBaseURI("https://example.com/$hashtype/$hash/$pkg/$filename")
273-
require.NoError(t, err)
274-
})
275-
276-
t.Run("missing placeholder returns error", func(t *testing.T) {
277-
tests := []struct {
278-
uri string
279-
missing string
280-
}{
281-
{"https://example.com/$hash/$pkg/$filename", "$hashtype"},
282-
{"https://example.com/$hashtype/$hash/$filename", "$pkg"},
283-
{"https://example.com/$hashtype/$hash/$pkg/", "$filename"},
284-
}
285-
286-
for _, tc := range tests {
287-
t.Run(tc.missing, func(t *testing.T) {
288-
err := verifyFedoraLookasideBaseURI(tc.uri)
289-
require.Error(t, err, "URI %q should fail for missing %s", tc.uri, tc.missing)
290-
assert.Contains(t, err.Error(), tc.missing)
291-
})
292-
}
293-
})
272+
func TestBuildLookasideURL(t *testing.T) {
273+
tests := []struct {
274+
name string
275+
template string
276+
pkg string
277+
filename string
278+
hashType string
279+
hash string
280+
expected string
281+
expectedError string
282+
}{
283+
{
284+
name: "standard template",
285+
template: "https://example.com/repo/pkgs/$pkg/$filename/$hashtype/$hash/$filename",
286+
pkg: "my-pkg",
287+
filename: "source.tar.gz",
288+
hashType: "SHA512",
289+
hash: "abc123",
290+
expected: "https://example.com/repo/pkgs/my-pkg/source.tar.gz/sha512/abc123/source.tar.gz",
291+
},
292+
{
293+
name: "different placeholder order",
294+
template: "https://example.com/$hashtype/$hash/$pkg/$filename",
295+
pkg: "test-pkg",
296+
filename: "file.tar.gz",
297+
hashType: "SHA256",
298+
hash: "def456",
299+
expected: "https://example.com/sha256/def456/test-pkg/file.tar.gz",
300+
},
301+
{
302+
name: "template without filename placeholder",
303+
template: "https://example.com/$pkg/$hashtype/$hash",
304+
pkg: "my-pkg",
305+
filename: "source.tar.gz",
306+
hashType: "SHA512",
307+
hash: "abc123",
308+
expected: "https://example.com/my-pkg/sha512/abc123",
309+
},
310+
{
311+
name: "packageName containing placeholder",
312+
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
313+
pkg: "evil-$filename-pkg",
314+
filename: "source.tar.gz",
315+
hashType: "SHA512",
316+
hash: "abc123",
317+
expectedError: "ambiguous substitution",
318+
},
319+
{
320+
name: "fileName containing placeholder",
321+
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
322+
pkg: "my-pkg",
323+
filename: "$hash-source.tar.gz",
324+
hashType: "SHA512",
325+
hash: "abc123",
326+
expectedError: "ambiguous substitution",
327+
},
328+
}
329+
330+
for _, testCase := range tests {
331+
t.Run(testCase.name, func(t *testing.T) {
332+
result, err := BuildLookasideURL(
333+
testCase.template, testCase.pkg, testCase.filename, testCase.hashType, testCase.hash,
334+
)
335+
if testCase.expectedError != "" {
336+
assert.ErrorContains(t, err, testCase.expectedError)
337+
} else {
338+
require.NoError(t, err)
339+
assert.Equal(t, testCase.expected, result)
340+
}
341+
})
342+
}
294343
}

internal/providers/sourceproviders/fedorasource/fedorasource_test/fedorasource_mocks.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)