Skip to content

Commit 29b58e3

Browse files
authored
refactor: splits deployer into two phases (#150)
1 parent 7d2a064 commit 29b58e3

File tree

6 files changed

+369
-234
lines changed

6 files changed

+369
-234
lines changed

cli/cmd/cmds/module/deploy.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,32 @@ func (c *DeployCmd) Run(ctx run.RunContext) error {
3434
ctx.Logger,
3535
ctx.CueCtx,
3636
)
37-
if err := d.Deploy(project.Name, deployment.NewModuleBundle(&project), dryrun); err != nil {
38-
if err == deployer.ErrNoChanges {
37+
38+
dr, err := d.CreateDeployment(project.Name, deployment.NewModuleBundle(&project))
39+
if err != nil {
40+
return fmt.Errorf("failed creating deployment: %w", err)
41+
}
42+
43+
if !dryrun {
44+
changes, err := dr.HasChanges()
45+
if err != nil {
46+
return fmt.Errorf("failed checking for changes: %w", err)
47+
}
48+
49+
if !changes {
3950
ctx.Logger.Warn("no changes to deploy")
4051
return nil
4152
}
4253

43-
return fmt.Errorf("failed deploying project: %w", err)
54+
if err := dr.Commit(); err != nil {
55+
return fmt.Errorf("failed committing deployment: %w", err)
56+
}
57+
} else {
58+
ctx.Logger.Info("Dry-run: not committing or pushing changes")
59+
ctx.Logger.Info("Dumping manifests")
60+
for _, r := range dr.Manifests {
61+
fmt.Println(string(r))
62+
}
4463
}
4564

4665
return nil

lib/project/deployment/deployer/deployer.go

Lines changed: 115 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,46 @@ var (
4141
ErrNoChanges = fmt.Errorf("no changes to commit")
4242
)
4343

44+
// Deployment is a prepared deployment to a GitOps repository.
45+
type Deployment struct {
46+
// Bundle is the deployment bundle being deployed.
47+
Bundle deployment.ModuleBundle
48+
49+
// Manifests is the generated manifests for the deployment.
50+
// The key is the name of the manifest and the value is the manifest content.
51+
Manifests map[string][]byte
52+
53+
// RawBundle is the raw representation of the deployment bundle (in CUE).
54+
RawBundle []byte
55+
56+
// Repo is an in-memory clone of the GitOps repository being deployed to.
57+
Repo repo.GitRepo
58+
59+
// Project is the name of the project being deployed.
60+
Project string
61+
62+
logger *slog.Logger
63+
}
64+
65+
// DeployerConfig is the configuration for a Deployer.
4466
type DeployerConfig struct {
45-
Git DeployerConfigGit
67+
// Git is the configuration for the GitOps repository.
68+
Git DeployerConfigGit
69+
70+
// RootDir is the root directory in the GitOps repository to deploy to.
4671
RootDir string
4772
}
4873

74+
// DeployerConfigGit is the configuration for the GitOps repository.
4975
type DeployerConfigGit struct {
76+
// Creds is the credentials to use for the GitOps repository.
5077
Creds common.Secret
51-
Ref string
52-
Url string
78+
79+
// Ref is the Git reference to deploy to.
80+
Ref string
81+
82+
// Url is the URL of the GitOps repository.
83+
Url string
5384
}
5485

5586
// Deployer performs GitOps deployments for projects.
@@ -63,94 +94,116 @@ type Deployer struct {
6394
ss secrets.SecretStore
6495
}
6596

66-
// DeployProject deploys the manifests for a project to the GitOps repository.
67-
func (d *Deployer) Deploy(projectName string, bundle deployment.ModuleBundle, dryrun bool) error {
68-
if bundle.Bundle.Env == "prod" {
69-
return fmt.Errorf("cannot deploy to production environment")
97+
// PrepareOptions are options for preparing a deployment.
98+
type PrepareOptions struct {
99+
fs afero.Fs
100+
}
101+
102+
// PrepareOption is an option for preparing a deployment.
103+
type PrepareOption func(*PrepareOptions)
104+
105+
// WithFS sets the filesystem to use for preparing a deployment.
106+
func WithFS(fs afero.Fs) PrepareOption {
107+
return func(o *PrepareOptions) {
108+
o.fs = fs
70109
}
110+
}
71111

72-
r, err := d.clone()
73-
if err != nil {
74-
return err
112+
// CreateDeployment creates a deployment for the given project and bundle.
113+
func (d *Deployer) CreateDeployment(
114+
project string,
115+
bundle deployment.ModuleBundle,
116+
opts ...PrepareOption,
117+
) (*Deployment, error) {
118+
options := PrepareOptions{
119+
fs: afero.NewMemMapFs(),
120+
}
121+
for _, o := range opts {
122+
o(&options)
75123
}
76124

77-
prjPath := d.buildProjectPath(bundle, projectName)
125+
r, err := d.clone(d.cfg.Git.Url, d.cfg.Git.Ref, options.fs)
126+
if err != nil {
127+
return nil, err
128+
}
78129

130+
prjPath := buildProjectPath(d.cfg.RootDir, project, bundle)
79131
d.logger.Info("Checking if project path exists", "path", prjPath)
80132
if err := d.checkProjectPath(prjPath, &r); err != nil {
81-
return fmt.Errorf("failed checking project path: %w", err)
82-
}
83-
84-
d.logger.Info("Clearing project path", "path", prjPath)
85-
if err := d.clearProjectPath(prjPath, &r); err != nil {
86-
return fmt.Errorf("could not clear project path: %w", err)
133+
return nil, fmt.Errorf("failed checking project path: %w", err)
87134
}
88135

89136
env, err := d.LoadEnv(prjPath, d.ctx, &r)
90137
if err != nil {
91-
return fmt.Errorf("could not load environment: %w", err)
138+
return nil, fmt.Errorf("could not load environment: %w", err)
92139
}
93140

94141
d.logger.Info("Generating manifests")
95142
result, err := d.gen.GenerateBundle(bundle, env)
96143
if err != nil {
97-
return fmt.Errorf("could not generate deployment manifests: %w", err)
144+
return nil, fmt.Errorf("could not generate deployment manifests: %w", err)
145+
}
146+
147+
d.logger.Info("Clearing project path", "path", prjPath)
148+
if err := d.clearProjectPath(prjPath, &r); err != nil {
149+
return nil, fmt.Errorf("could not clear project path: %w", err)
98150
}
99151

100-
modPath := filepath.Join(prjPath, "mod.cue")
101-
d.logger.Info("Writing module", "path", modPath)
102-
if err := r.WriteFile(modPath, []byte(result.Module)); err != nil {
103-
return fmt.Errorf("could not write module: %w", err)
152+
bundlePath := filepath.Join(prjPath, "bundle.cue")
153+
d.logger.Info("Writing bundle", "path", bundlePath)
154+
if err := r.WriteFile(bundlePath, []byte(result.Module)); err != nil {
155+
return nil, fmt.Errorf("could not write bundle: %w", err)
104156
}
105157

106-
if err := r.StageFile(modPath); err != nil {
107-
return fmt.Errorf("could not add module to working tree: %w", err)
158+
if err := r.StageFile(bundlePath); err != nil {
159+
return nil, fmt.Errorf("could not add bundle to working tree: %w", err)
108160
}
109161

110162
for name, result := range result.Manifests {
111163
manPath := filepath.Join(prjPath, fmt.Sprintf("%s.yaml", name))
112164

113165
d.logger.Info("Writing manifest", "path", manPath)
114166
if err := r.WriteFile(manPath, []byte(result)); err != nil {
115-
return fmt.Errorf("could not write manifest: %w", err)
167+
return nil, fmt.Errorf("could not write manifest: %w", err)
116168
}
117169
if err := r.StageFile(manPath); err != nil {
118-
return fmt.Errorf("could not add manifest to working tree: %w", err)
170+
return nil, fmt.Errorf("could not add manifest to working tree: %w", err)
119171
}
120172
}
121173

122-
if !dryrun {
123-
changes, err := r.HasChanges()
124-
if err != nil {
125-
return fmt.Errorf("could not check if worktree has changes: %w", err)
126-
} else if !changes {
127-
return ErrNoChanges
128-
}
174+
return &Deployment{
175+
Bundle: bundle,
176+
Manifests: result.Manifests,
177+
RawBundle: result.Module,
178+
Repo: r,
179+
logger: d.logger,
180+
}, nil
181+
}
129182

130-
d.logger.Info("Committing changes")
131-
_, err = r.Commit(fmt.Sprintf(GIT_MESSAGE, projectName))
132-
if err != nil {
133-
return fmt.Errorf("could not commit changes: %w", err)
134-
}
183+
// Commit commits the deployment to the GitOps repository.
184+
func (d *Deployment) Commit() error {
185+
d.logger.Info("Committing changes")
186+
_, err := d.Repo.Commit(fmt.Sprintf(GIT_MESSAGE, d.Project))
187+
if err != nil {
188+
return fmt.Errorf("could not commit changes: %w", err)
189+
}
135190

136-
d.logger.Info("Pushing changes")
137-
if err := r.Push(); err != nil {
138-
return fmt.Errorf("could not push changes: %w", err)
139-
}
140-
} else {
141-
d.logger.Info("Dry-run: not committing or pushing changes")
142-
d.logger.Info("Dumping manifests")
143-
for _, r := range result.Manifests {
144-
fmt.Println(string(r))
145-
}
191+
d.logger.Info("Pushing changes")
192+
if err := d.Repo.Push(); err != nil {
193+
return fmt.Errorf("could not push changes: %w", err)
146194
}
147195

148196
return nil
149197
}
150198

151-
// buildProjectPath builds the path to the project in the GitOps repository.
152-
func (d *Deployer) buildProjectPath(b deployment.ModuleBundle, projectName string) string {
153-
return fmt.Sprintf(PATH, d.cfg.RootDir, b.Bundle.Env, projectName)
199+
// HasChanges checks if the deployment results in changes to the GitOps repository.
200+
func (d *Deployment) HasChanges() (bool, error) {
201+
changes, err := d.Repo.HasChanges()
202+
if err != nil {
203+
return false, fmt.Errorf("could not check if worktree has changes: %w", err)
204+
}
205+
206+
return changes, nil
154207
}
155208

156209
// checkProjectPath checks if the project path exists and creates it if it does not.
@@ -195,12 +248,12 @@ func (d *Deployer) clearProjectPath(path string, r *repo.GitRepo) error {
195248
return nil
196249
}
197250

198-
// clone clones the GitOps repository.
199-
func (d *Deployer) clone() (repo.GitRepo, error) {
251+
// clone clones the given repository and returns the GitRepo.
252+
func (d *Deployer) clone(url, ref string, fs afero.Fs) (repo.GitRepo, error) {
200253
opts := []repo.GitRepoOption{
201254
repo.WithAuthor(GIT_NAME, GIT_EMAIL),
202255
repo.WithGitRemoteInteractor(d.remote),
203-
repo.WithFS(d.fs),
256+
repo.WithFS(fs),
204257
}
205258

206259
creds, err := providers.GetGitProviderCreds(&d.cfg.Git.Creds, &d.ss, d.logger)
@@ -210,9 +263,9 @@ func (d *Deployer) clone() (repo.GitRepo, error) {
210263
opts = append(opts, repo.WithAuth("forge", creds.Token))
211264
}
212265

213-
d.logger.Info("Cloning repository", "url", d.cfg.Git.Url, "ref", d.cfg.Git.Ref)
266+
d.logger.Info("Cloning repository", "url", url, "ref", ref)
214267
r := repo.NewGitRepo(d.logger, opts...)
215-
if err := r.Clone("/repo", d.cfg.Git.Url, d.cfg.Git.Ref); err != nil {
268+
if err := r.Clone("/repo", url, ref); err != nil {
216269
return repo.GitRepo{}, fmt.Errorf("could not clone repository: %w", err)
217270
}
218271

@@ -278,3 +331,8 @@ func NewDeployerConfigFromProject(p *project.Project) DeployerConfig {
278331
RootDir: p.Blueprint.Global.Deployment.Root,
279332
}
280333
}
334+
335+
// buildProjectPath builds the path to the project in the GitOps repository.
336+
func buildProjectPath(root string, project string, b deployment.ModuleBundle) string {
337+
return fmt.Sprintf(PATH, root, b.Bundle.Env, project)
338+
}

0 commit comments

Comments
 (0)