Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 1 | package image_repositories |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "net/http" |
| 7 | "os" |
Mohammed Naser | 501dc41 | 2022-09-06 16:25:18 -0400 | [diff] [blame] | 8 | "time" |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 9 | |
| 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 Naser | 98ec126 | 2022-09-06 16:23:16 -0400 | [diff] [blame] | 15 | "github.com/go-git/go-git/v5/plumbing/object" |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 16 | 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 | |
| 23 | type ImageRepository struct { |
| 24 | Project string |
| 25 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 26 | githubClient *github.Client |
| 27 | githubProjectName string |
| 28 | gitAuth *git_http.BasicAuth |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 29 | } |
| 30 | |
| 31 | func 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 43 | githubClient: github.NewClient(tc), |
| 44 | githubProjectName: fmt.Sprintf("docker-openstack-%s", project), |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 45 | gitAuth: &git_http.BasicAuth{ |
Mohammed Naser | 7680369 | 2022-09-06 15:35:03 -0400 | [diff] [blame] | 46 | Username: "x-access-token", |
| 47 | Password: githubToken, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 48 | }, |
| 49 | } |
| 50 | } |
| 51 | |
Mohammed Naser | aaba50a | 2022-09-06 16:15:36 -0400 | [diff] [blame] | 52 | func (i *ImageRepository) WriteFiles(ctx context.Context, fs billy.Filesystem) error { |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 53 | // .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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 61 | sync := NewSyncWorkflow(i.Project) |
| 62 | err = sync.WriteFile(fs) |
| 63 | if err != nil { |
| 64 | return err |
| 65 | } |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 66 | |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 67 | // .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 Naser | aaba50a | 2022-09-06 16:15:36 -0400 | [diff] [blame] | 82 | df, err := NewDockerfile(ctx, i) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 83 | 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 Naser | 1e1d4b3 | 2022-10-07 19:11:42 +0000 | [diff] [blame] | 92 | mf, err := NewImageManifest(i.Project, i.githubClient) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 93 | 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 Naser | 7e0cd9e | 2022-09-06 15:50:36 -0400 | [diff] [blame] | 114 | func (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 Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 128 | func (i *ImageRepository) GetGitHubRepository(ctx context.Context, owner string) (*github.Repository, error) { |
| 129 | repo, _, err := i.githubClient.Repositories.Get(ctx, owner, i.githubProjectName) |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 130 | if err != nil { |
| 131 | return nil, err |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 132 | } |
| 133 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 134 | return repo, nil |
| 135 | } |
| 136 | |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 137 | func (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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 148 | func (i *ImageRepository) UpdateGithubConfiguration(ctx context.Context) error { |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 149 | // 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 166 | repo, _, err := i.githubClient.Repositories.Edit(ctx, "vexxhost", i.githubProjectName, repo) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 167 | if err != nil { |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 168 | return err |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 169 | } |
| 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 Naser | b5a6dc9 | 2022-09-28 21:55:38 -0400 | [diff] [blame] | 195 | { |
| 196 | Context: "image (zed)", |
| 197 | }, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 198 | }, |
| 199 | }, |
| 200 | RequiredConversationResolution: github.Bool(true), |
| 201 | RequireLinearHistory: github.Bool(true), |
Mohammed Naser | 7e0cd9e | 2022-09-06 15:50:36 -0400 | [diff] [blame] | 202 | EnforceAdmins: false, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 203 | 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 208 | return err |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 209 | } |
| 210 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 211 | return nil |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 212 | } |
| 213 | |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 214 | func (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 Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 224 | if err != nil { |
| 225 | return err |
| 226 | } |
| 227 | |
| 228 | storer := memory.NewStorage() |
| 229 | fs := memfs.New() |
| 230 | |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 231 | upstreamUrl := fmt.Sprintf("https://github.com/vexxhost/%s.git", i.githubProjectName) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 232 | repo, err := git.Clone(storer, fs, &git.CloneOptions{ |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 233 | 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 Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 244 | }) |
| 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 Naser | aaba50a | 2022-09-06 16:15:36 -0400 | [diff] [blame] | 272 | err = i.WriteFiles(ctx, fs) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 273 | 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 Naser | d31612c | 2022-09-06 16:21:24 -0400 | [diff] [blame] | 294 | Author: &object.Signature{ |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 295 | Name: "vexxhost-bot", |
| 296 | Email: "mnaser+bot@vexxhost.com", |
Mohammed Naser | 501dc41 | 2022-09-06 16:25:18 -0400 | [diff] [blame] | 297 | When: time.Now(), |
Mohammed Naser | d31612c | 2022-09-06 16:21:24 -0400 | [diff] [blame] | 298 | }, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 299 | }) |
| 300 | if err != nil { |
| 301 | return err |
| 302 | } |
| 303 | |
| 304 | err = repo.Push(&git.PushOptions{ |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 305 | Auth: i.gitAuth, |
| 306 | RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"}, |
| 307 | RemoteName: "origin", |
| 308 | Force: true, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 309 | }) |
| 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 | |
| 322 | func (i *ImageRepository) CreatePullRequest(ctx context.Context, repo *github.Repository, commit plumbing.Hash) error { |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 323 | head := fmt.Sprintf("%s:%s", *repo.Owner.Login, "sync/atmosphere-ci") |
| 324 | |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 325 | newPR := &github.NewPullRequest{ |
| 326 | Title: github.String("⚙️ Automatic sync from `atmosphere-ci`"), |
Mohammed Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 327 | Head: github.String(head), |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 328 | 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 Naser | 07493fc | 2022-09-06 17:33:20 -0400 | [diff] [blame] | 342 | pr, resp, err := i.githubClient.PullRequests.Create(ctx, "vexxhost", *repo.Name, newPR) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 343 | if err != nil && resp.StatusCode != http.StatusUnprocessableEntity { |
| 344 | return err |
| 345 | } |
| 346 | |
| 347 | log.Info("PR created: ", pr.GetHTMLURL()) |
| 348 | return nil |
| 349 | } |