forked from toolshed/abra
		
	feat: support local apps.json loading
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 https://git.autonomic.zone/coop-cloud/go-abra/issues/9.
This commit is contained in:
		
							
								
								
									
										136
									
								
								cli/recipe.go
									
									
									
									
									
								
							
							
						
						
									
										136
									
								
								cli/recipe.go
									
									
									
									
									
								
							@ -3,53 +3,60 @@ package cli
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/config"
 | 
			
		||||
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Image struct {
 | 
			
		||||
type AppImage struct {
 | 
			
		||||
	Image  string `json:"image"`
 | 
			
		||||
	Rating string `json:"rating"`
 | 
			
		||||
	Source string `json:"source"`
 | 
			
		||||
	URL    string `json:"url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AppFeatureSpec struct {
 | 
			
		||||
	Backups     string `json:"backups"`
 | 
			
		||||
	Email       string `json:"email"`
 | 
			
		||||
	Healthcheck string `json:"healthcheck"`
 | 
			
		||||
	Image       Image  `json:"image"`
 | 
			
		||||
	Status      int    `json:"status"`
 | 
			
		||||
	Tests       string `json:"tests"`
 | 
			
		||||
type Feature struct {
 | 
			
		||||
	Backups     string   `json:"backups"`
 | 
			
		||||
	Email       string   `json:"email"`
 | 
			
		||||
	Healthcheck string   `json:"healthcheck"`
 | 
			
		||||
	Image       AppImage `json:"image"`
 | 
			
		||||
	Status      int      `json:"status"`
 | 
			
		||||
	Tests       string   `json:"tests"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AppVersionSpec struct {
 | 
			
		||||
type Version struct {
 | 
			
		||||
	Digest string `json:"digest"`
 | 
			
		||||
	Image  string `json:"image"`
 | 
			
		||||
	Tag    string `json:"tag"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AppSpec struct {
 | 
			
		||||
	Category      string                               `json:"category"`
 | 
			
		||||
	DefaultBranch string                               `json:"default_branch"`
 | 
			
		||||
	Description   string                               `json:"description"`
 | 
			
		||||
	Features      AppFeatureSpec                       `json:"features"`
 | 
			
		||||
	Icon          string                               `json:"icon"`
 | 
			
		||||
	Name          string                               `json:"name"`
 | 
			
		||||
	Repository    string                               `json:"repository"`
 | 
			
		||||
	Versions      map[string]map[string]AppVersionSpec `json:"versions"`
 | 
			
		||||
	Website       string                               `json:"website"`
 | 
			
		||||
type App struct {
 | 
			
		||||
	Category      string                        `json:"category"`
 | 
			
		||||
	DefaultBranch string                        `json:"default_branch"`
 | 
			
		||||
	Description   string                        `json:"description"`
 | 
			
		||||
	Features      Feature                       `json:"features"`
 | 
			
		||||
	Icon          string                        `json:"icon"`
 | 
			
		||||
	Name          string                        `json:"name"`
 | 
			
		||||
	Repository    string                        `json:"repository"`
 | 
			
		||||
	Versions      map[string]map[string]Version `json:"versions"`
 | 
			
		||||
	Website       string                        `json:"website"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AppsJson map[string]AppSpec
 | 
			
		||||
type Apps map[string]App
 | 
			
		||||
 | 
			
		||||
func getJson(url string, target interface{}) error {
 | 
			
		||||
	client := &http.Client{Timeout: 5 * time.Second}
 | 
			
		||||
	res, err := client.Get(url)
 | 
			
		||||
var httpClient = &http.Client{Timeout: 5 * time.Second}
 | 
			
		||||
 | 
			
		||||
var AppsUrl = "https://apps.coopcloud.tech"
 | 
			
		||||
 | 
			
		||||
func readJson(url string, target interface{}) error {
 | 
			
		||||
	res, err := httpClient.Get(url)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@ -57,16 +64,80 @@ func getJson(url string, target interface{}) error {
 | 
			
		||||
	return json.NewDecoder(res.Body).Decode(target)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAppsJSON() (AppsJson, error) {
 | 
			
		||||
	url := "https://apps.coopcloud.tech"
 | 
			
		||||
	apps := make(AppsJson)
 | 
			
		||||
	if err := getJson(url, &apps); err != nil {
 | 
			
		||||
func AppsFSIsLatest() (bool, error) {
 | 
			
		||||
	res, err := httpClient.Head(AppsUrl)
 | 
			
		||||
	if 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sortByAppName(apps AppsJson) []string {
 | 
			
		||||
func SortByAppName(apps Apps) []string {
 | 
			
		||||
	var names []string
 | 
			
		||||
	for name := range apps {
 | 
			
		||||
		names = append(names, name)
 | 
			
		||||
@ -79,15 +150,16 @@ var recipeListCommand = &cli.Command{
 | 
			
		||||
	Name:    "list",
 | 
			
		||||
	Aliases: []string{"ls"},
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		apps, err := GetAppsJSON()
 | 
			
		||||
		appSpecs, err := ReadAppsWeb()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		tableCol := []string{"Name", "Category", "Status"}
 | 
			
		||||
		table := createTable(tableCol)
 | 
			
		||||
		for _, name := range sortByAppName(apps) {
 | 
			
		||||
			appSpec := apps[name]
 | 
			
		||||
			tableRow := []string{appSpec.Name, appSpec.Category, fmt.Sprintf("%v", appSpec.Features.Status)}
 | 
			
		||||
		for _, appName := range SortByAppName(appSpecs) {
 | 
			
		||||
			appSpec := appSpecs[appName]
 | 
			
		||||
			status := fmt.Sprintf("%v", appSpec.Features.Status)
 | 
			
		||||
			tableRow := []string{appSpec.Name, appSpec.Category, status}
 | 
			
		||||
			table.Append(tableRow)
 | 
			
		||||
		}
 | 
			
		||||
		table.Render()
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import (
 | 
			
		||||
 | 
			
		||||
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
 | 
			
		||||
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 AppEnv = map[string]string
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user