-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathgitops_plugin_kanvas.go
231 lines (205 loc) · 7.68 KB
/
gitops_plugin_kanvas.go
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
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
gogit "github.com/go-git/go-git/v5"
"github.com/davinci-std/kanvas/client"
"github.com/davinci-std/kanvas/client/cli"
)
// GitOpsPluginKanvas is a gocat gitops plugin to prepare
// deployments using kanvas.
// This is used when you want to use gocat as a workflow engine
// with a chatops interface, while using kanvas as a deployment tool.
//
// Unlike GitOpsPluginKustomize which uses gocat's builtin Git and GitHub support,
// GitOpsPluginKanvas uses kanvas's Git and GitHub support.
type GitOpsPluginKanvas struct {
github *GitHub
git *GitOperator
}
func NewGitOpsPluginKanvas(github *GitHub, git *GitOperator) GitOpsPlugin {
return &GitOpsPluginKanvas{github: github, git: git}
}
func (k GitOpsPluginKanvas) Prepare(pj DeployProject, phase string, branch string, assigner User, tag string) (GitOpsPrepareOutput, error) {
var o GitOpsPrepareOutput
o.status = DeployStatusFail
if tag == "" {
ecr, err := CreateECRInstance()
if err != nil {
return o, err
}
tag, err = ecr.FindImageTagByRegexp(pj.ECRRegistryId(), pj.ECRRepository(), pj.ImageTagRegexp(), pj.TargetRegexp(), ImageTagVars{Branch: branch, Phase: phase})
if err != nil {
return o, err
}
}
ph := pj.FindPhase(phase)
if ph.Name == "" {
return o, fmt.Errorf("phase %s not found for project %s", phase, pj.ID)
}
// The head of the pull request is bot/docker-image-tag-<project_id>-<phase_name>-<tag>.
// And it's used by kanvas to create a pull request against the master or the main branch of the repository
// specified in the kanvas.yaml, not the repository that contains kanvas.yaml.
//
// Let's say you have two repositories:
// - myapp
// - infra
//
// myapp contains kanvas.yaml and infra contains the actual deployment configuration files.
//
// In this case, the head of the pull request is bot/docker-image-tag-<project_id>-<phase_name>-<tag>
// in the infra repository, not the myapp repository.
head := fmt.Sprintf("bot/docker-image-tag-%s-%s-%s", pj.ID, ph.Name, tag)
// Treat the kanvas.yaml as the way to generate the desired state of the deployment,
// not the desired state itself.
// That's why we don't create pull requests against the master or the main branch of the repository
// that contains kanvas.yaml!
//
// We use kanvas.yaml in the master or the main branch to do the deployment.
//
// Unlike gocat kustomize model, we don't create pull requests against the master or the main branch of
// the repository that contains kanvas.yaml.
//
// Instead, we let kanvas to create pull requests against the master or the main branch of the repository
// as defined in the kanvas.yaml.
git := *k.git
git.repository = nil
git.repo = "https://github.com/" + k.github.org + "/" + pj.gitHubRepository + ".git"
if err := git.Clone(); err != nil {
if !errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
return o, fmt.Errorf("failed to clone repository: %w", err)
}
}
defer func() {
if err := git.Clean(); err != nil {
fmt.Println(err)
}
}()
wt, err := git.checkoutMainBranch()
if err != nil {
return o, err
}
c := cli.New()
tmpdir := filepath.Join(git.getLocalRepoRoot(), ".kanvastmp")
if err := os.MkdirAll(tmpdir, 0755); err != nil {
return o, fmt.Errorf("failed to create .kanvastmp directory: %w", err)
}
applyOpts := client.ApplyOptions{
SkippedComponents: map[string]map[string]string{
// Any kanvas.yaml that can be used by gocat needs to have
// "image" component that uses the kanavs's docker provider for building the container image.
//
// We also assume that the components(like kustomize, argocd app, etc,) that depends on the "image" component uses
// either the "tag" or the "id" output of the "image" component for the deployment.
"image": {
"tag": tag,
"id": tag,
},
// This is an opinionated convention that we use in gocat.
// You can add any component named "prereq" in kanvas.yaml, and it is not used when triggered via gocat.
"prereq": {},
},
GitUserName: git.username,
PullRequestHead: head,
EnvVars: map[string]string{
// This is a hack to make kanvas to use a directory that we can clean up later.
// This is necessary to not leave any temporary files in random directories.
//
// You usually see json files containing information about the pull request created by
// kanvs apply command in this directory.
//
// We assume kanvas recursively creates a directory if it doesn't exist.
// That's why we don't create this .kanvastmp directory ourselves here.
"TMPDIR": tmpdir,
// kanvas requires the token to be set in the GITHUB_TOKEN envvar,
// where gocat expects the token to be set in the CONFIG_GITHUB_ACCESS_TOKEN envvar.
"GITHUB_TOKEN": os.Getenv("CONFIG_GITHUB_ACCESS_TOKEN"),
},
}
if assigner.GitHubNodeID != "" {
applyOpts.PullRequestAssigneeIDs = []string{assigner.GitHubNodeID}
}
// path is the relative path to the kanvas.yaml from the root of the repository.
//
// In case the project's Phases look like this:
//
// Phases: |
// - name: staging
// path: path/to/kanvas.yaml
// - name: production
// path: path/to
//
// The path to the config file is path/to/kanvas.yaml in both cases.
path := ph.Path
if path == "" {
path = "kanvas.yaml"
} else if filepath.Base(path) != "kanvas.yaml" {
path = filepath.Join(path, "kanvas.yaml")
}
realPath := wt.Filesystem.Join(git.getLocalRepoRoot(), path)
// We treat gocat "phase" as kanvas "environment".
//
// This means that kanvas.yaml need to have all the environments
// corresponding to the gocat phases.
//
// Let's say you have two gocat phases: staging and production.
// kanvas.yaml need to have both staging and production environments like this:
// environments:
// staging: ...
// production: ...
// sandbox: ...
// local: ...
// components:
// ...
//
// This Apply call corresponds to the following kanvas command:
//
// KANVAS_PULLREQUEST_ASSIGNEE_IDS=< assigner.GitHubNodeID > \
// KANVAS_PULLREQUEST_HEAD=< head > \
// kanvas apply --env <phase> --config <path> --skipped-jobs-outputs '{"image":{"id":"<tag>","tag":"<tag>"}}'
//
r, err := c.Apply(context.Background(), realPath, phase, applyOpts)
if err != nil {
return o, err
}
prs := r.GetPullRequests()
if len(prs) == 0 {
// Instead of determining if the desired image tag is already deployed or not by
// getting the current tag using:
//
// ph.Destination.GetCurrentRevision(GetCurrentRevisionInput{github: k.github})
//
// we determine it by checking if the pull request is created or not.
//
// That's possible because, if the image.tag is already deployed, kanvas won't create a pull request.
o.status = DeployStatusAlready
return o, nil
} else if len(prs) > 1 {
fmt.Println("gocat does not yet support multiple pull requests created by kanvas: ", prs)
return o, errors.New("gocat does not yet support multiple pull requests created by kanvas")
}
pr := prs[0]
prNum, err := strconv.Atoi(pr.Number)
if err != nil {
return o, fmt.Errorf("failed to convert pull request number to int: %w", err)
}
o = GitOpsPrepareOutput{
PullRequestID: pr.NodeID,
PullRequestNumber: prNum,
// PullRequestHTMLURL is the URL to the pull request in the config repository,
// not the repository that contains kanvas.yaml.
//
// This is necessary because unlike the kustomize model that creates pull requests
// against the specified repository, kanvas uses the kanvas.yaml in the repository
// specified in the gocat configmap, to create pull requests against the repository
// specified in the kanvas.yaml.
PullRequestHTMLURL: pr.HTMLURL,
Branch: head,
status: DeployStatusSuccess,
}
return o, nil
}