feat: support local apps.json loading
continuous-integration/drone/pr Build is passing Details

This logic supports the following cases:

- Download a fresh apps.json and load it if missing
- Check if a local apps.json is old and get a fresh one if so
- Always save a local copy after downloading a fresh apps.json

The http.Head() call is faster than a http.Get() call (only carries back
respones headers) and aims to make the more general case more
performant: you have the latest copy of the apps.json and don't need to
download another one. This a direct port of our Bash implementation
logic.

Closes #9.
This commit is contained in:
decentral1se 2021-07-21 18:21:45 +02:00
parent 6b370599fa
commit 1f6c0e8c4b
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
2 changed files with 105 additions and 32 deletions

View File

@ -3,53 +3,60 @@ package cli
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os"
"sort" "sort"
"time" "time"
"coopcloud.tech/abra/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
type Image struct { type AppImage struct {
Image string `json:"image"` Image string `json:"image"`
Rating string `json:"rating"` Rating string `json:"rating"`
Source string `json:"source"` Source string `json:"source"`
URL string `json:"url"` URL string `json:"url"`
} }
type AppFeatureSpec struct { type Feature struct {
Backups string `json:"backups"` Backups string `json:"backups"`
Email string `json:"email"` Email string `json:"email"`
Healthcheck string `json:"healthcheck"` Healthcheck string `json:"healthcheck"`
Image Image `json:"image"` Image AppImage `json:"image"`
Status int `json:"status"` Status int `json:"status"`
Tests string `json:"tests"` Tests string `json:"tests"`
} }
type AppVersionSpec struct { type Version struct {
Digest string `json:"digest"` Digest string `json:"digest"`
Image string `json:"image"` Image string `json:"image"`
Tag string `json:"tag"` Tag string `json:"tag"`
} }
type AppSpec struct { type App struct {
Category string `json:"category"` Category string `json:"category"`
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
Description string `json:"description"` Description string `json:"description"`
Features AppFeatureSpec `json:"features"` Features Feature `json:"features"`
Icon string `json:"icon"` Icon string `json:"icon"`
Name string `json:"name"` Name string `json:"name"`
Repository string `json:"repository"` Repository string `json:"repository"`
Versions map[string]map[string]AppVersionSpec `json:"versions"` Versions map[string]map[string]Version `json:"versions"`
Website string `json:"website"` Website string `json:"website"`
} }
type AppsJson map[string]AppSpec type Apps map[string]App
func getJson(url string, target interface{}) error { var httpClient = &http.Client{Timeout: 5 * time.Second}
client := &http.Client{Timeout: 5 * time.Second}
res, err := client.Get(url) var AppsUrl = "https://apps.coopcloud.tech"
func readJson(url string, target interface{}) error {
res, err := httpClient.Get(url)
if err != nil { if err != nil {
return err return err
} }
@ -57,16 +64,80 @@ func getJson(url string, target interface{}) error {
return json.NewDecoder(res.Body).Decode(target) return json.NewDecoder(res.Body).Decode(target)
} }
func GetAppsJSON() (AppsJson, error) { func AppsFSIsLatest() (bool, error) {
url := "https://apps.coopcloud.tech" res, err := httpClient.Head(AppsUrl)
apps := make(AppsJson) if err != nil {
if err := getJson(url, &apps); err != nil { return false, err
}
lastModified := res.Header["Last-Modified"][0]
parsed, err := time.Parse(time.RFC1123, lastModified)
if err != nil {
return false, err
}
info, err := os.Stat(config.APPS_JSON)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
localModifiedTime := info.ModTime().Unix()
remoteModifiedTime := parsed.Unix()
if localModifiedTime < remoteModifiedTime {
return false, nil
}
return true, nil
}
func ReadAppsFS(target interface{}) error {
appsJsonFS, err := ioutil.ReadFile(config.APPS_JSON)
if err != nil {
return err
}
if err := json.Unmarshal(appsJsonFS, &target); err != nil {
return err
}
return nil
}
func ReadAppsWeb() (Apps, error) {
apps := make(Apps)
appsFSIsLatest, err := AppsFSIsLatest()
if err != nil {
return nil, err return nil, err
} }
if !appsFSIsLatest {
if err := readJson(AppsUrl, &apps); err != nil {
return nil, err
}
appsJson, err := json.MarshalIndent(apps, "", " ")
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(config.APPS_JSON, appsJson, 0644); err != nil {
return nil, err
}
return apps, nil
}
if err := ReadAppsFS(&apps); err != nil {
return nil, err
}
return apps, nil return apps, nil
} }
func sortByAppName(apps AppsJson) []string { func SortByAppName(apps Apps) []string {
var names []string var names []string
for name := range apps { for name := range apps {
names = append(names, name) names = append(names, name)
@ -79,15 +150,16 @@ var recipeListCommand = &cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
apps, err := GetAppsJSON() appSpecs, err := ReadAppsWeb()
if err != nil { if err != nil {
logrus.Fatal(err.Error()) logrus.Fatal(err.Error())
} }
tableCol := []string{"Name", "Category", "Status"} tableCol := []string{"Name", "Category", "Status"}
table := createTable(tableCol) table := createTable(tableCol)
for _, name := range sortByAppName(apps) { for _, appName := range SortByAppName(appSpecs) {
appSpec := apps[name] appSpec := appSpecs[appName]
tableRow := []string{appSpec.Name, appSpec.Category, fmt.Sprintf("%v", appSpec.Features.Status)} status := fmt.Sprintf("%v", appSpec.Features.Status)
tableRow := []string{appSpec.Name, appSpec.Category, status}
table.Append(tableRow) table.Append(tableRow)
} }
table.Render() table.Render()

View File

@ -16,6 +16,7 @@ import (
var ABRA_DIR = os.ExpandEnv("$HOME/.abra") var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers") var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers")
var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
// Type aliases to make code hints easier to understand // Type aliases to make code hints easier to understand
type AppEnv = map[string]string type AppEnv = map[string]string