refactor: clear up app/recipe usage
continuous-integration/drone/push Build is passing Details

See coop-cloud/go-abra#36.
This commit is contained in:
decentral1se 2021-09-05 00:14:27 +02:00
parent 5e4114036b
commit ff21237a21
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
3 changed files with 112 additions and 95 deletions

View File

@ -1,3 +1,6 @@
// 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?).
package catalogue package catalogue
import ( import (
@ -16,63 +19,74 @@ import (
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
type Image struct { // RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
// image represents a recipe container image.
type image struct {
Image string `json:"image"` Image string `json:"image"`
Rating string `json:"rating"` Rating string `json:"rating"`
Source string `json:"source"` Source string `json:"source"`
URL string `json:"url"` URL string `json:"url"`
} }
// Feature represents a JSON struct for a recipes features // features represent what top-level features a recipe supports (e.g. does this
type Feature struct { // recipe support backups?).
type features struct {
Backups string `json:"backups"` Backups string `json:"backups"`
Email string `json:"email"` Email string `json:"email"`
Healthcheck string `json:"healthcheck"` Healthcheck string `json:"healthcheck"`
Image Image `json:"image"` Image image `json:"image"`
Status int `json:"status"` Status int `json:"status"`
Tests string `json:"tests"` Tests string `json:"tests"`
} }
// Tag represents a git tag // tag represents a git tag.
type Tag = string type tag = string
type Service = string
type ServiceMeta struct { // service represents a service within a recipe.
type service = string
// serviceMeta represents meta info associated with a service.
type serviceMeta struct {
Digest string `json:"digest"` Digest string `json:"digest"`
Image string `json:"image"` Image string `json:"image"`
Tag string `json:"tag"` Tag string `json:"tag"`
} }
// App reprents an App in the abra catalogue // recipe represents a recipe in the abra catalogue
type App struct { type Recipe struct {
Category string `json:"category"` Category string `json:"category"`
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
Description string `json:"description"` Description string `json:"description"`
Features Feature `json:"features"` Features features `json:"features"`
Icon string `json:"icon"` Icon string `json:"icon"`
Name string `json:"name"` Name string `json:"name"`
Repository string `json:"repository"` Repository string `json:"repository"`
Versions map[Tag]map[Service]ServiceMeta `json:"versions"` Versions map[tag]map[service]serviceMeta `json:"versions"`
Website string `json:"website"` Website string `json:"website"`
} }
// EnsureExists checks the app has been cloned locally // EnsureExists checks whether a recipe has been cloned locally or not.
func (a App) EnsureExists() error { func (r Recipe) EnsureExists() error {
appDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(a.Name)) recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(r.Name))
if _, err := os.Stat(appDir); os.IsNotExist(err) {
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, a.Name) if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
_, err := git.PlainClone(appDir, false, &git.CloneOptions{URL: url, Tags: git.AllTags}) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name)
_, err := git.PlainClone(recipeDir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
// EnsureVersion checks if an given version is used for the app // EnsureVersion checks whether a specific version exists for a recipe.
func (a App) EnsureVersion(version string) error { func (r Recipe) EnsureVersion(version string) error {
appDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(a.Name)) recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(r.Name))
repo, err := git.PlainOpen(appDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
return err return err
} }
@ -109,41 +123,42 @@ func (a App) EnsureVersion(version string) error {
return nil return nil
} }
// LatestVersion returns the latest version of the app // LatestVersion returns the latest version of a recipe.
func (a App) LatestVersion() string { func (r Recipe) LatestVersion() string {
var latestVersion string var latestVersion string
for tag := range a.Versions { for tag := range r.Versions {
// apps.json versions are sorted so the last key is latest // apps.json versions are sorted so the last key is latest
latestVersion = tag latestVersion = tag
} }
return latestVersion return latestVersion
} }
// Name represents a recipe name.
type Name = string type Name = string
type AppsCatalogue map[Name]App
// RecipeCatalogue represents the entire recipe catalogue.
type RecipeCatalogue map[Name]Recipe
// Flatten converts AppCatalogue to slice // Flatten converts AppCatalogue to slice
func (a AppsCatalogue) Flatten() []App { func (r RecipeCatalogue) Flatten() []Recipe {
apps := make([]App, 0, len(a)) recipes := make([]Recipe, 0, len(r))
for name := range a { for name := range r {
apps = append(apps, a[name]) recipes = append(recipes, r[name])
} }
return apps return recipes
} }
type ByAppName []App type ByRecipeName []Recipe
func (a ByAppName) Len() int { return len(a) } func (r ByRecipeName) Len() int { return len(r) }
func (a ByAppName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (r ByRecipeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (a ByAppName) Less(i, j int) bool { func (r ByRecipeName) Less(i, j int) bool {
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name) return strings.ToLower(r[i].Name) < strings.ToLower(r[j].Name)
} }
var appsCatalogueURL = "https://apps.coopcloud.tech" func recipeCatalogueFSIsLatest() (bool, error) {
func appsCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout} httpClient := &http.Client{Timeout: web.Timeout}
res, err := httpClient.Head(appsCatalogueURL) res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -171,50 +186,50 @@ func appsCatalogueFSIsLatest() (bool, error) {
return true, nil return true, nil
} }
func ReadAppsCatalogue() (AppsCatalogue, error) { func ReadRecipeCatalogue() (RecipeCatalogue, error) {
apps := make(AppsCatalogue) recipes := make(RecipeCatalogue)
appsFSIsLatest, err := appsCatalogueFSIsLatest() recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !appsFSIsLatest { if !recipeFSIsLatest {
if err := readAppsCatalogueWeb(&apps); err != nil { if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err return nil, err
} }
return apps, nil return recipes, nil
} }
if err := readAppsCatalogueFS(&apps); err != nil { if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err return nil, err
} }
return apps, nil return recipes, nil
} }
func readAppsCatalogueFS(target interface{}) error { func readRecipeCatalogueFS(target interface{}) error {
appsJSONFS, err := ioutil.ReadFile(config.APPS_JSON) recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON)
if err != nil { if err != nil {
return err return err
} }
if err := json.Unmarshal(appsJSONFS, &target); err != nil { if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
return err return err
} }
return nil return nil
} }
func readAppsCatalogueWeb(target interface{}) error { func readRecipeCatalogueWeb(target interface{}) error {
if err := web.ReadJSON(appsCatalogueURL, &target); err != nil { if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
return err return err
} }
appsJSON, err := json.MarshalIndent(target, "", " ") recipesJSON, err := json.MarshalIndent(target, "", " ")
if err != nil { if err != nil {
return err return err
} }
if err := ioutil.WriteFile(config.APPS_JSON, appsJSON, 0644); err != nil { if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
return err return err
} }
@ -222,20 +237,20 @@ func readAppsCatalogueWeb(target interface{}) error {
} }
func VersionsOfService(recipe, serviceName string) ([]string, error) { func VersionsOfService(recipe, serviceName string) ([]string, error) {
catl, err := ReadAppsCatalogue() catalogue, err := ReadRecipeCatalogue()
if err != nil { if err != nil {
return nil, err return nil, err
} }
app, ok := catl[recipe] rec, ok := catalogue[recipe]
if !ok { if !ok {
return nil, fmt.Errorf("recipe '%s' does not exist?", recipe) return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
} }
versions := []string{} versions := []string{}
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
for version := range app.Versions { for version := range rec.Versions {
appVersion := app.Versions[version][serviceName].Tag appVersion := rec.Versions[version][serviceName].Tag
if _, ok := alreadySeen[appVersion]; !ok { if _, ok := alreadySeen[appVersion]; !ok {
alreadySeen[appVersion] = true alreadySeen[appVersion] = true
versions = append(versions, appVersion) versions = append(versions, appVersion)

View File

@ -45,14 +45,14 @@ var newAppNameFlag = &cli.StringFlag{
} }
var appNewDescription = ` var appNewDescription = `
This command takes an app recipe and uses it to create a new app. This new app This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server. configuration is stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so. deploy <app>" to do so.
You can see what apps can be created (i.e. values for the <type> argument) by You can see what recipes are available (i.e. values for the <recipe> argument)
running "abra recipe ls". by running "abra recipe ls".
Passing the "--secrets/-S" flag will automatically generate secrets for your Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These app and store them encrypted at rest on the chosen target server. These
@ -75,27 +75,29 @@ var appNewCommand = &cli.Command{
internal.PassFlag, internal.PassFlag,
internal.SecretsFlag, internal.SecretsFlag,
}, },
ArgsUsage: "<type>", ArgsUsage: "<recipe>",
Action: action, Action: action,
} }
func appLookup(appType string) (catalogue.App, error) { // getRecipe retrieves a recipe from the recipe catalogue.
catl, err := catalogue.ReadAppsCatalogue() func getRecipe(recipeName string) (catalogue.Recipe, error) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil { if err != nil {
return catalogue.App{}, err return catalogue.Recipe{}, err
} }
app, ok := catl[appType] recipe, ok := catl[recipeName]
if !ok { if !ok {
return catalogue.App{}, fmt.Errorf("app type does not exist: %s", appType) return catalogue.Recipe{}, fmt.Errorf("recipe '%s' does not exist?", recipeName)
} }
if err := app.EnsureExists(); err != nil { if err := recipe.EnsureExists(); err != nil {
return catalogue.App{}, err return catalogue.Recipe{}, err
} }
return app, nil
return recipe, nil
} }
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag() error { func ensureDomainFlag() error {
if domain == "" { if domain == "" {
prompt := &survey.Input{ prompt := &survey.Input{
@ -108,7 +110,7 @@ func ensureDomainFlag() error {
return nil return nil
} }
// ensureServerFlag checks if the server flag was used. if not, asks the user for it // ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error { func ensureServerFlag() error {
appFiles, err := config.LoadAppFiles(newAppServer) appFiles, err := config.LoadAppFiles(newAppServer)
if err != nil { if err != nil {
@ -127,7 +129,7 @@ func ensureServerFlag() error {
return nil return nil
} }
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it // ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error { func ensureAppNameFlag() error {
if newAppName == "" { if newAppName == "" {
prompt := &survey.Input{ prompt := &survey.Input{
@ -141,6 +143,7 @@ func ensureAppNameFlag() error {
return nil return nil
} }
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (secrets, error) { func createSecrets(sanitisedAppName string) (secrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath) appEnv, err := config.ReadEnv(appEnvPath)
@ -165,23 +168,24 @@ func createSecrets(sanitisedAppName string) (secrets, error) {
return secrets, nil return secrets, nil
} }
// action is the main command-line action for this package
func action(c *cli.Context) error { func action(c *cli.Context) error {
appType := c.Args().First() recipeName := c.Args().First()
if appType == "" { if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app type provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
} }
if err := config.EnsureAbraDirExists(); err != nil { if err := config.EnsureAbraDirExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
app, err := appLookup(appType) recipe, err := getRecipe(recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
latestVersion := app.LatestVersion() latestVersion := recipe.LatestVersion()
if err := app.EnsureVersion(latestVersion); err != nil { if err := recipe.EnsureVersion(latestVersion); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -203,7 +207,7 @@ func action(c *cli.Context) error {
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName) logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
} }
if err := config.CopyAppEnvSample(appType, newAppName, newAppServer); err != nil { if err := config.CopyAppEnvSample(recipeName, newAppName, newAppServer); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -218,15 +222,13 @@ func action(c *cli.Context) error {
for secret := range secrets { for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]}) secretTable.Append([]string{secret, secrets[secret]})
} }
// Defer secret table first so it is last no matter what
defer secretTable.Render() defer secretTable.Render()
} }
tableCol := []string{"Name", "Domain", "Type", "Server"} tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, appType, newAppServer}) table.Append([]string{sanitisedAppName, domain, recipeName, newAppServer})
defer table.Render() defer table.Render()
return nil return nil
} }

View File

@ -30,17 +30,17 @@ var recipeListCommand = &cli.Command{
Usage: "List all available recipes", Usage: "List all available recipes",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
catl, err := catalogue.ReadAppsCatalogue() catl, err := catalogue.ReadRecipeCatalogue()
if err != nil { if err != nil {
logrus.Fatal(err.Error()) logrus.Fatal(err.Error())
} }
apps := catl.Flatten() recipes := catl.Flatten()
sort.Sort(catalogue.ByAppName(apps)) sort.Sort(catalogue.ByRecipeName(recipes))
tableCol := []string{"Name", "Category", "Status"} tableCol := []string{"Name", "Category", "Status"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
for _, app := range apps { for _, recipe := range recipes {
status := fmt.Sprintf("%v", app.Features.Status) status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{app.Name, app.Category, status} tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow) table.Append(tableRow)
} }
table.Render() table.Render()
@ -58,18 +58,18 @@ var recipeVersionCommand = &cli.Command{
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
} }
catalogue, err := catalogue.ReadAppsCatalogue() catalogue, err := catalogue.ReadRecipeCatalogue()
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
return nil return nil
} }
if app, ok := catalogue[recipe]; ok { if recipe, ok := catalogue[recipe]; ok {
tableCol := []string{"Version", "Service", "Image", "Digest"} tableCol := []string{"Version", "Service", "Image", "Digest"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
for version := range app.Versions { for version := range recipe.Versions {
for service := range app.Versions[version] { for service := range recipe.Versions[version] {
meta := app.Versions[version][service] meta := recipe.Versions[version][service]
table.Append([]string{version, service, meta.Image, meta.Digest}) table.Append([]string{version, service, meta.Image, meta.Digest})
} }
} }