Skip to content

Commit 40377f3

Browse files
authored
feat: adds support for fetching bundles from git (#152)
1 parent dfbbe6e commit 40377f3

File tree

4 files changed

+196
-36
lines changed

4 files changed

+196
-36
lines changed

lib/project/deployment/deployer/deployer.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"path/filepath"
77

88
"cuelang.org/go/cue"
9+
"cuelang.org/go/cue/cuecontext"
910
"github.com/input-output-hk/catalyst-forge/lib/project/deployment"
1011
"github.com/input-output-hk/catalyst-forge/lib/project/deployment/generator"
1112
"github.com/input-output-hk/catalyst-forge/lib/project/project"
@@ -95,17 +96,17 @@ type Deployer struct {
9596
ss secrets.SecretStore
9697
}
9798

98-
// PrepareOptions are options for preparing a deployment.
99-
type PrepareOptions struct {
99+
// CloneOptions are options for cloning a repository.
100+
type CloneOptions struct {
100101
fs fs.Filesystem
101102
}
102103

103-
// PrepareOption is an option for preparing a deployment.
104-
type PrepareOption func(*PrepareOptions)
104+
// CloneOption is an option for cloning a repository.
105+
type CloneOption func(*CloneOptions)
105106

106-
// WithFS sets the filesystem to use for preparing a deployment.
107-
func WithFS(fs fs.Filesystem) PrepareOption {
108-
return func(o *PrepareOptions) {
107+
// WithFS sets the filesystem to use for cloning a repository.
108+
func WithFS(fs fs.Filesystem) CloneOption {
109+
return func(o *CloneOptions) {
109110
o.fs = fs
110111
}
111112
}
@@ -114,9 +115,9 @@ func WithFS(fs fs.Filesystem) PrepareOption {
114115
func (d *Deployer) CreateDeployment(
115116
project string,
116117
bundle deployment.ModuleBundle,
117-
opts ...PrepareOption,
118+
opts ...CloneOption,
118119
) (*Deployment, error) {
119-
options := PrepareOptions{
120+
options := CloneOptions{
120121
fs: billy.NewInMemoryFs(),
121122
}
122123
for _, o := range opts {
@@ -181,6 +182,40 @@ func (d *Deployer) CreateDeployment(
181182
}, nil
182183
}
183184

185+
// FetchBundle fetches a deployment bundle from the given project and repository.
186+
func (d *Deployer) FetchBundle(url, ref, projectPath string, opts ...CloneOption) (deployment.ModuleBundle, error) {
187+
options := CloneOptions{
188+
fs: billy.NewInMemoryFs(),
189+
}
190+
for _, o := range opts {
191+
o(&options)
192+
}
193+
194+
r, err := d.clone(url, ref, options.fs)
195+
if err != nil {
196+
return deployment.ModuleBundle{}, err
197+
}
198+
199+
exists, err := r.Exists(projectPath)
200+
if err != nil {
201+
return deployment.ModuleBundle{}, fmt.Errorf("could not check if project path exists: %w", err)
202+
} else if !exists {
203+
return deployment.ModuleBundle{}, fmt.Errorf("project path does not exist: %s", projectPath)
204+
}
205+
206+
loader := project.NewDefaultProjectLoader(cuecontext.New(), d.ss, d.logger, project.WithFs(r.WorkFs()))
207+
p, err := loader.Load("/" + projectPath)
208+
if err != nil {
209+
return deployment.ModuleBundle{}, fmt.Errorf("could not load project: %w", err)
210+
}
211+
212+
if p.Blueprint.Project == nil || p.Blueprint.Project.Deployment == nil {
213+
return deployment.ModuleBundle{}, fmt.Errorf("project does not have a deployment bundle")
214+
}
215+
216+
return deployment.NewModuleBundle(&p), nil
217+
}
218+
184219
// Commit commits the deployment to the GitOps repository.
185220
func (d *Deployment) Commit() error {
186221
d.logger.Info("Committing changes")

lib/project/deployment/deployer/deployer_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,76 @@ import (
2020
"github.com/stretchr/testify/require"
2121
)
2222

23+
func TestDeployerFetchBundle(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
cfg DeployerConfig
27+
files map[string]string
28+
validate func(*testing.T, deployment.ModuleBundle, error)
29+
}{
30+
{
31+
name: "success",
32+
cfg: makeConfig(),
33+
files: map[string]string{
34+
"project/blueprint.cue": makeBlueprint(),
35+
},
36+
validate: func(t *testing.T, bundle deployment.ModuleBundle, err error) {
37+
require.NoError(t, err)
38+
39+
b := bundle.Bundle
40+
assert.Equal(t, "test", b.Env)
41+
assert.Len(t, b.Modules, 1)
42+
assert.Equal(t, "module", b.Modules["main"].Name)
43+
assert.Equal(t, "v1.0.0", b.Modules["main"].Version)
44+
},
45+
},
46+
{
47+
name: "no bundle",
48+
cfg: makeConfig(),
49+
files: map[string]string{
50+
"project/blueprint.cue": `version: "1.0"`,
51+
},
52+
validate: func(t *testing.T, bundle deployment.ModuleBundle, err error) {
53+
require.Error(t, err)
54+
require.Equal(t, "project does not have a deployment bundle", err.Error())
55+
},
56+
},
57+
{
58+
name: "no project",
59+
cfg: makeConfig(),
60+
files: map[string]string{
61+
"project1/blueprint.cue": `version: "1.0"`,
62+
},
63+
validate: func(t *testing.T, bundle deployment.ModuleBundle, err error) {
64+
require.Error(t, err)
65+
require.Equal(t, "project path does not exist: project", err.Error())
66+
},
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
fs := billy.NewInMemoryFs()
73+
74+
remote, _, err := tu.NewMockGitRemoteInterface(tt.files)
75+
require.NoError(t, err)
76+
77+
ss := tu.NewMockSecretStore(map[string]string{"token": "password"})
78+
d := Deployer{
79+
cfg: tt.cfg,
80+
ctx: cuecontext.New(),
81+
fs: fs,
82+
logger: testutils.NewNoopLogger(),
83+
remote: remote,
84+
ss: ss,
85+
}
86+
87+
bundle, err := d.FetchBundle("github.com/org/repo", "main", "project")
88+
tt.validate(t, bundle, err)
89+
})
90+
}
91+
}
92+
2393
func TestDeployerCreateDeployment(t *testing.T) {
2494
type testResult struct {
2595
cloneOpts *gg.CloneOptions
@@ -241,3 +311,42 @@ func makeConfig() DeployerConfig {
241311
RootDir: "root",
242312
}
243313
}
314+
315+
func makeBlueprint() string {
316+
return `
317+
{
318+
version: "1.0"
319+
project: {
320+
name: "project"
321+
deployment: {
322+
on: {}
323+
bundle: {
324+
env: "test"
325+
modules: {
326+
main: {
327+
name: "module"
328+
version: "v1.0.0"
329+
values: {
330+
foo: "bar"
331+
}
332+
}
333+
}
334+
}
335+
}
336+
}
337+
global: {
338+
deployment: {
339+
registries: {
340+
containers: "registry.com"
341+
modules: "registry.com"
342+
}
343+
repo: {
344+
ref: "main"
345+
url: "github.com/org/repo"
346+
}
347+
root: "root"
348+
}
349+
}
350+
}
351+
`
352+
}

lib/project/project/loader.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ type ProjectLoader interface {
2929
Load(projectPath string) (Project, error)
3030
}
3131

32+
type ProjectLoaderOption func(*DefaultProjectLoader)
33+
34+
// WithFs sets the filesystem for the project loader.
35+
func WithFs(fs fs.Filesystem) ProjectLoaderOption {
36+
return func(p *DefaultProjectLoader) {
37+
p.fs = fs
38+
}
39+
}
40+
41+
// WithInjectors sets the blueprint injectors for the project loader.
42+
func WithInjectors(injectors []injector.BlueprintInjector) ProjectLoaderOption {
43+
return func(p *DefaultProjectLoader) {
44+
p.injectors = injectors
45+
}
46+
}
47+
48+
// WithRuntimes sets the runtime data for the project loader.
49+
func WithRuntimes(runtimes []RuntimeData) ProjectLoaderOption {
50+
return func(p *DefaultProjectLoader) {
51+
p.runtimes = runtimes
52+
}
53+
}
54+
3255
// DefaultProjectLoader is the default implementation of the ProjectLoader.
3356
type DefaultProjectLoader struct {
3457
blueprintLoader blueprint.BlueprintLoader
@@ -204,6 +227,7 @@ func NewDefaultProjectLoader(
204227
ctx *cue.Context,
205228
store secrets.SecretStore,
206229
logger *slog.Logger,
230+
opts ...ProjectLoaderOption,
207231
) DefaultProjectLoader {
208232
if logger == nil {
209233
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
@@ -214,11 +238,9 @@ func NewDefaultProjectLoader(
214238
}
215239

216240
fs := billy.NewBaseOsFS()
217-
bl := blueprint.NewDefaultBlueprintLoader(ctx, logger)
218-
return DefaultProjectLoader{
219-
blueprintLoader: &bl,
220-
ctx: ctx,
221-
fs: fs,
241+
l := DefaultProjectLoader{
242+
ctx: ctx,
243+
fs: fs,
222244
injectors: []injector.BlueprintInjector{
223245
injector.NewBlueprintEnvInjector(ctx, logger),
224246
injector.NewBlueprintGlobalInjector(ctx, logger),
@@ -230,31 +252,15 @@ func NewDefaultProjectLoader(
230252
},
231253
store: store,
232254
}
233-
}
234255

235-
// NewCustomProjectLoader creates a new DefaultProjectLoader with custom dependencies.
236-
func NewCustomProjectLoader(
237-
ctx *cue.Context,
238-
fs fs.Filesystem,
239-
bl blueprint.BlueprintLoader,
240-
injectors []injector.BlueprintInjector,
241-
runtimes []RuntimeData,
242-
store secrets.SecretStore,
243-
logger *slog.Logger,
244-
) DefaultProjectLoader {
245-
if logger == nil {
246-
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
256+
for _, o := range opts {
257+
o(&l)
247258
}
248259

249-
return DefaultProjectLoader{
250-
blueprintLoader: bl,
251-
ctx: ctx,
252-
fs: fs,
253-
injectors: injectors,
254-
logger: logger,
255-
runtimes: runtimes,
256-
store: store,
257-
}
260+
bl := blueprint.NewCustomBlueprintLoader(ctx, l.fs, logger)
261+
l.blueprintLoader = &bl
262+
263+
return l
258264
}
259265

260266
// validateAndDecode validates and decodes a raw blueprint.

lib/tools/git/repo/repo.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ func (g *GitRepo) GetCurrentTag() (string, error) {
157157
return tag, nil
158158
}
159159

160+
// GitFs returns the filesystem for the .git directory.
161+
func (g *GitRepo) GitFs() fs.Filesystem {
162+
return g.gfs
163+
}
164+
160165
// HasChanges returns true if the repository has changes.
161166
func (g *GitRepo) HasChanges() (bool, error) {
162167
status, err := g.worktree.Status()
@@ -300,6 +305,11 @@ func (g *GitRepo) UnstageFile(path string) error {
300305
return nil
301306
}
302307

308+
// WorkFs returns the filesystem for the working directory.
309+
func (g *GitRepo) WorkFs() fs.Filesystem {
310+
return g.wfs
311+
}
312+
303313
// WriteFile writes the given contents to the given path in the repository.
304314
// It also automatically adds the file to the staging area.
305315
func (g *GitRepo) WriteFile(path string, contents []byte) error {

0 commit comments

Comments
 (0)