diff --git a/internal/handler/eolfunctions.go b/internal/handler/eolfunctions.go new file mode 100644 index 0000000..4f8c19a --- /dev/null +++ b/internal/handler/eolfunctions.go @@ -0,0 +1,135 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" +) + +// eolfunctions.go contains all the basic functionality for checking key facts' end of life status against https://endoflife.date/docs/api + +const eloCacheLocation = "/tmp/eol.cache" + +type PackageInfo struct { + Cycle string `json:"cycle"` + ReleaseDate string `json:"releaseDate"` + EOL string `json:"eol"` + Latest string `json:"latest"` + LatestReleaseDate string `json:"latestReleaseDate"` + Link string `json:"link"` + LTS bool `json:"lts"` +} + +type EOLData struct { + Packages map[string][]PackageInfo + CacheLocation string +} + +type NewEOLDataArgs struct { + Packages []string + CacheLocation string + PreventCacheRefresh bool + ForceCacheRefresh bool +} + +// NewEOLData creates a new EOLData struct with end-of-life information for the provided Packages. +func NewEOLData(args NewEOLDataArgs) (*EOLData, error) { + + // basic assertions of logic + if args.ForceCacheRefresh && !args.PreventCacheRefresh { + return nil, fmt.Errorf("You cannot Force Cache Refresh AND Prevent Cache Refresh") + } + + packages := args.Packages + cacheLocation := args.CacheLocation + data := &EOLData{ + CacheLocation: cacheLocation, + } + + // Check if cache file exists + if _, err := os.Stat(data.CacheLocation); err == nil || args.ForceCacheRefresh { + // Cache file exists, load data from file + if err := loadDataFromFile(data.CacheLocation, data); err != nil { + return nil, err + } + } else if os.IsNotExist(err) { + // Cache file does not exist, fetch data and write to file + endOfLifeInfo := GetEndOfLifeInfo(packages) + data.Packages = endOfLifeInfo + + // Write to cache file + if err := writeDataToFile(data.CacheLocation, data); err != nil { + return nil, err + } + } else { + // Some other error occurred + return nil, err + } + + return data, nil +} + +func GetEndOfLifeInfo(packageNames []string) map[string][]PackageInfo { + endOfLifeInfo := make(map[string][]PackageInfo) + + for _, packageName := range packageNames { + url := fmt.Sprintf("https://endoflife.date/api/%s.json", packageName) + response, err := http.Get(url) + if err != nil { + fmt.Printf("Error getting end of life info for %s: %v\n", packageName, err) + continue + } + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + fmt.Printf("Error reading response body for %s: %v\n", packageName, err) + continue + } + + var data []PackageInfo + if err := json.Unmarshal(body, &data); err != nil { + fmt.Printf("Error parsing JSON for %s: %v\n", packageName, err) + continue + } + + // Assuming the API returns an array of PackageInfo + if len(data) > 0 { + endOfLifeInfo[packageName] = data // Assuming we're interested in the first entry + } + } + + return endOfLifeInfo +} + +// loadDataFromFile loads data from a file into an EOLData struct. +func loadDataFromFile(filename string, data *EOLData) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + decoder := json.NewDecoder(file) + if err := decoder.Decode(data); err != nil { + return err + } + + return nil +} + +// writeDataToFile writes data from an EOLData struct to a file. +func writeDataToFile(filename string, data *EOLData) error { + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + if err := ioutil.WriteFile(filename, jsonData, 0644); err != nil { + return err + } + + return nil +} diff --git a/internal/handler/eolfunctions_test.go b/internal/handler/eolfunctions_test.go new file mode 100644 index 0000000..60b8fbd --- /dev/null +++ b/internal/handler/eolfunctions_test.go @@ -0,0 +1,128 @@ +package handler + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestGetEndOfLifeInfo(t *testing.T) { + type args struct { + packageNames []string + } + tests := []struct { + name string + args args + wantResponse bool + }{ + { + name: "Get alpine information", + args: args{packageNames: []string{ + "alpine", + "ubuntu", + }}, + wantResponse: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetEndOfLifeInfo(tt.args.packageNames) + + if tt.wantResponse == true { + for _, name := range tt.args.packageNames { + if len(got[name]) == 0 { + t.Errorf("Expected data for package %v, got nothing", name) + } + } + } + }) + } +} + +func TestNewEOLData(t *testing.T) { + type args struct { + EolArgs NewEOLDataArgs + } + tests := []struct { + name string + args args + want *EOLData + wantErr bool + }{ + { + name: "Test No Cache", + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "testnocache.json", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Let's set up a temporary location to save the incoming data + dir, err := os.MkdirTemp("", "*-test") + if err != nil { + t.Errorf("Unable to create test directory") + return + } + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + fmt.Println("Unable to remove directory: ", path) + } + }(dir) + tt.args.EolArgs.CacheLocation = filepath.Join(dir, tt.args.EolArgs.CacheLocation) + got, err := NewEOLData(tt.args.EolArgs) + if (err != nil) != tt.wantErr { + t.Errorf("NewEOLData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got.Packages[tt.args.EolArgs.Packages[0]]) == 0 { + t.Errorf("Could not find any Packages for package '%v'\n", tt.args.EolArgs.Packages[0]) + } + + }) + } +} + +func TestNewEOLDataWithExistingCache(t *testing.T) { + type args struct { + EolArgs NewEOLDataArgs + } + tests := []struct { + name string + args args + want *EOLData + wantErr bool + }{ + { + name: "Test No Cache", + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "testassets/EOLdata/testnocache.json", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Let's set up a temporary location to save the incoming data + got, err := NewEOLData(tt.args.EolArgs) + if (err != nil) != tt.wantErr { + t.Errorf("NewEOLData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got.Packages[tt.args.EolArgs.Packages[0]]) == 0 { + t.Errorf("Could not find any Packages for package '%v'\n", tt.args.EolArgs.Packages[0]) + } + }) + } +} diff --git a/internal/handler/testassets/EOLdata/testnocache.json b/internal/handler/testassets/EOLdata/testnocache.json new file mode 100644 index 0000000..223702b --- /dev/null +++ b/internal/handler/testassets/EOLdata/testnocache.json @@ -0,0 +1,123 @@ +{ + "Packages": { + "alpine": [ + { + "cycle": "3.19", + "releaseDate": "2023-12-07", + "eol": "2025-11-01", + "latest": "3.19.1", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.19.1-released.html", + "lts": false + }, + { + "cycle": "3.18", + "releaseDate": "2023-05-09", + "eol": "2025-05-09", + "latest": "3.18.6", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.17", + "releaseDate": "2022-11-22", + "eol": "2024-11-22", + "latest": "3.17.7", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.16", + "releaseDate": "2022-05-23", + "eol": "2024-05-23", + "latest": "3.16.9", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.15", + "releaseDate": "2021-11-24", + "eol": "2023-11-01", + "latest": "3.15.11", + "latestReleaseDate": "2023-11-30", + "link": "https://alpinelinux.org/posts/Alpine-3.15.10-3.16.7-3.17.5-3.18.3-released.html", + "lts": false + }, + { + "cycle": "3.14", + "releaseDate": "2021-06-15", + "eol": "2023-05-01", + "latest": "3.14.10", + "latestReleaseDate": "2023-03-29", + "link": "https://alpinelinux.org/posts/Alpine-3.14.10-3.15.8-3.16.5-released.html", + "lts": false + }, + { + "cycle": "3.13", + "releaseDate": "2021-01-14", + "eol": "2022-11-01", + "latest": "3.13.12", + "latestReleaseDate": "2022-08-09", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.12", + "releaseDate": "2020-05-29", + "eol": "2022-05-01", + "latest": "3.12.12", + "latestReleaseDate": "2022-04-04", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.11", + "releaseDate": "2019-12-19", + "eol": "2021-11-01", + "latest": "3.11.13", + "latestReleaseDate": "2021-11-12", + "link": "https://alpinelinux.org/posts/Alpine-3.11.13-3.12.9-3.13.7-released.html", + "lts": false + }, + { + "cycle": "3.10", + "releaseDate": "2019-06-19", + "eol": "2021-05-01", + "latest": "3.10.9", + "latestReleaseDate": "2021-04-14", + "link": "https://alpinelinux.org/posts/Alpine-3.10.9-3.11.11-3.12.7-released.html", + "lts": false + }, + { + "cycle": "3.9", + "releaseDate": "2019-01-29", + "eol": "2020-11-01", + "latest": "3.9.6", + "latestReleaseDate": "2020-04-23", + "link": "https://alpinelinux.org/posts/Alpine-3.9.6-and-3.10.5-released.html", + "lts": false + }, + { + "cycle": "3.8", + "releaseDate": "2018-06-26", + "eol": "2020-05-01", + "latest": "3.8.5", + "latestReleaseDate": "2020-01-23", + "link": "https://git.alpinelinux.org/aports/log/?h=3.8-stable", + "lts": false + }, + { + "cycle": "3.7", + "releaseDate": "2017-11-30", + "eol": "2019-11-01", + "latest": "3.7.3", + "latestReleaseDate": "2019-03-06", + "link": "https://git.alpinelinux.org/aports/log/?h=3.7-stable", + "lts": false + } + ] + } +} \ No newline at end of file