package catalogue import ( "encoding/json" "fmt" "io/ioutil" "net/http" "os" "path" "strings" "time" "coopcloud.tech/abra/config" "coopcloud.tech/abra/web" "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"` } // Feature represents a JSON struct for a recipes features 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"` } // Tag represents a git tag type Tag = string type Service = string type ServiceMeta struct { Digest string `json:"digest"` Image string `json:"image"` Tag string `json:"tag"` } // App reprents an App in the abra catalogue 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"` } // EnsureExists checks the app has been cloned locally 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 } // EnsureVersion checks if an given version is used for the app 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 } // LatestVersion returns the latest version of the app 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 // Flatten converts AppCatalogue to slice 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: web.Timeout} 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 := web.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 } func VersionsOfService(recipe, serviceName string) ([]string, error) { catl, err := ReadAppsCatalogue() if err != nil { return nil, err } app, ok := catl[recipe] if !ok { return nil, fmt.Errorf("recipe '%s' does not exist?", recipe) } versions := []string{} alreadySeen := make(map[string]bool) for version := range app.Versions { appVersion := app.Versions[version][serviceName].Tag if _, ok := alreadySeen[appVersion]; !ok { alreadySeen[appVersion] = true versions = append(versions, appVersion) } } return versions, nil }