Skip to content

Commit 5e62218

Browse files
wip generation of terraform code from application code (#1855)
* terraform code generation demo --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 3965287 commit 5e62218

File tree

2 files changed

+232
-11
lines changed

2 files changed

+232
-11
lines changed

backend/controllers/github.go

+163-11
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,37 @@ import (
1010
"github.com/diggerhq/digger/backend/ci_backends"
1111
config2 "github.com/diggerhq/digger/backend/config"
1212
"github.com/diggerhq/digger/backend/locking"
13+
"github.com/diggerhq/digger/backend/middleware"
14+
"github.com/diggerhq/digger/backend/models"
1315
"github.com/diggerhq/digger/backend/segment"
1416
"github.com/diggerhq/digger/backend/services"
17+
"github.com/diggerhq/digger/backend/utils"
1518
"github.com/diggerhq/digger/libs/ci"
1619
"github.com/diggerhq/digger/libs/ci/generic"
20+
dg_github "github.com/diggerhq/digger/libs/ci/github"
1721
comment_updater "github.com/diggerhq/digger/libs/comment_utils/reporting"
22+
dg_configuration "github.com/diggerhq/digger/libs/digger_config"
1823
dg_locking "github.com/diggerhq/digger/libs/locking"
1924
orchestrator_scheduler "github.com/diggerhq/digger/libs/scheduler"
25+
"github.com/dominikbraun/graph"
26+
"github.com/gin-gonic/gin"
27+
"github.com/google/go-github/v61/github"
2028
"github.com/google/uuid"
29+
"github.com/samber/lo"
30+
"golang.org/x/oauth2"
2131
"gorm.io/gorm"
2232
"log"
2333
"math/rand"
2434
"net/http"
2535
"net/url"
2636
"os"
2737
"path"
38+
"path/filepath"
2839
"reflect"
2940
"runtime/debug"
3041
"slices"
3142
"strconv"
3243
"strings"
33-
34-
"github.com/diggerhq/digger/backend/middleware"
35-
"github.com/diggerhq/digger/backend/models"
36-
"github.com/diggerhq/digger/backend/utils"
37-
dg_github "github.com/diggerhq/digger/libs/ci/github"
38-
dg_configuration "github.com/diggerhq/digger/libs/digger_config"
39-
"github.com/dominikbraun/graph"
40-
"github.com/gin-gonic/gin"
41-
"github.com/google/go-github/v61/github"
42-
"github.com/samber/lo"
43-
"golang.org/x/oauth2"
4444
)
4545

4646
type IssueCommentHook func(gh utils.GithubClientProvider, payload *github.IssueCommentEvent, ciBackendProvider ci_backends.CiBackendProvider) error
@@ -765,6 +765,158 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
765765
return fmt.Errorf("error getting digger config")
766766
}
767767

768+
// terraform code generator
769+
if os.Getenv("DIGGER_GENERATION_ENABLED") == "1" {
770+
if strings.HasPrefix(*payload.Comment.Body, "digger generate") {
771+
projectName := ci.ParseProjectName(*payload.Comment.Body)
772+
if projectName == "" {
773+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: generate requires argument -p <project_name>: %v", err))
774+
log.Printf("missing project in command: %v", *payload.Comment.Body)
775+
return fmt.Errorf("generate requires argument -p <project_name>: %v", err)
776+
}
777+
778+
project := config.GetProject(projectName)
779+
if project == nil {
780+
commentReporterManager.UpdateComment(fmt.Sprintf("could not find project %v in digger.yml", projectName))
781+
log.Printf("could not find project %v in digger.yml", projectName)
782+
return fmt.Errorf("could not find project %v in digger.yml", projectName)
783+
}
784+
785+
commentReporterManager.UpdateComment(fmt.Sprintf(":white_check_mark: Successfully loaded project"))
786+
787+
generationEndpoint := os.Getenv("DIGGER_GENERATION_ENDPOINT")
788+
if generationEndpoint == "" {
789+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: server does not have generation endpoint configured, please verify"))
790+
log.Printf("server does not have generation endpoint configured, please verify")
791+
return fmt.Errorf("server does not have generation endpoint configured, please verify")
792+
}
793+
webhookSecret := os.Getenv("DIGGER_GENERATION_WEBHOOK_SECRET")
794+
795+
// Get all code content from the repository at a specific commit
796+
getCodeFromCommit := func(ghService *dg_github.GithubService, repoOwner, repoName string, commitSha *string, projectDir string) (string, error) {
797+
const MaxPatchSize = 1024 * 1024 // 1MB limit
798+
799+
// Get the commit's changes compared to default branch
800+
comparison, _, err := ghService.Client.Repositories.CompareCommits(
801+
context.Background(),
802+
repoOwner,
803+
repoName,
804+
defaultBranch,
805+
*commitSha,
806+
nil,
807+
)
808+
if err != nil {
809+
return "", fmt.Errorf("error comparing commits: %v", err)
810+
}
811+
812+
var appCode strings.Builder
813+
for _, file := range comparison.Files {
814+
if file.Patch == nil {
815+
continue // Skip files without patches
816+
}
817+
log.Printf("Processing patch for file: %s", *file.Filename)
818+
if *file.Additions > 0 {
819+
lines := strings.Split(*file.Patch, "\n")
820+
for _, line := range lines {
821+
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
822+
appCode.WriteString(strings.TrimPrefix(line, "+"))
823+
appCode.WriteString("\n")
824+
}
825+
}
826+
}
827+
appCode.WriteString("\n")
828+
}
829+
830+
if appCode.Len() == 0 {
831+
return "", fmt.Errorf("no code changes found in commit %s. Please ensure the PR contains added or modified code", *commitSha)
832+
}
833+
834+
return appCode.String(), nil
835+
}
836+
837+
appCode, err := getCodeFromCommit(ghService, repoOwner, repoName, commitSha, project.Dir)
838+
if err != nil {
839+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Failed to get code content: %v", err))
840+
log.Printf("Error getting code content: %v", err)
841+
return fmt.Errorf("error getting code content: %v", err)
842+
}
843+
844+
commentReporterManager.UpdateComment(fmt.Sprintf(":white_check_mark: Successfully loaded code from commit"))
845+
846+
log.Printf("the app code is: %v", appCode)
847+
848+
commentReporterManager.UpdateComment(fmt.Sprintf("Generating terraform..."))
849+
terraformCode, err := utils.GenerateTerraformCode(appCode, generationEndpoint, webhookSecret)
850+
if err != nil {
851+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: could not generate terraform code: %v", err))
852+
log.Printf("could not generate terraform code: %v", err)
853+
return fmt.Errorf("could not generate terraform code: %v", err)
854+
}
855+
856+
commentReporterManager.UpdateComment(fmt.Sprintf(":white_check_mark: Generated terraform"))
857+
858+
// comment terraform code to project dir
859+
//project.Dir
860+
log.Printf("terraform code is %v", terraformCode)
861+
862+
baseTree, _, err := ghService.Client.Git.GetTree(context.Background(), repoOwner, repoName, *commitSha, false)
863+
if err != nil {
864+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Failed to get base tree: %v", err))
865+
log.Printf("Error getting base tree: %v", err)
866+
return fmt.Errorf("error getting base tree: %v", err)
867+
}
868+
869+
// Create a new tree with the new file
870+
treeEntries := []*github.TreeEntry{
871+
{
872+
Path: github.String(filepath.Join(project.Dir, fmt.Sprintf("generated_%v.tf", issueNumber))),
873+
Mode: github.String("100644"),
874+
Type: github.String("blob"),
875+
Content: github.String(terraformCode),
876+
},
877+
}
878+
879+
newTree, _, err := ghService.Client.Git.CreateTree(context.Background(), repoOwner, repoName, *baseTree.SHA, treeEntries)
880+
if err != nil {
881+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Failed to create new tree: %v", err))
882+
log.Printf("Error creating new tree: %v", err)
883+
return fmt.Errorf("error creating new tree: %v", err)
884+
}
885+
886+
// Create the commit
887+
commitMsg := fmt.Sprintf("Add generated Terraform code for %v", projectName)
888+
commit := &github.Commit{
889+
Message: &commitMsg,
890+
Tree: newTree,
891+
Parents: []*github.Commit{{SHA: commitSha}},
892+
}
893+
894+
newCommit, _, err := ghService.Client.Git.CreateCommit(context.Background(), repoOwner, repoName, commit, nil)
895+
if err != nil {
896+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Failed to commit Terraform file: %v", err))
897+
log.Printf("Error committing Terraform file: %v", err)
898+
return fmt.Errorf("error committing Terraform file: %v", err)
899+
}
900+
901+
// Update the reference to point to the new commit
902+
ref := &github.Reference{
903+
Ref: github.String(fmt.Sprintf("refs/heads/%s", *branch)),
904+
Object: &github.GitObject{
905+
SHA: newCommit.SHA,
906+
},
907+
}
908+
_, _, err = ghService.Client.Git.UpdateRef(context.Background(), repoOwner, repoName, ref, false)
909+
if err != nil {
910+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Failed to update branch reference: %v", err))
911+
log.Printf("Error updating branch reference: %v", err)
912+
return fmt.Errorf("error updating branch reference: %v", err)
913+
}
914+
915+
commentReporterManager.UpdateComment(":white_check_mark: Successfully generated and committed Terraform code")
916+
return nil
917+
}
918+
}
919+
768920
commentIdStr := strconv.FormatInt(userCommentId, 10)
769921
err = ghService.CreateCommentReaction(commentIdStr, string(dg_github.GithubCommentEyesReaction))
770922
if err != nil {

backend/utils/ai.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package utils
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
)
10+
11+
func GenerateTerraformCode(appCode string, generationEndpoint string, webhookSecret string) (string, error) {
12+
13+
payload := map[string]string{
14+
"code": appCode,
15+
}
16+
17+
// Convert payload to JSON
18+
jsonData, err := json.Marshal(payload)
19+
if err != nil {
20+
return "", fmt.Errorf("Error marshalling JSON: %v\n", err)
21+
}
22+
23+
// Create request
24+
req, err := http.NewRequest("POST", generationEndpoint, bytes.NewBuffer(jsonData))
25+
if err != nil {
26+
return "", fmt.Errorf("Error creating request: %v\n", err)
27+
}
28+
29+
// Set headers
30+
req.Header.Set("Content-Type", "application/json")
31+
req.Header.Set("X-Webhook-Secret", webhookSecret) // Replace with your webhook secret
32+
33+
// Make the request
34+
client := &http.Client{}
35+
resp, err := client.Do(req)
36+
if err != nil {
37+
return "", fmt.Errorf("Error making request: %v\n", err)
38+
}
39+
defer resp.Body.Close()
40+
41+
// Read response
42+
body, err := io.ReadAll(resp.Body)
43+
if err != nil {
44+
return "", fmt.Errorf("Error reading response: %v\n", err)
45+
}
46+
47+
// Print response
48+
if resp.StatusCode == 400 {
49+
return "", fmt.Errorf("unable to generate terraform code from the code available, is it valid application code")
50+
}
51+
52+
if resp.StatusCode != 200 {
53+
return "", fmt.Errorf("unexpected error occured while generating code")
54+
}
55+
56+
type GeneratorResponse struct {
57+
Result string `json:"result"`
58+
Status string `json:"status"`
59+
}
60+
61+
var response GeneratorResponse
62+
err = json.Unmarshal(body, &response)
63+
if err != nil {
64+
return "", fmt.Errorf("unable to parse generator response: %v", err)
65+
}
66+
67+
return response.Result, nil
68+
69+
}

0 commit comments

Comments
 (0)