Skip to content

Commit

Permalink
Add support for tags
Browse files Browse the repository at this point in the history
  • Loading branch information
yuvalpress committed Dec 23, 2022
1 parent 039fa74 commit 667bc9c
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 167 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ WORKDIR /app

COPY ./src ./

ENV GOTRACEBACK "none"

RUN go mod download
RUN go build -o /version-notifier

Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,10 @@ pod=$(kubectl get pods -n notifier -l app=version-notifier -o yaml | yq '.items[
```

## Upcoming Features ✨
* Support for more notification methods (currently Slack only).
* Support more than one notification method at a time.
* Add support for Pypi repositories
* Add support for private GitHub repositories
* Add support for Docker Images in Dockerhub
<br>

## Want to contribute? 💻
PR's are more than welcome!

Expand Down
2 changes: 1 addition & 1 deletion docker.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.8
1.1.0
9 changes: 5 additions & 4 deletions src/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
repos:
- yuvalpress: version-notifier
- hashicorp: terraform-provider-aws
- hashicorp: terraform-provider-google
- kubernetes: kubectl
# - yuvalpress: version-notifier
- yuvalpress: test
# - hashicorp: terraform-provider-aws
# - hashicorp: terraform-provider-google
# - kubernetes: kubectl
78 changes: 78 additions & 0 deletions src/internal/anchor/anchor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package anchor

import (
jparser "github.com/Jeffail/gabs/v2"
"log"
"os"
"strings"
"yuvalpress/version-notifier/internal/config"
"yuvalpress/version-notifier/internal/utils"
)

var (
// Reset color variables after call
Reset = "\033[0m"

// Red color for logs
Red = "\033[31m"

LogLevel = os.Getenv("LOG_LEVEL")
)

// Anchor holds the first initialized information for the service
type Anchor struct {
RepoList []Latest
}

// Latest holds all the needed information for a repo instance
type Latest struct {
User string
Repo string
Latest string
URL string
}

func (l *Latest) init(t, username, repoName string, data *jparser.Container) {
l.User = username
l.Repo = repoName

if t == "release" {
l.Latest = utils.GetLatestTag(data.Path("tag_name").String(), LogLevel)
l.URL = strings.ReplaceAll(data.Path("html_url").String(), "\"", "")
} else if t == "tag" {
l.Latest = utils.GetLatestTag(data.Path("name").String(), LogLevel)
l.URL = strings.ReplaceAll(data.Path("zipball_url").String(), "\"", "")
}

}

// Init method for main Anchor object
func (a *Anchor) Init() bool {
confData, err := config.ReadConfigFile()
if err != nil {
log.Fatalf("Failed during initialization process with the following error: %v", err)
}

for _, info := range confData.Repos {
for username, repoName := range info {
data, requestType, err := utils.GetVersion(username, repoName)
if err != nil {
log.Printf("Failed getting latest release of "+username+"/"+repoName+" with the following error: "+Red+"%v"+Reset, err)
log.Println("Skipping..")
continue
}

if requestType == "release" && data.Path("tag_name").String() == "" || requestType == "tag" && data.Path("name").String() == "" {
return false
}

log.Println("Fetched latest asset of: " + username + "/" + repoName)

latest := Latest{}
latest.init(requestType, username, repoName, data)
a.RepoList = append(a.RepoList, latest)
}
}

return true
}
28 changes: 28 additions & 0 deletions src/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package config

import (
"gopkg.in/yaml.v3"
"os"
)

// Conf struct holds all the repositories to configure
type Conf struct {
Repos []map[string]string
}

// ReadConfigFile reads the repositories to scrape from the configmap attached to the pod as volume
func ReadConfigFile() (Conf, error) {
var configData Conf
conf, err := os.ReadFile("config.yaml")
if err != nil {
return Conf{}, err
}

err = yaml.Unmarshal(conf, &configData)

if err != nil {
return Conf{}, err
}

return configData, nil
}
95 changes: 73 additions & 22 deletions src/internal/scraper/scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
"strconv"
)

// GetURL build the needed GitHub urls with the received user and repo arguments
func GetURL(username, repoName string) (string, string) {
return "https://api.github.com/repos/" + username + "/" + repoName + "/releases/latest",
"https://api.github.com/repos/" + username + "/" + repoName + "/tags"
}

// getRequest returns a request with all the needed headers
func getRequest(url string) *http.Request {
token, exist := os.LookupEnv("GITHUB_TOKEN")
Expand All @@ -25,47 +31,92 @@ func getRequest(url string) *http.Request {
return req
}

// APIRequest uses a GitHub oauth token to retrieve needed data
func APIRequest(url, LogLevel string) (*jparser.Container, error) {
if LogLevel == "DEBUG" {
log.Println("Fetching latest release from:", url)
}

// request perform the actual request with the given url
func request(url, LogLevel string) (int, *http.Response) {
// initialize request
client := &http.Client{}
req := getRequest(url)

// perform the request
resp, err := client.Do(req)
if err != nil {
return nil, err
return 0, nil
}

// convert to []byte
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return resp.StatusCode, resp
}

_ = resp.Body.Close()
// bodyToJson receives a http.Response pointer object and returns parsed json as pointer
func bodyToJson(resp *http.Response) (*jparser.Container, error) {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

// load json to container object
parseJSON, err := jparser.ParseJSON(bodyBytes)
if err != nil {
return nil, err
}
_ = resp.Body.Close()

return parseJSON, nil
// load json to container object
parseJSON, err := jparser.ParseJSON(bodyBytes)
if err != nil {
return nil, err
}

return parseJSON, nil
}

// buildError build a readable error from the http.Response pointer object received
func buildError(resp *http.Response) error {
r, _ := io.ReadAll(resp.Body)
json, err := jparser.ParseJSON(r)
if err != nil {
return nil, err
return err
}

_ = resp.Body.Close()

return nil, errors.New(strconv.Itoa(resp.StatusCode) + ": request returned with the following message: " + json.Path("message").String())
return errors.New(strconv.Itoa(resp.StatusCode) + ": request returned with the following message: " + json.Path("message").String())
}

func parseTagResponse(json *jparser.Container) *jparser.Container {
return json.Children()[0]
}

// APIRequest uses a GitHub oauth token to retrieve needed data
func APIRequest(username, repoName, LogLevel string) (jsonObject *jparser.Container, requestType string, err error) {
releaseURL, tagURL := GetURL(username, repoName)
if LogLevel == "DEBUG" {
log.Printf("Looking for tag and releases for %s/%s\n", username, repoName)
}

// initialize requests
releaseRequestStatus, resp := request(releaseURL, LogLevel)

// check if release is set for this repo
if releaseRequestStatus == http.StatusOK {
json, err := bodyToJson(resp)
if err != nil {
return nil, "", err
}

return json, "release", nil

} else if releaseRequestStatus == http.StatusNotFound {
tagRequestStatus, resp := request(tagURL, LogLevel)

// check if tag is set for this repo
if tagRequestStatus == http.StatusOK {
json, err := bodyToJson(resp)
if err != nil {
return nil, "", err
}

return parseTagResponse(json), "tag", nil

} else if tagRequestStatus == http.StatusNotFound {
return nil, "", errors.New("No tag or release are set for " + username + "/" + repoName)
}

}

return nil, "", buildError(resp)
}
51 changes: 46 additions & 5 deletions src/internal/utils/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
jparser "github.com/Jeffail/gabs/v2"
"github.com/Masterminds/semver/v3"
validate "golang.org/x/mod/semver"
"log"
Expand All @@ -9,6 +10,9 @@ import (
"strconv"
"strings"
"time"
"yuvalpress/version-notifier/internal/scraper"
"yuvalpress/version-notifier/internal/slack_notifier"
"yuvalpress/version-notifier/internal/telegram_notifier"
)

var (
Expand All @@ -17,12 +21,9 @@ var (

// Red color for logs
Red = "\033[31m"
)

// GetURL build the GitHub url with the needed user and repo
func GetURL(username, repoName string) string {
return "https://api.github.com/repos/" + username + "/" + repoName + "/releases/latest"
}
LogLevel = os.Getenv("LOG_LEVEL")
)

// GetUpdateLevel returns the update level: Major, Minor, Patch
// no need to validate this are semantic version formatted as this portion of the code is executed only after a test
Expand Down Expand Up @@ -123,3 +124,43 @@ func WaitForInterval() {
time.Sleep(time.Duration(intInterval) * time.Minute)
log.Println("Starting new run...")
}

// GetVersion is responsible to fetch the latest data from the relative url
func GetVersion(username, repoName string) (*jparser.Container, string, error) {
json, requestType, err := scraper.APIRequest(username, repoName, LogLevel)
if err != nil {
return nil, "", err
}

return json, requestType, nil
}

// Notify is responsible for notifying a selected Slack channel.
// in the future, more methods will be added
func Notify(user, repo, url, oldVer, newVer string) {
method, found := os.LookupEnv("NOTIFICATION_METHOD")
if !found {
log.Panicln("The NOTIFICATION_METHOD environment variable must be set!")
}

sendFullChangelog, found := os.LookupEnv("SEND_FULL_CHANGELOG")
if !found {
log.Println("The SEND_FULL_CHANGELOG environment variable is not set! Defaulting to `false`")
}

// convert to bool
sendBool, err := strconv.ParseBool(sendFullChangelog)
if err != nil {
log.Panicf("The SEND_FULL_CHANGELOG environment variable must be set to true or false only!")
}

if method == "none" {
log.Panicln("The NOTIFICATION_METHOD environment variable must be set!")

} else if method == "telegram" {
telegram_notifier.Notify(user, repo, url, oldVer, newVer, GetUpdateLevel(oldVer, newVer), sendBool)

} else if method == "slack" {
slack_notifier.Notify(user, repo, url, oldVer, newVer, GetUpdateLevel(oldVer, newVer), sendBool)
}
}
Loading

0 comments on commit 667bc9c

Please sign in to comment.