Skip to content

Commit d5d309d

Browse files
author
Antonio Salinas
authored
feat: Local components build /w upstream sources and arbitrary files (#439)
* Local components build /w upstream sources * Extra validation on urls and file ref def * Added origins back * Test fix * Refactored local source retrieval
1 parent 1ab3ba2 commit d5d309d

16 files changed

Lines changed: 728 additions & 149 deletions

internal/app/azldev/core/componentbuilder/componentbuilder_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func setupBuilder(t *testing.T) *componentBuilderTestParams {
5959
},
6060
)
6161

62-
sourceManager.EXPECT().FetchFile(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
62+
sourceManager.EXPECT().FetchFiles(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
6363

6464
preparer, err := sources.NewPreparer(sourceManager, testEnv.Env.FS(), testEnv.Env, testEnv.Env)
6565

internal/app/azldev/core/sources/sourceprep.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,16 @@ func NewPreparer(
8686
func (p *sourcePreparerImpl) PrepareSources(
8787
ctx context.Context, component components.Component, outputDir string, applyOverlays bool,
8888
) error {
89-
// Use the source manager to fetch components
90-
err := p.sourceManager.FetchComponent(ctx, component, outputDir)
89+
// Download any explicitly configured source files first.
90+
// Files defined here take precedence over any sources implicitly defined by the fedora upstream.
91+
err := p.sourceManager.FetchFiles(ctx, component, outputDir)
92+
if err != nil {
93+
return fmt.Errorf("failed to fetch source files for component %#q:\n%w",
94+
component.GetName(), err)
95+
}
96+
97+
// Fetch the component sources.
98+
err = p.sourceManager.FetchComponent(ctx, component, outputDir)
9199
if err != nil {
92100
return fmt.Errorf("failed to fetch sources for component %#q:\n%w",
93101
component.GetName(), err)

internal/app/azldev/core/sources/sourceprep_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func TestPrepareSources_Success(t *testing.T) {
5555

5656
component.EXPECT().GetName().AnyTimes().Return("test-component")
5757
component.EXPECT().GetConfig().AnyTimes().Return(&projectconfig.ComponentConfig{})
58+
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(nil)
5859
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
5960
func(_ interface{}, _ interface{}, outputDir string) error {
6061
// Create the expected spec file.
@@ -87,10 +88,10 @@ func TestPrepareSources_SourceManagerError(t *testing.T) {
8788
sourceManager := sourceproviders_test.NewMockSourceManager(ctrl)
8889
ctx := testctx.NewCtx()
8990

90-
expectedErr := errors.New("failed to fetch component")
91+
expectedErr := errors.New("failed to fetch files")
9192

9293
component.EXPECT().GetName().AnyTimes().Return("test-component")
93-
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).Return(expectedErr)
94+
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(expectedErr)
9495

9596
preparer, err := sources.NewPreparer(sourceManager, ctx.FS(), ctx, ctx)
9697
require.NoError(t, err)
@@ -112,6 +113,7 @@ func TestPrepareSources_WritesMacrosFile(t *testing.T) {
112113
With: []string{"feature"},
113114
},
114115
})
116+
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(nil)
115117
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
116118
func(_ interface{}, _ interface{}, outputDir string) error {
117119
// Create the expected spec file.
@@ -358,6 +360,7 @@ func TestPrepareSources_CheckSkip(t *testing.T) {
358360
},
359361
},
360362
})
363+
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(nil)
361364
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
362365
func(_ interface{}, _ interface{}, outputDir string) error {
363366
// Create the expected spec file with a %check section.
@@ -414,6 +417,7 @@ func TestPrepareSources_CheckSkipDisabled(t *testing.T) {
414417
},
415418
},
416419
})
420+
sourceManager.EXPECT().FetchFiles(gomock.Any(), component, testOutputDir).Return(nil)
417421
sourceManager.EXPECT().FetchComponent(gomock.Any(), component, testOutputDir).DoAndReturn(
418422
func(_ interface{}, _ interface{}, outputDir string) error {
419423
// Create the expected spec file with a %check section.

internal/projectconfig/component.go

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,20 @@ type ComponentReference struct {
2929
Version *rpm.Version
3030
}
3131

32-
// Origin Types.
32+
// OriginType indicates the type of origin for a source file.
3333
type OriginType string
3434

3535
const (
36-
OriginTypeURL OriginType = "url"
37-
OriginTypeGitHub OriginType = "github"
38-
OriginTypeLocal OriginType = "local"
39-
OriginTypeStorageAccount OriginType = "storageAccount"
40-
OriginTypeGenerated OriginType = "generated"
36+
// OriginTypeURI indicates that the source file is fetched from a URI.
37+
OriginTypeURI OriginType = "download"
4138
)
4239

43-
// Origin represents a single source location with its type and configuration.
40+
// Origin describes where a source file comes from and how to retrieve it.
4441
type Origin struct {
45-
// Type of source: "url", "github", "local", "blobstore"
46-
Type OriginType `toml:"Type" json:"type"`
47-
48-
// Location - interpretation depends on Type:
49-
// - "url": the full URL
50-
// - "local": relative or absolute path
51-
Location string `toml:"Location,omitempty" json:"location,omitempty"`
52-
53-
// Optional: Script for generated files
54-
Script string `toml:"Script,omitempty" json:"script,omitempty"`
55-
56-
// Optional: Additional arguments specific to the origin type
57-
// Examples:
58-
// - GitHub: {"release": "v1.2.3", "asset": "foo.tar.gz"}
59-
// - Generated: {"arg1": "value1", "arg2": "value2"} (passed as --arg1=value1)
60-
Args []string `toml:"Args,omitempty" json:"args,omitempty"`
42+
// Type indicates how the source file should be acquired.
43+
Type OriginType `toml:"type" json:"type" jsonschema:"required,enum=download,title=Origin type,description=Type of origin for this source file"`
44+
// Uri to download the source file from if origin type is 'download'. Ignored for other origin types.
45+
Uri string `toml:"uri,omitempty" json:"uri,omitempty" jsonschema:"title=URI,description=URI to download the source file from if origin type is 'download',example=https://example.com/source.tar.gz"`
6146
}
6247

6348
// SourceFileReference encapsulates a reference to a specific source file artifact.
@@ -66,16 +51,16 @@ type SourceFileReference struct {
6651
Component ComponentReference `toml:"-" json:"-"`
6752

6853
// Name of the source file; must be non-empty.
69-
Filename string `toml:"-" json:"-"`
54+
Filename string `toml:"filename" json:"filename"`
7055

7156
// Hash of the source file, expressed as a hex string.
72-
Hash string `toml:"Hash,omitempty" json:"hash,omitempty"`
57+
Hash string `toml:"hash,omitempty" json:"hash,omitempty"`
7358

74-
// Type of hash used by `Hash` (if present).
75-
HashType fileutils.HashType `toml:"HashType,omitempty" json:"hashType,omitempty"`
59+
// Type of hash used by Hash (e.g., "sha256", "sha512").
60+
HashType fileutils.HashType `toml:"hash-type,omitempty" json:"hashType,omitempty"`
7661

77-
// Ordered list of source origins to try in priority order
78-
Origins []Origin `toml:"Origins,omitempty" json:"origins,omitempty"`
62+
// Type of origin for this source file (e.g., URI, custom).
63+
Origin Origin `toml:"origin" json:"origin"`
7964
}
8065

8166
// Defines a component group. Component groups are logical groupings of components (see [ComponentConfig]).
@@ -128,6 +113,9 @@ type ComponentConfig struct {
128113

129114
// Configuration for building the component.
130115
Build ComponentBuildConfig `toml:"build,omitempty" json:"build,omitempty" table:"-" jsonschema:"title=Build configuration,description=Configuration for building the component"`
116+
117+
// Source file references for this component.
118+
SourceFiles []SourceFileReference `toml:"source-files,omitempty" json:"sourceFiles,omitempty" table:"-" jsonschema:"title=Source files,description=Source files to download for this component"`
131119
}
132120

133121
// Mutates the component config, updating it with overrides present in other.
@@ -151,6 +139,7 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
151139
SourceConfigFile: c.SourceConfigFile,
152140
Spec: deep.MustCopy(c.Spec),
153141
Build: deep.MustCopy(c.Build),
142+
SourceFiles: deep.MustCopy(c.SourceFiles),
154143
}
155144

156145
// Fix up paths.

internal/providers/sourceproviders/fedorasource/fedorasource.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,24 @@ func (g *FedoraSourceDownloaderImpl) downloadAndVerifySources(
162162
repoDir string,
163163
) error {
164164
for _, sourceFile := range sourceFiles {
165+
destFilePath := filepath.Join(repoDir, sourceFile.fileName)
166+
167+
exists, err := fileutils.Exists(g.fileSystem, destFilePath)
168+
if err != nil {
169+
return fmt.Errorf("failed to check if file exists at %#q:\n%w", destFilePath, err)
170+
}
171+
172+
if exists {
173+
slog.Debug("File already exists, skipping download", "fileName", sourceFile.fileName, "path", destFilePath)
174+
175+
continue
176+
}
177+
165178
slog.Info("Downloading source file...",
166179
"fileName", sourceFile.fileName,
167-
"URI", sourceFile.uri)
168-
169-
destFilePath := filepath.Join(repoDir, sourceFile.fileName)
180+
"URI", sourceFile.uri,
181+
"destPath", destFilePath,
182+
)
170183

171184
if err := g.downloader.Download(ctx, sourceFile.uri, destFilePath); err != nil {
172185
return fmt.Errorf("failed to download from %#q to %#q:\n%w", sourceFile.uri, destFilePath, err)

internal/providers/sourceproviders/fedorasourceprovider.go

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/gim-home/azldev-preview/internal/global/opctx"
1717
"github.com/gim-home/azldev-preview/internal/projectconfig"
1818
"github.com/gim-home/azldev-preview/internal/providers/sourceproviders/fedorasource"
19+
"github.com/gim-home/azldev-preview/internal/utils/fileutils"
1920
"github.com/gim-home/azldev-preview/internal/utils/git"
2021
)
2122

@@ -27,6 +28,7 @@ type DistroResolver func(distroRef projectconfig.DistroReference) (
2728
// FedoraSourcesProviderImpl implements ComponentSourceProvider for Git repositories.
2829
type FedoraSourcesProviderImpl struct {
2930
fs opctx.FS
31+
dryRunnable opctx.DryRunnable
3032
gitProvider git.GitProvider
3133
downloader fedorasource.FedoraSourceDownloader
3234
distroResolver DistroResolver
@@ -36,6 +38,7 @@ var _ ComponentSourceProvider = (*FedoraSourcesProviderImpl)(nil)
3638

3739
func NewFedoraSourcesProviderImpl(
3840
fs opctx.FS,
41+
dryRunnable opctx.DryRunnable,
3942
gitProvider git.GitProvider,
4043
downloader fedorasource.FedoraSourceDownloader,
4144
distroResolver DistroResolver,
@@ -44,6 +47,10 @@ func NewFedoraSourcesProviderImpl(
4447
return nil, errors.New("filesystem cannot be nil")
4548
}
4649

50+
if dryRunnable == nil {
51+
return nil, errors.New("dryRunnable cannot be nil")
52+
}
53+
4754
if gitProvider == nil {
4855
return nil, errors.New("git provider cannot be nil")
4956
}
@@ -58,6 +65,7 @@ func NewFedoraSourcesProviderImpl(
5865

5966
return &FedoraSourcesProviderImpl{
6067
fs: fs,
68+
dryRunnable: dryRunnable,
6169
gitProvider: gitProvider,
6270
downloader: downloader,
6371
distroResolver: distroResolver,
@@ -66,7 +74,7 @@ func NewFedoraSourcesProviderImpl(
6674

6775
func (g *FedoraSourcesProviderImpl) GetComponent(
6876
ctx context.Context, component components.Component, destDirPath string,
69-
) error {
77+
) (err error) {
7078
componentName := component.GetName()
7179
if componentName == "" {
7280
return errors.New("component name cannot be empty")
@@ -97,45 +105,80 @@ func (g *FedoraSourcesProviderImpl) GetComponent(
97105
"upstreamComponent", upstreamNameToUse,
98106
"branch", distroGitBranch)
99107

100-
// Clone the repository directly to the destination
108+
// Clone to a temp directory first, then copy files to destination.
109+
tempDir, err := fileutils.MkdirTempInTempDir(g.fs, "azldev-clone-")
110+
if err != nil {
111+
return fmt.Errorf("failed to create temp directory for clone:\n%w", err)
112+
}
113+
114+
defer fileutils.RemoveAllAndUpdateErrorIfNil(g.fs, tempDir, &err)
115+
116+
// Clone the repository to temp directory
101117
err = g.gitProvider.Clone(
102118
ctx,
103119
gitRepoURL,
104-
destDirPath,
120+
tempDir,
105121
git.WithGitBranch(distroGitBranch))
106122
if err != nil {
107123
return fmt.Errorf("failed to clone git repository %#q:\n%w", gitRepoURL, err)
108124
}
109125

110126
// Checkout the appropriate commit based on component/distro config
111-
err = g.checkoutTargetCommit(ctx, effectiveDistroRef, destDirPath)
127+
err = g.checkoutTargetCommit(ctx, effectiveDistroRef, tempDir)
112128
if err != nil {
113129
return fmt.Errorf("failed to checkout target commit:\n%w", err)
114130
}
115131

116-
// Delete the .git directory so it's not packaged up.
117-
err = g.fs.RemoveAll(filepath.Join(destDirPath, ".git"))
132+
// Delete the .git directory so it's not copied to destination.
133+
err = g.fs.RemoveAll(filepath.Join(tempDir, ".git"))
118134
if err != nil {
119135
return fmt.Errorf("failed to remove .git directory from cloned repository at %#q:\n%w",
120-
destDirPath, err)
136+
tempDir, err)
121137
}
122138

123-
// Extract sources from repo (downloads lookaside files into the repo)
124-
err = g.downloader.ExtractSourcesFromRepo(ctx, destDirPath, upstreamNameToUse, lookasideBaseURI)
139+
// Extract sources from repo (downloads lookaside files into the temp dir)
140+
err = g.downloader.ExtractSourcesFromRepo(ctx, tempDir, upstreamNameToUse, lookasideBaseURI)
125141
if err != nil {
126142
return fmt.Errorf("failed to extract sources from git repository:\n%w", err)
127143
}
128144

129-
// If the upstream name differs from the component name, we will need to rename the spec.
130-
if upstreamNameToUse != componentName {
131-
downloadedSpecPath := filepath.Join(destDirPath, upstreamNameToUse+".spec")
132-
desiredSpecPath := filepath.Join(destDirPath, componentName+".spec")
145+
// If the upstream name differs from the component name, rename the spec in temp dir.
146+
err = g.renameSpecIfNeeded(tempDir, upstreamNameToUse, componentName)
147+
if err != nil {
148+
return err
149+
}
133150

134-
err := g.fs.Rename(downloadedSpecPath, desiredSpecPath)
135-
if err != nil {
136-
return fmt.Errorf("failed to rename fetched spec file from %#q to %#q:\n%w",
137-
downloadedSpecPath, desiredSpecPath, err)
138-
}
151+
// Copy files from temp dir to destination, skipping files that already exist.
152+
// This preserves any files downloaded by FetchFiles, giving them precedence.
153+
copyOptions := fileutils.CopyDirOptions{
154+
CopyFileOptions: fileutils.CopyFileOptions{
155+
PreserveFileMode: true,
156+
},
157+
FileFilter: fileutils.SkipExistingFiles,
158+
}
159+
160+
err = fileutils.CopyDirRecursive(g.dryRunnable, g.fs, tempDir, destDirPath, copyOptions)
161+
if err != nil {
162+
return fmt.Errorf("failed to copy files to destination:\n%w", err)
163+
}
164+
165+
return nil
166+
}
167+
168+
// renameSpecIfNeeded renames the spec file in the given directory if the upstream name
169+
// differs from the desired component name.
170+
func (g *FedoraSourcesProviderImpl) renameSpecIfNeeded(dir, upstreamName, componentName string) error {
171+
if upstreamName == componentName {
172+
return nil
173+
}
174+
175+
downloadedSpecPath := filepath.Join(dir, upstreamName+".spec")
176+
desiredSpecPath := filepath.Join(dir, componentName+".spec")
177+
178+
err := g.fs.Rename(downloadedSpecPath, desiredSpecPath)
179+
if err != nil {
180+
return fmt.Errorf("failed to rename fetched spec file from %#q to %#q:\n%w",
181+
downloadedSpecPath, desiredSpecPath, err)
139182
}
140183

141184
return nil

0 commit comments

Comments
 (0)