abra/cli/recipe.go

294 lines
6.3 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"sort"
"text/template"
"time"
"coopcloud.tech/abra/config"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
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 Version 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[string]map[string]Version `json:"versions"`
Website string `json:"website"`
}
type Apps map[string]App
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
}
defer res.Body.Close()
return json.NewDecoder(res.Body).Decode(target)
}
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 ReadApps() (Apps, error) {
apps := make(Apps)
appsFSIsLatest, err := AppsFSIsLatest()
if err != nil {
return nil, err
}
if !appsFSIsLatest {
if err := ReadAppsWeb(&apps); err != nil {
return nil, err
}
return apps, nil
}
if err := ReadAppsFS(&apps); err != nil {
return nil, err
}
return apps, 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(target interface{}) error {
if err := readJson(AppsUrl, &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 SortByAppName(apps Apps) []string {
var names []string
for name := range apps {
names = append(names, name)
}
sort.Strings(names)
return names
}
var recipeListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
apps, err := ReadApps()
if err != nil {
logrus.Fatal(err.Error())
}
tableCol := []string{"Name", "Category", "Status"}
table := createTable(tableCol)
for _, name := range SortByAppName(apps) {
app := apps[name]
status := fmt.Sprintf("%v", app.Features.Status)
tableRow := []string{app.Name, app.Category, status}
table.Append(tableRow)
}
table.Render()
return nil
},
}
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List available versions for <recipe>",
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := c.Args().First()
if recipe == "" {
cli.ShowSubcommandHelp(c)
return nil
}
apps, err := ReadApps()
if err != nil {
logrus.Fatal(err)
return nil
}
if app, ok := apps[recipe]; ok {
tableCol := []string{"Version", "Service", "Image", "Digest"}
table := createTable(tableCol)
for version := range app.Versions {
for service := range app.Versions[version] {
meta := app.Versions[version][service]
table.Append([]string{version, service, meta.Image, meta.Digest})
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
}
logrus.Fatalf("'%s' recipe doesn't exist?", recipe)
return nil
},
}
var recipeCreateCommand = &cli.Command{
Name: "create",
Usage: "Create a new recipe",
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := c.Args().First()
if recipe == "" {
cli.ShowSubcommandHelp(c)
return nil
}
directory := path.Join(config.APPS_DIR, recipe)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
_, err := git.PlainClone(directory, false, &git.CloneOptions{URL: url})
if err != nil {
logrus.Fatal(err)
return nil
}
gitRepo := path.Join(config.APPS_DIR, recipe, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
}
toParse := []string{
path.Join(config.APPS_DIR, recipe, "README.md"),
path.Join(config.APPS_DIR, recipe, ".env.sample"),
path.Join(config.APPS_DIR, recipe, ".drone.yml"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe, "TODO"}); err != nil {
logrus.Fatal(err)
return nil
}
}
fmt.Printf(
"New recipe '%s' created in %s, happy hacking!",
recipe, path.Join(config.APPS_DIR, recipe),
)
return nil
},
}
var RecipeCommand = &cli.Command{
Name: "recipe",
HideHelp: true,
Subcommands: []*cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeCreateCommand,
},
}