blob: 0d2f369f272fb0e20a40dabbefb0e0283348ef89 [file] [log] [blame]
Mohammed Naserdabb1dc2022-09-06 14:45:59 -04001package image_repositories
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
Mohammed Naser501dc412022-09-06 16:25:18 -04008 "time"
Mohammed Naserdabb1dc2022-09-06 14:45:59 -04009
10 "github.com/go-git/go-billy/v5"
11 "github.com/go-git/go-billy/v5/memfs"
12 "github.com/go-git/go-git/v5"
13 "github.com/go-git/go-git/v5/config"
14 "github.com/go-git/go-git/v5/plumbing"
Mohammed Naser98ec1262022-09-06 16:23:16 -040015 "github.com/go-git/go-git/v5/plumbing/object"
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040016 git_http "github.com/go-git/go-git/v5/plumbing/transport/http"
17 "github.com/go-git/go-git/v5/storage/memory"
18 "github.com/google/go-github/v47/github"
19 log "github.com/sirupsen/logrus"
20 "golang.org/x/oauth2"
21)
22
23type ImageRepository struct {
24 Project string
25
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040026 githubClient *github.Client
27 githubProjectName string
28 gitAuth *git_http.BasicAuth
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040029}
30
31func NewImageRepository(project string) *ImageRepository {
32 githubToken := os.Getenv("GITHUB_TOKEN")
33
34 ctx := context.TODO()
35 ts := oauth2.StaticTokenSource(
36 &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
37 )
38 tc := oauth2.NewClient(ctx, ts)
39
40 return &ImageRepository{
41 Project: project,
42
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040043 githubClient: github.NewClient(tc),
44 githubProjectName: fmt.Sprintf("docker-openstack-%s", project),
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040045 gitAuth: &git_http.BasicAuth{
Mohammed Naser76803692022-09-06 15:35:03 -040046 Username: "x-access-token",
47 Password: githubToken,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040048 },
49 }
50}
51
Mohammed Naseraaba50a2022-09-06 16:15:36 -040052func (i *ImageRepository) WriteFiles(ctx context.Context, fs billy.Filesystem) error {
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040053 // .github/workflows/build.yml
54 build := NewBuildWorkflow(i.Project)
55 err := build.WriteFile(fs)
56 if err != nil {
57 return err
58 }
59
60 // .github/workflows/sync.yml
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040061 sync := NewSyncWorkflow(i.Project)
62 err = sync.WriteFile(fs)
63 if err != nil {
64 return err
65 }
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040066
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040067 // .dockerignore
68 di := NewDockerIgnore()
69 err = di.WriteFile(fs)
70 if err != nil {
71 return err
72 }
73
74 // .pre-commit-config.yaml
75 pcc := NewPreCommitConfig()
76 err = pcc.WriteFile(fs)
77 if err != nil {
78 return err
79 }
80
81 // Dockerfile
Mohammed Naseraaba50a2022-09-06 16:15:36 -040082 df, err := NewDockerfile(ctx, i)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040083 if err != nil {
84 return err
85 }
86 err = df.WriteFile(fs)
87 if err != nil {
88 return err
89 }
90
91 // manifest.yml
Mohammed Naser1e1d4b32022-10-07 19:11:42 +000092 mf, err := NewImageManifest(i.Project, i.githubClient)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040093 if err != nil {
94 return err
95 }
96 err = mf.WriteFile(fs)
97 if err != nil {
98 return err
99 }
100
101 // README.md
102 rm, err := NewReadme(i.Project)
103 if err != nil {
104 return err
105 }
106 err = rm.WriteFile(fs)
107 if err != nil {
108 return err
109 }
110
111 return nil
112}
113
Mohammed Naser7e0cd9e2022-09-06 15:50:36 -0400114func (i *ImageRepository) CreateGithubRepository(ctx context.Context) error {
115 repo := &github.Repository{
116 Name: github.String(i.githubProjectName),
117 AutoInit: github.Bool(true),
118 }
119
120 _, _, err := i.githubClient.Repositories.Create(ctx, "vexxhost", repo)
121 if err != nil {
122 return err
123 }
124
125 return nil
126}
127
Mohammed Naser07493fc2022-09-06 17:33:20 -0400128func (i *ImageRepository) GetGitHubRepository(ctx context.Context, owner string) (*github.Repository, error) {
129 repo, _, err := i.githubClient.Repositories.Get(ctx, owner, i.githubProjectName)
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400130 if err != nil {
131 return nil, err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400132 }
133
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400134 return repo, nil
135}
136
Mohammed Naser07493fc2022-09-06 17:33:20 -0400137func (i *ImageRepository) ForkGitHubRepository(ctx context.Context) (*github.Repository, error) {
138 repo, err := i.GetGitHubRepository(ctx, "vexxhost-bot")
139 if err != nil {
140 i.githubClient.Repositories.CreateFork(ctx, "vexxhost", i.githubProjectName, nil)
141 time.Sleep(20 * time.Second)
142 return i.GetGitHubRepository(ctx, "vexxhost-bot")
143 }
144
145 return repo, nil
146}
147
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400148func (i *ImageRepository) UpdateGithubConfiguration(ctx context.Context) error {
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400149 // Description
150 description := fmt.Sprintf("Docker image for OpenStack: %s", i.Project)
151
152 // Updated repository
153 repo := &github.Repository{
154 AllowMergeCommit: github.Bool(false),
155 AllowRebaseMerge: github.Bool(true),
156 AllowSquashMerge: github.Bool(false),
157 DeleteBranchOnMerge: github.Bool(true),
158 Description: github.String(description),
159 Visibility: github.String("public"),
160 HasWiki: github.Bool(false),
161 HasIssues: github.Bool(false),
162 HasProjects: github.Bool(false),
163 }
164
165 // Update the repository with the correct settings
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400166 repo, _, err := i.githubClient.Repositories.Edit(ctx, "vexxhost", i.githubProjectName, repo)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400167 if err != nil {
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400168 return err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400169 }
170
171 // Branch protection
172 protection := &github.ProtectionRequest{
173 RequiredPullRequestReviews: &github.PullRequestReviewsEnforcementRequest{
174 RequiredApprovingReviewCount: 1,
175 DismissStaleReviews: true,
176 BypassPullRequestAllowancesRequest: &github.BypassPullRequestAllowancesRequest{
177 Users: []string{"mnaser"},
178 Teams: []string{},
179 Apps: []string{},
180 },
181 },
182 RequiredStatusChecks: &github.RequiredStatusChecks{
183 Strict: true,
184 Contexts: nil,
185 Checks: []*github.RequiredStatusCheck{
186 {
187 Context: "image (wallaby)",
188 },
189 {
190 Context: "image (xena)",
191 },
192 {
193 Context: "image (yoga)",
194 },
Mohammed Naserb5a6dc92022-09-28 21:55:38 -0400195 {
196 Context: "image (zed)",
197 },
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400198 },
199 },
200 RequiredConversationResolution: github.Bool(true),
201 RequireLinearHistory: github.Bool(true),
Mohammed Naser7e0cd9e2022-09-06 15:50:36 -0400202 EnforceAdmins: false,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400203 AllowForcePushes: github.Bool(false),
204 AllowDeletions: github.Bool(false),
205 }
206 _, _, err = i.githubClient.Repositories.UpdateBranchProtection(ctx, *repo.Owner.Login, *repo.Name, "main", protection)
207 if err != nil {
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400208 return err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400209 }
210
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400211 return nil
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400212}
213
Mohammed Naser07493fc2022-09-06 17:33:20 -0400214func (i *ImageRepository) Synchronize(ctx context.Context, admin bool) error {
215 var githubRepo *github.Repository
216 var err error
217
218 if admin {
219 githubRepo, err = i.GetGitHubRepository(ctx, "vexxhost")
220 } else {
221 githubRepo, err = i.ForkGitHubRepository(ctx)
222 }
223
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400224 if err != nil {
225 return err
226 }
227
228 storer := memory.NewStorage()
229 fs := memfs.New()
230
Mohammed Naser07493fc2022-09-06 17:33:20 -0400231 upstreamUrl := fmt.Sprintf("https://github.com/vexxhost/%s.git", i.githubProjectName)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400232 repo, err := git.Clone(storer, fs, &git.CloneOptions{
Mohammed Naser07493fc2022-09-06 17:33:20 -0400233 Auth: i.gitAuth,
234 URL: upstreamUrl,
235 RemoteName: "upstream",
236 })
237 if err != nil {
238 return err
239 }
240
241 _, err = repo.CreateRemote(&config.RemoteConfig{
242 Name: "origin",
243 URLs: []string{*githubRepo.CloneURL},
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400244 })
245 if err != nil {
246 return err
247 }
248
249 headRef, err := repo.Head()
250 if err != nil {
251 return err
252 }
253
254 ref := plumbing.NewHashReference("refs/heads/sync/atmosphere-ci", headRef.Hash())
255 err = repo.Storer.SetReference(ref)
256 if err != nil {
257 return err
258 }
259
260 worktree, err := repo.Worktree()
261 if err != nil {
262 return err
263 }
264
265 err = worktree.Checkout(&git.CheckoutOptions{
266 Branch: ref.Name(),
267 })
268 if err != nil {
269 return err
270 }
271
Mohammed Naseraaba50a2022-09-06 16:15:36 -0400272 err = i.WriteFiles(ctx, fs)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400273 if err != nil {
274 return err
275 }
276
277 status, err := worktree.Status()
278 if err != nil {
279 return err
280 }
281
282 if status.IsClean() {
283 log.Info("No changes to commit")
284 return nil
285 }
286
287 _, err = worktree.Add(".")
288 if err != nil {
289 return err
290 }
291
292 commit, err := worktree.Commit("chore: sync using `atmosphere-ci`", &git.CommitOptions{
293 All: true,
Mohammed Naserd31612c2022-09-06 16:21:24 -0400294 Author: &object.Signature{
Mohammed Naser07493fc2022-09-06 17:33:20 -0400295 Name: "vexxhost-bot",
296 Email: "mnaser+bot@vexxhost.com",
Mohammed Naser501dc412022-09-06 16:25:18 -0400297 When: time.Now(),
Mohammed Naserd31612c2022-09-06 16:21:24 -0400298 },
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400299 })
300 if err != nil {
301 return err
302 }
303
304 err = repo.Push(&git.PushOptions{
Mohammed Naser07493fc2022-09-06 17:33:20 -0400305 Auth: i.gitAuth,
306 RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
307 RemoteName: "origin",
308 Force: true,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400309 })
310 if err != nil {
311 return err
312 }
313
314 err = i.CreatePullRequest(ctx, githubRepo, commit)
315 if err != nil {
316 return err
317 }
318
319 return nil
320}
321
322func (i *ImageRepository) CreatePullRequest(ctx context.Context, repo *github.Repository, commit plumbing.Hash) error {
Mohammed Naser07493fc2022-09-06 17:33:20 -0400323 head := fmt.Sprintf("%s:%s", *repo.Owner.Login, "sync/atmosphere-ci")
324
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400325 newPR := &github.NewPullRequest{
326 Title: github.String("⚙️ Automatic sync from `atmosphere-ci`"),
Mohammed Naser07493fc2022-09-06 17:33:20 -0400327 Head: github.String(head),
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400328 Base: github.String("main"),
329 Body: github.String("This is an automatic pull request from `atmosphere-ci`"),
330 }
331
332 prs, _, err := i.githubClient.PullRequests.ListPullRequestsWithCommit(ctx, *repo.Owner.Login, *repo.Name, commit.String(), &github.PullRequestListOptions{})
333 if err != nil {
334 return err
335 }
336
337 if len(prs) > 0 {
338 log.Info("Pull request already exists: ", prs[0].GetHTMLURL())
339 return nil
340 }
341
Mohammed Naser07493fc2022-09-06 17:33:20 -0400342 pr, resp, err := i.githubClient.PullRequests.Create(ctx, "vexxhost", *repo.Name, newPR)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400343 if err != nil && resp.StatusCode != http.StatusUnprocessableEntity {
344 return err
345 }
346
347 log.Info("PR created: ", pr.GetHTMLURL())
348 return nil
349}