From b08bdd7f54c174c7db0247c100b8cd7ac756c6b7 Mon Sep 17 00:00:00 2001 From: Luca Lanziani Date: Thu, 30 Nov 2023 13:14:01 +0100 Subject: [PATCH] fix: code refactoring and cleanup (#156) --- src/cli/deploy.go | 16 +++- src/cli/onbranch.go | 19 +---- src/services/git/repo.go | 143 ++++++++++++++++++++---------------- src/services/k8s/knative.go | 20 ++--- 4 files changed, 106 insertions(+), 92 deletions(-) diff --git a/src/cli/deploy.go b/src/cli/deploy.go index 308d06ce..83a82f18 100644 --- a/src/cli/deploy.go +++ b/src/cli/deploy.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "os" "github.com/nearform/initium-cli/src/services/git" knative "github.com/nearform/initium-cli/src/services/k8s" @@ -47,7 +48,20 @@ func (c *icli) Deploy(cCtx *cli.Context) error { return err } - return knative.Apply(serviceManifest, config) + url, err := knative.Apply(serviceManifest, config) + + if err != nil { + return err + } + + _, isPR := os.LookupEnv("GITHUB_HEAD_REF") + if os.Getenv("CI") == "true" && os.Getenv("GITHUB_ACTIONS") == "true" && isPR { + return git.PublishCommentPRGithub(url) + } + + fmt.Fprintf(c.Writer, "You can reach the app via %s\n", url) + + return nil } func (c icli) DeployCMD() *cli.Command { diff --git a/src/cli/onbranch.go b/src/cli/onbranch.go index 34edaf00..5ca82e7b 100644 --- a/src/cli/onbranch.go +++ b/src/cli/onbranch.go @@ -2,8 +2,6 @@ package cli import ( "fmt" - "net/url" - "os" "github.com/nearform/initium-cli/src/services/git" "github.com/nearform/initium-cli/src/utils" @@ -32,22 +30,7 @@ func (c icli) buildPushDeploy(cCtx *cli.Context) error { return err } - err = c.Deploy(cCtx) - appUrl, urlErr := url.Parse(err.Error()) // Check if it contains the app URL or it's a legit error - if urlErr != nil { - fmt.Println("No app URL available") - return err - } - - // Check if the CI environment variable is set to GitHub Actions - if os.Getenv("CI") == "true" && os.Getenv("GITHUB_ACTIONS") == "true" { - err = git.PublishCommentPRGithub(appUrl.String()) - } else { - fmt.Printf("You can reach the app via %s\n", appUrl.String()) - err = nil - } - - return err + return c.Deploy(cCtx) } func (c icli) OnBranchCMD() *cli.Command { diff --git a/src/services/git/repo.go b/src/services/git/repo.go index 35a95969..a55d1155 100644 --- a/src/services/git/repo.go +++ b/src/services/git/repo.go @@ -9,14 +9,17 @@ import ( "strings" "time" + "github.com/charmbracelet/log" git "github.com/go-git/go-git/v5" github "github.com/google/go-github/v56/github" oauth2 "golang.org/x/oauth2" ) const ( - httpgithubprefix = "https://github.com/" - gitgithubprefix = "git@github.com:" + httpgithubprefix = "https://github.com/" + gitgithubprefix = "git@github.com:" + githubRefPattern = `refs/pull/(\d+)/merge` + githubCommentIdentifier = "Deployed by [Initium](https://initium.nearform.com)" // used to find and update existing comments ) func initRepo() (*git.Repository, error) { @@ -115,95 +118,109 @@ func GetGithubOrg() (string, error) { return splitRemote[0], nil } -func PublishCommentPRGithub (url string) error { - var message, owner, repo string - var prNumber int +func buildMarkdownMessage(url string) (string, error) { commitSha, err := GetHash() + if err != nil { + return "", err + } - // Build message - message = fmt.Sprintf("Application URL: %s\n", url) + fmt.Sprintf("Commit hash: %s\n", commitSha) + fmt.Sprintf("Timestamp: %v\n", time.Now()) + message := fmt.Sprintf(githubCommentIdentifier+` +|Application URL | %s | +|:-----------------|:----| +|Commit hash | %s | +|Timestamp | %s | +`, url, commitSha, time.Now().UTC()) + return message, nil +} - // Check GITHUB_TOKEN +func PublishCommentPRGithub(url string) error { token := os.Getenv("GITHUB_TOKEN") + prRef := os.Getenv("GITHUB_REF") + repoInfo := os.Getenv("GITHUB_REPOSITORY") + if token == "" { - return fmt.Errorf("Please set up the GITHUB_TOKEN environment variable") + return fmt.Errorf("GITHUB_TOKEN environment variable not set") } - // Create an authenticated GitHub client - ctx := context.Background() - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) - tc := oauth2.NewClient(ctx, ts) - client := github.NewClient(tc) + // Extract pull request number + prNumber, err := extractPullRequestNumber(prRef) + if err != nil { + return err + } - // Get required data to publish a comment - repoInfo := os.Getenv("GITHUB_REPOSITORY") - repoParts := strings.Split(repoInfo, "/") - if len(repoParts) == 2 { - owner = repoParts[0] - repo = repoParts[1] - } else { - return fmt.Errorf("Invalid repository information") - } - - // Check if the workflow was triggered by a pull request event - eventName := os.Getenv("GITHUB_EVENT_NAME") - if eventName == "pull_request" { - // Get the pull request ref - prRef := os.Getenv("GITHUB_REF") - - // Extract the pull request number using a regular expression - re := regexp.MustCompile(`refs/pull/(\d+)/merge`) - matches := re.FindStringSubmatch(prRef) - - if len(matches) == 2 { - prNumber, err = strconv.Atoi(matches[1]) - if err != nil { - return fmt.Errorf("Error converting string to int: %v", err) - } - } else { - return fmt.Errorf("Unable to extract pull request number from GITHUB_REF") - } - } else { - return fmt.Errorf("This workflow was not triggered by a pull request event") + message, err := buildMarkdownMessage(url) + if err != nil { + return fmt.Errorf("cannot build the message: %v", err) } - // Create comment with body comment := &github.IssueComment{ Body: github.String(message), } - // List comments on the PR + // Get required data to publish a comment + repoParts := strings.Split(repoInfo, "/") + if len(repoParts) != 2 { + return fmt.Errorf("invalid repository information %s", repoInfo) + } + owner := repoParts[0] + repo := repoParts[1] + + // Create an authenticated GitHub client + ctx := context.Background() + client := createGithubClient(ctx, token) + + // Check if we have to update an existing comment comments, _, err := client.Issues.ListComments(ctx, owner, repo, prNumber, nil) if err != nil { return err } - commentID := findExistingCommentIDPRGithub(comments, "Application URL:") // Search for app URL comment - - if commentID != 0 { - // Update existing comment - updatedComment, _, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment) + matchingComments := findExistingGithubComments(comments, githubCommentIdentifier) // Search for app URL comment + if n := len(matchingComments); n != 0 { + log.Infof("%d matching comment[s] found %v, will always update the last one", n, matchingComments) + updatedComment, _, err := client.Issues.EditComment(ctx, owner, repo, matchingComments[n-1], comment) if err != nil { return err } - fmt.Printf("Comment updated successfully: %s\n", updatedComment.GetHTMLURL()) - } else { - // Publish a new comment - newComment, _, err := client.Issues.CreateComment(ctx, owner, repo, prNumber, comment) - if err != nil { - return err - } - fmt.Printf("Comment published: %s\n", newComment.GetHTMLURL()) + log.Infof("Comment updated successfully: %s\n", updatedComment.GetHTMLURL()) + return nil } + // Publish a new comment + newComment, _, err := client.Issues.CreateComment(ctx, owner, repo, prNumber, comment) + if err != nil { + return err + } + log.Infof("Comment published: %s\n", newComment.GetHTMLURL()) return nil } -func findExistingCommentIDPRGithub(comments []*github.IssueComment, targetBody string) int64 { +func createGithubClient(ctx context.Context, token string) *github.Client { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + return github.NewClient(tc) +} + +func extractPullRequestNumber(prRef string) (int, error) { + matches := regexp.MustCompile(githubRefPattern).FindStringSubmatch(prRef) + if len(matches) != 2 { + return 0, fmt.Errorf("unable to extract pull request number from GITHUB_REF %s", prRef) + } + + prNumber, err := strconv.Atoi(matches[1]) + if err != nil { + return 0, fmt.Errorf("error converting string to int: %v", err) + } + return prNumber, nil +} + +func findExistingGithubComments(comments []*github.IssueComment, targetString string) []int64 { + matchingComments := []int64{} for _, comment := range comments { - if strings.Contains(comment.GetBody(), targetBody) { - return comment.GetID() + body := comment.GetBody() + if strings.Contains(body, targetString) && strings.Contains(body, "initium") { + matchingComments = append(matchingComments, comment.GetID()) } } - return 0 + return matchingComments } diff --git a/src/services/k8s/knative.go b/src/services/k8s/knative.go index 4fe4af02..8b7b9de4 100644 --- a/src/services/k8s/knative.go +++ b/src/services/k8s/knative.go @@ -163,11 +163,11 @@ func setEnv(manifest *servingv1.Service, envFile string, manifestEnvVars map[str func loadEnvFile(envFile string, manifestEnvVars map[string]string) ([]corev1.EnvVar, error) { var envVarList []corev1.EnvVar - + if _, err := os.Stat(envFile); errors.Is(err, os.ErrNotExist) { return nil, nil } - + envVariables, err := godotenv.Read(envFile) if err != nil { return nil, fmt.Errorf("Error loading .env file. '%s' already set", err) @@ -204,7 +204,7 @@ func ToYaml(serviceManifest *servingv1.Service) ([]byte, error) { return yaml.JSONToYAML(jsonBytes) } -func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { +func Apply(serviceManifest *servingv1.Service, config *rest.Config) (string, error) { log.Info("Deploying Knative service", "host", config.Host, "name", serviceManifest.ObjectMeta.Name, "namespace", serviceManifest.ObjectMeta.Namespace) ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() @@ -212,12 +212,12 @@ func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { // Create a new Knative Serving client servingClient, err := servingv1client.NewForConfig(config) if err != nil { - return fmt.Errorf("Error creating the knative client %v", err) + return "", fmt.Errorf("Error creating the knative client %v", err) } client, err := kubernetes.NewForConfig(config) if err != nil { - return fmt.Errorf("Creating Kubernetes client %v", err) + return "", fmt.Errorf("Creating Kubernetes client %v", err) } _, err = client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ @@ -227,7 +227,7 @@ func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { }, metav1.CreateOptions{}) if err != nil && !apimachineryErrors.IsAlreadyExists(err) { - return fmt.Errorf("cannot create namespace %s, failed with %v", serviceManifest.ObjectMeta.Namespace, err) + return "", fmt.Errorf("cannot create namespace %s, failed with %v", serviceManifest.ObjectMeta.Namespace, err) } service, err := servingClient.Services(serviceManifest.ObjectMeta.Namespace).Get(ctx, serviceManifest.ObjectMeta.Name, metav1.GetOptions{}) @@ -235,13 +235,13 @@ func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { if err != nil { deployedService, err = servingClient.Services(serviceManifest.ObjectMeta.Namespace).Create(ctx, serviceManifest, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("Creating Knative service %v", err) + return "", fmt.Errorf("Creating Knative service %v", err) } } else { service.Spec = serviceManifest.Spec deployedService, err = servingClient.Services(serviceManifest.ObjectMeta.Namespace).Update(ctx, service, metav1.UpdateOptions{}) if err != nil { - return fmt.Errorf("Updating Knative service %v", err) + return "", fmt.Errorf("Updating Knative service %v", err) } } @@ -250,10 +250,10 @@ func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { for { service, err = servingClient.Services(serviceManifest.ObjectMeta.Namespace).Get(ctx, serviceManifest.ObjectMeta.Name, metav1.GetOptions{}) if err != nil { - return err + return "", err } if service.Status.URL != nil { - return fmt.Errorf("%s", service.Status.URL) // Overload error return variable with URL string + return service.Status.URL.String(), nil } time.Sleep(time.Millisecond * 500)