blob: cb2d81dee4cce7f1419f9cbc7b709c45e70cd7bd [file] [log] [blame]
Mohammed Naserdabb1dc2022-09-06 14:45:59 -04001package image_repositories
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "os"
8
9 "github.com/go-git/go-billy/v5"
10 "github.com/go-git/go-billy/v5/memfs"
11 "github.com/go-git/go-git/v5"
12 "github.com/go-git/go-git/v5/config"
13 "github.com/go-git/go-git/v5/plumbing"
14 git_http "github.com/go-git/go-git/v5/plumbing/transport/http"
15 "github.com/go-git/go-git/v5/storage/memory"
16 "github.com/google/go-github/v47/github"
17 log "github.com/sirupsen/logrus"
18 "golang.org/x/oauth2"
19)
20
21type ImageRepository struct {
22 Project string
23
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040024 githubClient *github.Client
25 githubProjectName string
26 gitAuth *git_http.BasicAuth
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040027}
28
29func NewImageRepository(project string) *ImageRepository {
30 githubToken := os.Getenv("GITHUB_TOKEN")
31
32 ctx := context.TODO()
33 ts := oauth2.StaticTokenSource(
34 &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
35 )
36 tc := oauth2.NewClient(ctx, ts)
37
38 return &ImageRepository{
39 Project: project,
40
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040041 githubClient: github.NewClient(tc),
42 githubProjectName: fmt.Sprintf("docker-openstack-%s", project),
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040043 gitAuth: &git_http.BasicAuth{
Mohammed Naser76803692022-09-06 15:35:03 -040044 Username: "x-access-token",
45 Password: githubToken,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040046 },
47 }
48}
49
50func (i *ImageRepository) WriteFiles(fs billy.Filesystem) error {
51 // .github/workflows/build.yml
52 build := NewBuildWorkflow(i.Project)
53 err := build.WriteFile(fs)
54 if err != nil {
55 return err
56 }
57
58 // .github/workflows/sync.yml
Mohammed Naser6d6b2c92022-09-06 15:24:23 -040059 sync := NewSyncWorkflow(i.Project)
60 err = sync.WriteFile(fs)
61 if err != nil {
62 return err
63 }
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040064
Mohammed Naserdabb1dc2022-09-06 14:45:59 -040065 // .dockerignore
66 di := NewDockerIgnore()
67 err = di.WriteFile(fs)
68 if err != nil {
69 return err
70 }
71
72 // .pre-commit-config.yaml
73 pcc := NewPreCommitConfig()
74 err = pcc.WriteFile(fs)
75 if err != nil {
76 return err
77 }
78
79 // Dockerfile
80 df, err := NewDockerfile()
81 if err != nil {
82 return err
83 }
84 err = df.WriteFile(fs)
85 if err != nil {
86 return err
87 }
88
89 // manifest.yml
90 mf, err := NewImageManifest(i.Project)
91 if err != nil {
92 return err
93 }
94 err = mf.WriteFile(fs)
95 if err != nil {
96 return err
97 }
98
99 // README.md
100 rm, err := NewReadme(i.Project)
101 if err != nil {
102 return err
103 }
104 err = rm.WriteFile(fs)
105 if err != nil {
106 return err
107 }
108
109 return nil
110}
111
Mohammed Naser7e0cd9e2022-09-06 15:50:36 -0400112func (i *ImageRepository) CreateGithubRepository(ctx context.Context) error {
113 repo := &github.Repository{
114 Name: github.String(i.githubProjectName),
115 AutoInit: github.Bool(true),
116 }
117
118 _, _, err := i.githubClient.Repositories.Create(ctx, "vexxhost", repo)
119 if err != nil {
120 return err
121 }
122
123 return nil
124}
125
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400126func (i *ImageRepository) GetGitHubRepository(ctx context.Context) (*github.Repository, error) {
127 repo, _, err := i.githubClient.Repositories.Get(ctx, "vexxhost", i.githubProjectName)
128 if err != nil {
129 return nil, err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400130 }
131
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400132 return repo, nil
133}
134
135func (i *ImageRepository) UpdateGithubConfiguration(ctx context.Context) error {
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400136 // Description
137 description := fmt.Sprintf("Docker image for OpenStack: %s", i.Project)
138
139 // Updated repository
140 repo := &github.Repository{
141 AllowMergeCommit: github.Bool(false),
142 AllowRebaseMerge: github.Bool(true),
143 AllowSquashMerge: github.Bool(false),
144 DeleteBranchOnMerge: github.Bool(true),
145 Description: github.String(description),
146 Visibility: github.String("public"),
147 HasWiki: github.Bool(false),
148 HasIssues: github.Bool(false),
149 HasProjects: github.Bool(false),
150 }
151
152 // Update the repository with the correct settings
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400153 repo, _, err := i.githubClient.Repositories.Edit(ctx, "vexxhost", i.githubProjectName, repo)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400154 if err != nil {
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400155 return err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400156 }
157
158 // Branch protection
159 protection := &github.ProtectionRequest{
160 RequiredPullRequestReviews: &github.PullRequestReviewsEnforcementRequest{
161 RequiredApprovingReviewCount: 1,
162 DismissStaleReviews: true,
163 BypassPullRequestAllowancesRequest: &github.BypassPullRequestAllowancesRequest{
164 Users: []string{"mnaser"},
165 Teams: []string{},
166 Apps: []string{},
167 },
168 },
169 RequiredStatusChecks: &github.RequiredStatusChecks{
170 Strict: true,
171 Contexts: nil,
172 Checks: []*github.RequiredStatusCheck{
173 {
174 Context: "image (wallaby)",
175 },
176 {
177 Context: "image (xena)",
178 },
179 {
180 Context: "image (yoga)",
181 },
182 },
183 },
184 RequiredConversationResolution: github.Bool(true),
185 RequireLinearHistory: github.Bool(true),
Mohammed Naser7e0cd9e2022-09-06 15:50:36 -0400186 EnforceAdmins: false,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400187 AllowForcePushes: github.Bool(false),
188 AllowDeletions: github.Bool(false),
189 }
190 _, _, err = i.githubClient.Repositories.UpdateBranchProtection(ctx, *repo.Owner.Login, *repo.Name, "main", protection)
191 if err != nil {
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400192 return err
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400193 }
194
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400195 return nil
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400196}
197
198func (i *ImageRepository) Synchronize(ctx context.Context) error {
Mohammed Naser6d6b2c92022-09-06 15:24:23 -0400199 githubRepo, err := i.GetGitHubRepository(ctx)
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400200 if err != nil {
201 return err
202 }
203
204 storer := memory.NewStorage()
205 fs := memfs.New()
206
207 repo, err := git.Clone(storer, fs, &git.CloneOptions{
208 Auth: i.gitAuth,
Mohammed Naser2a6c4242022-09-06 15:31:31 -0400209 URL: *githubRepo.CloneURL,
Mohammed Naserdabb1dc2022-09-06 14:45:59 -0400210 })
211 if err != nil {
212 return err
213 }
214
215 headRef, err := repo.Head()
216 if err != nil {
217 return err
218 }
219
220 ref := plumbing.NewHashReference("refs/heads/sync/atmosphere-ci", headRef.Hash())
221 err = repo.Storer.SetReference(ref)
222 if err != nil {
223 return err
224 }
225
226 worktree, err := repo.Worktree()
227 if err != nil {
228 return err
229 }
230
231 err = worktree.Checkout(&git.CheckoutOptions{
232 Branch: ref.Name(),
233 })
234 if err != nil {
235 return err
236 }
237
238 err = i.WriteFiles(fs)
239 if err != nil {
240 return err
241 }
242
243 status, err := worktree.Status()
244 if err != nil {
245 return err
246 }
247
248 if status.IsClean() {
249 log.Info("No changes to commit")
250 return nil
251 }
252
253 _, err = worktree.Add(".")
254 if err != nil {
255 return err
256 }
257
258 commit, err := worktree.Commit("chore: sync using `atmosphere-ci`", &git.CommitOptions{
259 All: true,
260 })
261 if err != nil {
262 return err
263 }
264
265 err = repo.Push(&git.PushOptions{
266 Auth: i.gitAuth,
267 RefSpecs: []config.RefSpec{"refs/heads/sync/atmosphere-ci:refs/heads/sync/atmosphere-ci"},
268 Force: true,
269 })
270 if err != nil {
271 return err
272 }
273
274 err = i.CreatePullRequest(ctx, githubRepo, commit)
275 if err != nil {
276 return err
277 }
278
279 return nil
280}
281
282func (i *ImageRepository) CreatePullRequest(ctx context.Context, repo *github.Repository, commit plumbing.Hash) error {
283 newPR := &github.NewPullRequest{
284 Title: github.String("⚙️ Automatic sync from `atmosphere-ci`"),
285 Head: github.String("sync/atmosphere-ci"),
286 Base: github.String("main"),
287 Body: github.String("This is an automatic pull request from `atmosphere-ci`"),
288 }
289
290 prs, _, err := i.githubClient.PullRequests.ListPullRequestsWithCommit(ctx, *repo.Owner.Login, *repo.Name, commit.String(), &github.PullRequestListOptions{})
291 if err != nil {
292 return err
293 }
294
295 if len(prs) > 0 {
296 log.Info("Pull request already exists: ", prs[0].GetHTMLURL())
297 return nil
298 }
299
300 pr, resp, err := i.githubClient.PullRequests.Create(ctx, *repo.Owner.Login, *repo.Name, newPR)
301 if err != nil && resp.StatusCode != http.StatusUnprocessableEntity {
302 return err
303 }
304
305 log.Info("PR created: ", pr.GetHTMLURL())
306 return nil
307}