Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: code refactoring and cleanup #156

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/cli/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 1 addition & 18 deletions src/cli/onbranch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
143 changes: 80 additions & 63 deletions src/services/git/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
20 changes: 10 additions & 10 deletions src/services/k8s/knative.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -204,20 +204,20 @@ 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()

// 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{
Expand All @@ -227,21 +227,21 @@ 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{})
var deployedService *servingv1.Service
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)
}
}

Expand All @@ -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)
Expand Down