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" |
| 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 | |
| 21 | type ImageRepository struct { |
| 22 | Project string |
| 23 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 24 | githubClient *github.Client |
| 25 | githubProjectName string |
| 26 | gitAuth *git_http.BasicAuth |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 27 | } |
| 28 | |
| 29 | func 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 41 | githubClient: github.NewClient(tc), |
| 42 | githubProjectName: fmt.Sprintf("docker-openstack-%s", project), |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 43 | gitAuth: &git_http.BasicAuth{ |
Mohammed Naser | 7680369 | 2022-09-06 15:35:03 -0400 | [diff] [blame] | 44 | Username: "x-access-token", |
| 45 | Password: githubToken, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 46 | }, |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func (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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 59 | sync := NewSyncWorkflow(i.Project) |
| 60 | err = sync.WriteFile(fs) |
| 61 | if err != nil { |
| 62 | return err |
| 63 | } |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 64 | |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 65 | // .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 Naser | 7e0cd9e | 2022-09-06 15:50:36 -0400 | [diff] [blame^] | 112 | func (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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 126 | func (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 Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 130 | } |
| 131 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 132 | return repo, nil |
| 133 | } |
| 134 | |
| 135 | func (i *ImageRepository) UpdateGithubConfiguration(ctx context.Context) error { |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 136 | // 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 153 | repo, _, err := i.githubClient.Repositories.Edit(ctx, "vexxhost", i.githubProjectName, repo) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 154 | if err != nil { |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 155 | return err |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 156 | } |
| 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 Naser | 7e0cd9e | 2022-09-06 15:50:36 -0400 | [diff] [blame^] | 186 | EnforceAdmins: false, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 187 | 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 Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 192 | return err |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 193 | } |
| 194 | |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 195 | return nil |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 196 | } |
| 197 | |
| 198 | func (i *ImageRepository) Synchronize(ctx context.Context) error { |
Mohammed Naser | 6d6b2c9 | 2022-09-06 15:24:23 -0400 | [diff] [blame] | 199 | githubRepo, err := i.GetGitHubRepository(ctx) |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 200 | 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 Naser | 2a6c424 | 2022-09-06 15:31:31 -0400 | [diff] [blame] | 209 | URL: *githubRepo.CloneURL, |
Mohammed Naser | dabb1dc | 2022-09-06 14:45:59 -0400 | [diff] [blame] | 210 | }) |
| 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 | |
| 282 | func (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 | } |