|
| 1 | +package controllers |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "github.com/diggerhq/digger/backend/models" |
| 6 | + "github.com/diggerhq/digger/backend/utils" |
| 7 | + "github.com/diggerhq/digger/libs/ci/github" |
| 8 | + "github.com/gin-gonic/gin" |
| 9 | + "github.com/google/uuid" |
| 10 | + "log/slog" |
| 11 | + "net/http" |
| 12 | + "strconv" |
| 13 | + "strings" |
| 14 | +) |
| 15 | + |
| 16 | +func (d DiggerController) GithubAppCallbackPage(c *gin.Context) { |
| 17 | + installationIdParams, installationIdExists := c.Request.URL.Query()["installation_id"] |
| 18 | + if !installationIdExists || len(installationIdParams) == 0 { |
| 19 | + slog.Error("There was no installation_id in the url query parameters") |
| 20 | + c.String(http.StatusBadRequest, "could not find the installation_id query parameter for github app") |
| 21 | + return |
| 22 | + } |
| 23 | + installationId := installationIdParams[0] |
| 24 | + if len(installationId) < 1 { |
| 25 | + slog.Error("Installation_id parameter is empty") |
| 26 | + c.String(http.StatusBadRequest, "installation_id parameter for github app is empty") |
| 27 | + return |
| 28 | + } |
| 29 | + //setupAction := c.Request.URL.Query()["setup_action"][0] |
| 30 | + codeParams, codeExists := c.Request.URL.Query()["code"] |
| 31 | + if !codeExists || len(codeParams) == 0 { |
| 32 | + slog.Error("There was no code in the url query parameters") |
| 33 | + c.String(http.StatusBadRequest, "could not find the code query parameter for github app") |
| 34 | + return |
| 35 | + } |
| 36 | + code := codeParams[0] |
| 37 | + if len(code) < 1 { |
| 38 | + slog.Error("Code parameter is empty") |
| 39 | + c.String(http.StatusBadRequest, "code parameter for github app is empty") |
| 40 | + return |
| 41 | + } |
| 42 | + appId := c.Request.URL.Query().Get("state") |
| 43 | + |
| 44 | + slog.Info("Processing GitHub app callback", "installationId", installationId, "appId", appId) |
| 45 | + |
| 46 | + clientId, clientSecret, _, _, err := d.GithubClientProvider.FetchCredentials(appId) |
| 47 | + if err != nil { |
| 48 | + slog.Error("Could not fetch credentials for GitHub app", "appId", appId, "error", err) |
| 49 | + c.String(http.StatusInternalServerError, "could not find credentials for github app") |
| 50 | + return |
| 51 | + } |
| 52 | + |
| 53 | + installationId64, err := strconv.ParseInt(installationId, 10, 64) |
| 54 | + if err != nil { |
| 55 | + slog.Error("Failed to parse installation ID", |
| 56 | + "installationId", installationId, |
| 57 | + "error", err, |
| 58 | + ) |
| 59 | + c.String(http.StatusInternalServerError, "Failed to parse installation_id.") |
| 60 | + return |
| 61 | + } |
| 62 | + |
| 63 | + slog.Debug("Validating GitHub callback", "installationId", installationId64, "clientId", clientId) |
| 64 | + |
| 65 | + result, installation, err := validateGithubCallback(d.GithubClientProvider, clientId, clientSecret, code, installationId64) |
| 66 | + if !result { |
| 67 | + slog.Error("Failed to validate installation ID", |
| 68 | + "installationId", installationId64, |
| 69 | + "error", err, |
| 70 | + ) |
| 71 | + c.String(http.StatusInternalServerError, "Failed to validate installation_id.") |
| 72 | + return |
| 73 | + } |
| 74 | + |
| 75 | + // TODO: Lookup org in GithubAppInstallation by installationID if found use that installationID otherwise |
| 76 | + // create a new org for this installationID |
| 77 | + // retrieve org for current orgID |
| 78 | + installationIdInt64, err := strconv.ParseInt(installationId, 10, 64) |
| 79 | + if err != nil { |
| 80 | + slog.Error("Failed to parse installation ID as int64", |
| 81 | + "installationId", installationId, |
| 82 | + "error", err, |
| 83 | + ) |
| 84 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "installationId could not be parsed"}) |
| 85 | + return |
| 86 | + } |
| 87 | + |
| 88 | + slog.Debug("Looking up GitHub app installation link", "installationId", installationIdInt64) |
| 89 | + |
| 90 | + var link *models.GithubAppInstallationLink |
| 91 | + link, err = models.DB.GetGithubAppInstallationLink(installationIdInt64) |
| 92 | + if err != nil { |
| 93 | + slog.Error("Error getting GitHub app installation link", |
| 94 | + "installationId", installationIdInt64, |
| 95 | + "error", err, |
| 96 | + ) |
| 97 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "error getting github app link"}) |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + if link == nil { |
| 102 | + slog.Info("No existing link found, creating new organization and link", |
| 103 | + "installationId", installationId, |
| 104 | + ) |
| 105 | + |
| 106 | + name := fmt.Sprintf("dggr-def-%v", uuid.NewString()[:8]) |
| 107 | + externalId := uuid.NewString() |
| 108 | + |
| 109 | + slog.Debug("Creating new organization", |
| 110 | + "name", name, |
| 111 | + "externalId", externalId, |
| 112 | + ) |
| 113 | + |
| 114 | + org, err := models.DB.CreateOrganisation(name, "digger", externalId) |
| 115 | + if err != nil { |
| 116 | + slog.Error("Error creating organization", |
| 117 | + "name", name, |
| 118 | + "error", err, |
| 119 | + ) |
| 120 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error with CreateOrganisation"}) |
| 121 | + return |
| 122 | + } |
| 123 | + |
| 124 | + slog.Debug("Creating GitHub installation link", |
| 125 | + "orgId", org.ID, |
| 126 | + "installationId", installationId64, |
| 127 | + ) |
| 128 | + |
| 129 | + link, err = models.DB.CreateGithubInstallationLink(org, installationId64) |
| 130 | + if err != nil { |
| 131 | + slog.Error("Error creating GitHub installation link", |
| 132 | + "orgId", org.ID, |
| 133 | + "installationId", installationId64, |
| 134 | + "error", err, |
| 135 | + ) |
| 136 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error with CreateGithubInstallationLink"}) |
| 137 | + return |
| 138 | + } |
| 139 | + |
| 140 | + slog.Info("Created new organization and installation link", |
| 141 | + "orgId", org.ID, |
| 142 | + "installationId", installationId64, |
| 143 | + ) |
| 144 | + } else { |
| 145 | + slog.Info("Found existing installation link", |
| 146 | + "orgId", link.OrganisationId, |
| 147 | + "installationId", installationId64, |
| 148 | + ) |
| 149 | + } |
| 150 | + |
| 151 | + org := link.Organisation |
| 152 | + orgId := link.OrganisationId |
| 153 | + |
| 154 | + // create a github installation link (org ID matched to installation ID) |
| 155 | + _, err = models.DB.CreateGithubInstallationLink(org, installationId64) |
| 156 | + if err != nil { |
| 157 | + slog.Error("Error creating GitHub installation link", |
| 158 | + "orgId", orgId, |
| 159 | + "installationId", installationId64, |
| 160 | + "error", err, |
| 161 | + ) |
| 162 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error updating GitHub installation"}) |
| 163 | + return |
| 164 | + } |
| 165 | + |
| 166 | + slog.Debug("Getting GitHub client", |
| 167 | + "appId", *installation.AppID, |
| 168 | + "installationId", installationId64, |
| 169 | + ) |
| 170 | + |
| 171 | + client, _, err := d.GithubClientProvider.Get(*installation.AppID, installationId64) |
| 172 | + if err != nil { |
| 173 | + slog.Error("Error retrieving GitHub client", |
| 174 | + "appId", *installation.AppID, |
| 175 | + "installationId", installationId64, |
| 176 | + "error", err, |
| 177 | + ) |
| 178 | + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error fetching organisation"}) |
| 179 | + return |
| 180 | + } |
| 181 | + |
| 182 | + // we get repos accessible to this installation |
| 183 | + slog.Debug("Listing repositories for installation", "installationId", installationId64) |
| 184 | + |
| 185 | + repos, err := github.ListGithubRepos(client) |
| 186 | + if err != nil { |
| 187 | + slog.Error("Failed to list existing repositories", |
| 188 | + "installationId", installationId64, |
| 189 | + "error", err, |
| 190 | + ) |
| 191 | + c.String(http.StatusInternalServerError, "Failed to list existing repos: %v", err) |
| 192 | + return |
| 193 | + } |
| 194 | + |
| 195 | + // resets all existing installations (soft delete) |
| 196 | + slog.Debug("Resetting existing GitHub installations", |
| 197 | + "installationId", installationId, |
| 198 | + ) |
| 199 | + |
| 200 | + var AppInstallation models.GithubAppInstallation |
| 201 | + err = models.DB.GormDB.Model(&AppInstallation).Where("github_installation_id=?", installationId).Update("status", models.GithubAppInstallDeleted).Error |
| 202 | + if err != nil { |
| 203 | + slog.Error("Failed to update GitHub installations", |
| 204 | + "installationId", installationId, |
| 205 | + "error", err, |
| 206 | + ) |
| 207 | + c.String(http.StatusInternalServerError, "Failed to update github installations: %v", err) |
| 208 | + return |
| 209 | + } |
| 210 | + |
| 211 | + // reset all existing repos (soft delete) |
| 212 | + slog.Debug("Soft deleting existing repositories", |
| 213 | + "orgId", orgId, |
| 214 | + ) |
| 215 | + |
| 216 | + var ExistingRepos []models.Repo |
| 217 | + err = models.DB.GormDB.Delete(ExistingRepos, "organisation_id=?", orgId).Error |
| 218 | + if err != nil { |
| 219 | + slog.Error("Could not delete repositories", |
| 220 | + "orgId", orgId, |
| 221 | + "error", err, |
| 222 | + ) |
| 223 | + c.String(http.StatusInternalServerError, "could not delete repos: %v", err) |
| 224 | + return |
| 225 | + } |
| 226 | + |
| 227 | + // here we mark repos that are available one by one |
| 228 | + slog.Info("Adding repositories to organization", |
| 229 | + "orgId", orgId, |
| 230 | + "repoCount", len(repos), |
| 231 | + ) |
| 232 | + |
| 233 | + for i, repo := range repos { |
| 234 | + repoFullName := *repo.FullName |
| 235 | + repoOwner := strings.Split(*repo.FullName, "/")[0] |
| 236 | + repoName := *repo.Name |
| 237 | + repoUrl := fmt.Sprintf("https://%v/%v", utils.GetGithubHostname(), repoFullName) |
| 238 | + |
| 239 | + slog.Debug("Processing repository", |
| 240 | + "index", i+1, |
| 241 | + "repoFullName", repoFullName, |
| 242 | + "repoOwner", repoOwner, |
| 243 | + "repoName", repoName, |
| 244 | + ) |
| 245 | + |
| 246 | + _, err := models.DB.GithubRepoAdded( |
| 247 | + installationId64, |
| 248 | + *installation.AppID, |
| 249 | + *installation.Account.Login, |
| 250 | + *installation.Account.ID, |
| 251 | + repoFullName, |
| 252 | + ) |
| 253 | + if err != nil { |
| 254 | + slog.Error("Error recording GitHub repository", |
| 255 | + "repoFullName", repoFullName, |
| 256 | + "error", err, |
| 257 | + ) |
| 258 | + c.String(http.StatusInternalServerError, "github repos added error: %v", err) |
| 259 | + return |
| 260 | + } |
| 261 | + |
| 262 | + cloneUrl := *repo.CloneURL |
| 263 | + defaultBranch := *repo.DefaultBranch |
| 264 | + |
| 265 | + _, _, err = createOrGetDiggerRepoForGithubRepo(repoFullName, repoOwner, repoName, repoUrl, installationId64, *installation.AppID, defaultBranch, cloneUrl) |
| 266 | + if err != nil { |
| 267 | + slog.Error("Error creating or getting Digger repo", |
| 268 | + "repoFullName", repoFullName, |
| 269 | + "error", err, |
| 270 | + ) |
| 271 | + c.String(http.StatusInternalServerError, "createOrGetDiggerRepoForGithubRepo error: %v", err) |
| 272 | + return |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + slog.Info("GitHub app callback processed successfully", |
| 277 | + "installationId", installationId64, |
| 278 | + "orgId", orgId, |
| 279 | + "repoCount", len(repos), |
| 280 | + ) |
| 281 | + |
| 282 | + c.HTML(http.StatusOK, "github_success.tmpl", gin.H{}) |
| 283 | +} |
0 commit comments