2021-09-04 22:14:27 +00:00
|
|
|
// Package catalogue provides ways of interacting with recipe catalogues which
|
|
|
|
// are JSON data structures which contain meta information about recipes (e.g.
|
|
|
|
// what versions of the Nextcloud recipe are available?).
|
2021-07-28 20:10:13 +00:00
|
|
|
package catalogue
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2021-07-30 11:16:28 +00:00
|
|
|
"fmt"
|
2021-07-28 20:10:13 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2021-09-05 19:37:03 +00:00
|
|
|
"coopcloud.tech/abra/pkg/config"
|
2021-09-08 10:55:33 +00:00
|
|
|
"coopcloud.tech/abra/pkg/recipe"
|
2021-09-05 19:37:03 +00:00
|
|
|
"coopcloud.tech/abra/pkg/web"
|
2021-09-10 22:54:02 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2021-07-28 20:10:13 +00:00
|
|
|
)
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// RecipeCatalogueURL is the only current recipe catalogue available.
|
|
|
|
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
|
|
|
|
|
2021-09-20 07:38:51 +00:00
|
|
|
// ReposMetadataURL is the recipe repository metadata
|
|
|
|
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// image represents a recipe container image.
|
|
|
|
type image struct {
|
2021-07-28 20:10:13 +00:00
|
|
|
Image string `json:"image"`
|
|
|
|
Rating string `json:"rating"`
|
|
|
|
Source string `json:"source"`
|
|
|
|
URL string `json:"url"`
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// features represent what top-level features a recipe supports (e.g. does this
|
|
|
|
// recipe support backups?).
|
|
|
|
type features struct {
|
2021-07-28 20:10:13 +00:00
|
|
|
Backups string `json:"backups"`
|
|
|
|
Email string `json:"email"`
|
|
|
|
Healthcheck string `json:"healthcheck"`
|
2021-09-04 22:14:27 +00:00
|
|
|
Image image `json:"image"`
|
2021-07-28 20:10:13 +00:00
|
|
|
Status int `json:"status"`
|
|
|
|
Tests string `json:"tests"`
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// tag represents a git tag.
|
|
|
|
type tag = string
|
|
|
|
|
|
|
|
// service represents a service within a recipe.
|
|
|
|
type service = string
|
|
|
|
|
|
|
|
// serviceMeta represents meta info associated with a service.
|
|
|
|
type serviceMeta struct {
|
2021-07-28 20:10:13 +00:00
|
|
|
Digest string `json:"digest"`
|
|
|
|
Image string `json:"image"`
|
|
|
|
Tag string `json:"tag"`
|
|
|
|
}
|
|
|
|
|
2021-09-05 23:47:59 +00:00
|
|
|
// RecipeMeta represents metadata for a recipe in the abra catalogue.
|
|
|
|
type RecipeMeta struct {
|
2021-09-06 14:51:42 +00:00
|
|
|
Category string `json:"category"`
|
|
|
|
DefaultBranch string `json:"default_branch"`
|
|
|
|
Description string `json:"description"`
|
|
|
|
Features features `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"`
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// LatestVersion returns the latest version of a recipe.
|
2021-09-05 23:47:59 +00:00
|
|
|
func (r RecipeMeta) LatestVersion() string {
|
2021-09-06 14:51:42 +00:00
|
|
|
var version string
|
|
|
|
|
|
|
|
// apps.json versions are sorted so the last key is latest
|
|
|
|
latest := r.Versions[len(r.Versions)-1]
|
|
|
|
|
|
|
|
for tag := range latest {
|
|
|
|
version = tag
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
2021-09-06 14:51:42 +00:00
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
|
|
|
|
|
2021-09-06 14:51:42 +00:00
|
|
|
return version
|
2021-07-31 10:47:09 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
// Name represents a recipe name.
|
2021-07-28 20:10:13 +00:00
|
|
|
type Name = string
|
2021-09-04 22:14:27 +00:00
|
|
|
|
|
|
|
// RecipeCatalogue represents the entire recipe catalogue.
|
2021-09-05 23:47:59 +00:00
|
|
|
type RecipeCatalogue map[Name]RecipeMeta
|
2021-07-28 20:10:13 +00:00
|
|
|
|
2021-08-02 06:36:35 +00:00
|
|
|
// Flatten converts AppCatalogue to slice
|
2021-09-05 23:47:59 +00:00
|
|
|
func (r RecipeCatalogue) Flatten() []RecipeMeta {
|
|
|
|
recipes := make([]RecipeMeta, 0, len(r))
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
for name := range r {
|
|
|
|
recipes = append(recipes, r[name])
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
return recipes
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// ByRecipeName sorts recipes by name.
|
2021-09-05 23:47:59 +00:00
|
|
|
type ByRecipeName []RecipeMeta
|
2021-07-28 20:10:13 +00:00
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
func (r ByRecipeName) Len() int { return len(r) }
|
|
|
|
func (r ByRecipeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
|
|
func (r ByRecipeName) Less(i, j int) bool {
|
|
|
|
return strings.ToLower(r[i].Name) < strings.ToLower(r[j].Name)
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
|
|
|
|
// is up to date.
|
2021-09-04 22:14:27 +00:00
|
|
|
func recipeCatalogueFSIsLatest() (bool, error) {
|
2021-09-04 21:18:34 +00:00
|
|
|
httpClient := &http.Client{Timeout: web.Timeout}
|
2021-09-04 22:14:27 +00:00
|
|
|
res, err := httpClient.Head(RecipeCatalogueURL)
|
2021-07-28 20:10:13 +00:00
|
|
|
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
|
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-07-28 20:10:13 +00:00
|
|
|
info, err := os.Stat(config.APPS_JSON)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("no recipe catalogue found in file system cache")
|
2021-07-28 20:10:13 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
localModifiedTime := info.ModTime().Unix()
|
|
|
|
remoteModifiedTime := parsed.Unix()
|
|
|
|
|
|
|
|
if localModifiedTime < remoteModifiedTime {
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debug("file system cached recipe catalogue is out-of-date")
|
2021-07-28 20:10:13 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debug("file system cached recipe catalogue is up-to-date")
|
|
|
|
|
2021-07-28 20:10:13 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// ReadRecipeCatalogue reads the recipe catalogue.
|
2021-09-04 22:14:27 +00:00
|
|
|
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
|
|
|
|
recipes := make(RecipeCatalogue)
|
2021-07-28 20:10:13 +00:00
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
|
2021-07-28 20:10:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
if !recipeFSIsLatest {
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("reading recipe catalogue from web to get latest")
|
2021-09-04 22:14:27 +00:00
|
|
|
if err := readRecipeCatalogueWeb(&recipes); err != nil {
|
2021-07-28 20:10:13 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-09-04 22:14:27 +00:00
|
|
|
return recipes, nil
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
|
2021-09-04 22:14:27 +00:00
|
|
|
if err := readRecipeCatalogueFS(&recipes); err != nil {
|
2021-07-28 20:10:13 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
return recipes, nil
|
2021-07-28 20:10:13 +00:00
|
|
|
}
|
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// readRecipeCatalogueFS reads the catalogue from the file system.
|
2021-09-04 22:14:27 +00:00
|
|
|
func readRecipeCatalogueFS(target interface{}) error {
|
|
|
|
recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON)
|
2021-07-28 20:10:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
|
2021-07-28 20:10:13 +00:00
|
|
|
return err
|
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
|
|
|
logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
|
|
|
|
|
2021-07-28 20:10:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// readRecipeCatalogueWeb reads the catalogue from the web.
|
2021-09-04 22:14:27 +00:00
|
|
|
func readRecipeCatalogueWeb(target interface{}) error {
|
|
|
|
if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
|
2021-07-28 20:10:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
recipesJSON, err := json.MarshalIndent(target, "", " ")
|
2021-07-28 20:10:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
|
2021-07-28 20:10:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
|
|
|
|
|
2021-07-28 20:10:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-08-06 13:40:23 +00:00
|
|
|
|
2021-09-04 22:17:28 +00:00
|
|
|
// VersionsOfService lists the version of a service.
|
2021-08-06 13:40:23 +00:00
|
|
|
func VersionsOfService(recipe, serviceName string) ([]string, error) {
|
2021-09-04 22:14:27 +00:00
|
|
|
catalogue, err := ReadRecipeCatalogue()
|
2021-08-06 13:40:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-04 22:14:27 +00:00
|
|
|
rec, ok := catalogue[recipe]
|
2021-08-06 13:40:23 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
|
|
|
|
}
|
|
|
|
|
|
|
|
versions := []string{}
|
|
|
|
alreadySeen := make(map[string]bool)
|
2021-09-06 14:51:42 +00:00
|
|
|
for _, serviceVersion := range rec.Versions {
|
|
|
|
for tag := range serviceVersion {
|
|
|
|
if _, ok := alreadySeen[tag]; !ok {
|
|
|
|
alreadySeen[tag] = true
|
|
|
|
versions = append(versions, tag)
|
|
|
|
}
|
2021-08-06 13:40:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("detected versions '%s' for '%s'", strings.Join(versions, ", "), recipe)
|
|
|
|
|
2021-08-06 13:40:23 +00:00
|
|
|
return versions, nil
|
|
|
|
}
|
2021-09-08 10:55:33 +00:00
|
|
|
|
|
|
|
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
|
|
|
|
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
|
|
|
|
catl, err := ReadRecipeCatalogue()
|
|
|
|
if err != nil {
|
|
|
|
return RecipeMeta{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
recipeMeta, ok := catl[recipeName]
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("recipe '%s' does not exist?", recipeName)
|
|
|
|
return RecipeMeta{}, err
|
|
|
|
}
|
2021-09-10 22:54:02 +00:00
|
|
|
|
2021-09-08 10:55:33 +00:00
|
|
|
if err := recipe.EnsureExists(recipeName); err != nil {
|
|
|
|
return RecipeMeta{}, err
|
|
|
|
}
|
|
|
|
|
2021-09-10 22:54:02 +00:00
|
|
|
logrus.Debugf("recipe metadata retrieved for '%s'", recipeName)
|
|
|
|
|
2021-09-08 10:55:33 +00:00
|
|
|
return recipeMeta, nil
|
|
|
|
}
|
2021-09-20 07:38:51 +00:00
|
|
|
|
|
|
|
// RepoMeta is a single recipe repo metadata.
|
|
|
|
type RepoMeta struct {
|
|
|
|
ID int `json:"id"`
|
|
|
|
Owner Owner
|
|
|
|
Name string `json:"name"`
|
|
|
|
FullName string `json:"full_name"`
|
|
|
|
Description string `json:"description"`
|
|
|
|
Empty bool `json:"empty"`
|
|
|
|
Private bool `json:"private"`
|
|
|
|
Fork bool `json:"fork"`
|
|
|
|
Template bool `json:"template"`
|
|
|
|
Parent interface{} `json:"parent"`
|
|
|
|
Mirror bool `json:"mirror"`
|
|
|
|
Size int `json:"size"`
|
|
|
|
HTMLURL string `json:"html_url"`
|
|
|
|
SSHURL string `json:"ssh_url"`
|
|
|
|
CloneURL string `json:"clone_url"`
|
|
|
|
OriginalURL string `json:"original_url"`
|
|
|
|
Website string `json:"website"`
|
|
|
|
StarsCount int `json:"stars_count"`
|
|
|
|
ForksCount int `json:"forks_count"`
|
|
|
|
WatchersCount int `json:"watchers_count"`
|
|
|
|
OpenIssuesCount int `json:"open_issues_count"`
|
|
|
|
OpenPRCount int `json:"open_pr_counter"`
|
|
|
|
ReleaseCounter int `json:"release_counter"`
|
|
|
|
DefaultBranch string `json:"default_branch"`
|
|
|
|
Archived bool `json:"archived"`
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
|
Permissions Permissions
|
|
|
|
HasIssues bool `json:"has_issues"`
|
|
|
|
InternalTracker InternalTracker
|
|
|
|
HasWiki bool `json:"has_wiki"`
|
|
|
|
HasPullRequests bool `json:"has_pull_requests"`
|
|
|
|
HasProjects bool `json:"has_projects"`
|
|
|
|
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
|
|
|
|
AllowMergeCommits bool `json:"allow_merge_commits"`
|
|
|
|
AllowRebase bool `json:"allow_rebase"`
|
|
|
|
AllowRebaseExplicit bool `json:"allow_rebase_explicit"`
|
|
|
|
AllowSquashMerge bool `json:"allow_squash_merge"`
|
|
|
|
AvatarURL string `json:"avatar_url"`
|
|
|
|
Internal bool `json:"internal"`
|
|
|
|
MirrorInterval string `json:"mirror_interval"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Owner is the repo organisation owner metadata.
|
|
|
|
type Owner struct {
|
|
|
|
ID int `json:"id"`
|
|
|
|
Login string `json:"login"`
|
|
|
|
FullName string `json:"full_name"`
|
|
|
|
Email string `json:"email"`
|
|
|
|
AvatarURL string `json:"avatar_url"`
|
|
|
|
Language string `json:"language"`
|
|
|
|
IsAdmin bool `json:"is_admin"`
|
|
|
|
LastLogin string `json:"last_login"`
|
|
|
|
Created string `json:"created"`
|
|
|
|
Restricted bool `json:"restricted"`
|
|
|
|
Username string `json:"username"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Permissions is perms metadata for a repo.
|
|
|
|
type Permissions struct {
|
|
|
|
Admin bool `json:"admin"`
|
|
|
|
Push bool `json:"push"`
|
|
|
|
Pull bool `json:"pull"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// InternalTracker is issue tracker metadata for a repo.
|
|
|
|
type InternalTracker struct {
|
|
|
|
EnableTimeTracker bool `json:"enable_time_tracker"`
|
|
|
|
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
|
|
|
|
EnableIssuesDependencies bool `json:"enable_issue_dependencies"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// RepoCatalogue represents all the recipe repo metadata.
|
|
|
|
type RepoCatalogue map[string]RepoMeta
|
|
|
|
|
|
|
|
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
|
|
|
|
func ReadReposMetadata() (RepoCatalogue, error) {
|
|
|
|
reposMeta := make(RepoCatalogue)
|
|
|
|
|
|
|
|
pageIdx := 1
|
|
|
|
for {
|
|
|
|
var reposList []RepoMeta
|
|
|
|
|
|
|
|
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
|
|
|
|
|
|
|
|
logrus.Debugf("fetching repo metadata from '%s'", pagedURL)
|
|
|
|
|
|
|
|
if err := web.ReadJSON(pagedURL, &reposList); err != nil {
|
|
|
|
return reposMeta, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(reposList) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx, repo := range reposList {
|
|
|
|
reposMeta[repo.Name] = reposList[idx]
|
|
|
|
}
|
|
|
|
|
|
|
|
pageIdx++
|
|
|
|
}
|
|
|
|
|
|
|
|
return reposMeta, nil
|
|
|
|
}
|