Finally had to fork godotenv because it strips comments and we need
those to parse length values (e.g. "FOO=v1 # length=10") (or in other
words, motivation to move to the YAML format).
There is a new secret module now, with functionality for dealing with
generation and parsing of secrets.
The final output needs some work and there is also the final step of
implementing the sending of secrets to the docker daemon. Coming Soon
™️.
215 lines
4.7 KiB
Go
215 lines
4.7 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"coopcloud.tech/abra/config"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
)
|
|
|
|
type Image struct {
|
|
Image string `json:"image"`
|
|
Rating string `json:"rating"`
|
|
Source string `json:"source"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type Feature 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 Tag = string
|
|
type Service = string
|
|
type ServiceMeta struct {
|
|
Digest string `json:"digest"`
|
|
Image string `json:"image"`
|
|
Tag string `json:"tag"`
|
|
}
|
|
|
|
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[Tag]map[Service]ServiceMeta `json:"versions"`
|
|
Website string `json:"website"`
|
|
}
|
|
|
|
func (a App) EnsureExists() error {
|
|
appDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(a.Name))
|
|
if _, err := os.Stat(appDir); os.IsNotExist(err) {
|
|
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, a.Name)
|
|
_, err := git.PlainClone(appDir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a App) EnsureVersion(version string) error {
|
|
appDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(a.Name))
|
|
|
|
repo, err := git.PlainOpen(appDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tags, err := repo.Tags()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var tagRef plumbing.ReferenceName
|
|
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
|
|
if ref.Name().Short() == version {
|
|
tagRef = ref.Name()
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if tagRef.String() == "" {
|
|
return fmt.Errorf("%s is not available?", version)
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := &git.CheckoutOptions{Branch: tagRef, Keep: true}
|
|
if err := worktree.Checkout(opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a App) LatestVersion() string {
|
|
var latestVersion string
|
|
for tag := range a.Versions {
|
|
// apps.json versions are sorted so the last key is latest
|
|
latestVersion = tag
|
|
}
|
|
return latestVersion
|
|
}
|
|
|
|
type Name = string
|
|
type AppsCatalogue map[Name]App
|
|
|
|
func (a AppsCatalogue) Flatten() []App {
|
|
apps := make([]App, 0, len(a))
|
|
for name := range a {
|
|
apps = append(apps, a[name])
|
|
}
|
|
return apps
|
|
}
|
|
|
|
type ByAppName []App
|
|
|
|
func (a ByAppName) Len() int { return len(a) }
|
|
func (a ByAppName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a ByAppName) Less(i, j int) bool {
|
|
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
|
|
}
|
|
|
|
var AppsCatalogueURL = "https://apps.coopcloud.tech"
|
|
|
|
func AppsCatalogueFSIsLatest() (bool, error) {
|
|
httpClient := &http.Client{Timeout: 5 * time.Second}
|
|
res, err := httpClient.Head(AppsCatalogueURL)
|
|
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 ReadAppsCatalogue() (AppsCatalogue, error) {
|
|
apps := make(AppsCatalogue)
|
|
|
|
appsFSIsLatest, err := AppsCatalogueFSIsLatest()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !appsFSIsLatest {
|
|
if err := ReadAppsCatalogueWeb(&apps); err != nil {
|
|
return nil, err
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
if err := ReadAppsCatalogueFS(&apps); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return apps, nil
|
|
}
|
|
|
|
func ReadAppsCatalogueFS(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 ReadAppsCatalogueWeb(target interface{}) error {
|
|
if err := readJson(AppsCatalogueURL, &target); err != nil {
|
|
return err
|
|
}
|
|
|
|
appsJson, err := json.MarshalIndent(target, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ioutil.WriteFile(config.APPS_JSON, appsJson, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|