diff --git a/catalogue/catalogue.go b/catalogue/catalogue.go index 6b6fee991..8c8a8d14d 100644 --- a/catalogue/catalogue.go +++ b/catalogue/catalogue.go @@ -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 import ( @@ -16,63 +19,74 @@ import ( "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"` Rating string `json:"rating"` Source string `json:"source"` URL string `json:"url"` } -// Feature represents a JSON struct for a recipes features -type Feature struct { +// features represent what top-level features a recipe supports (e.g. does this +// recipe support backups?). +type features struct { Backups string `json:"backups"` Email string `json:"email"` Healthcheck string `json:"healthcheck"` - Image Image `json:"image"` + 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 { +// 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 { Digest string `json:"digest"` Image string `json:"image"` Tag string `json:"tag"` } -// App reprents an App in the abra catalogue -type App struct { +// recipe represents a recipe in the abra catalogue +type Recipe struct { Category string `json:"category"` DefaultBranch string `json:"default_branch"` Description string `json:"description"` - Features Feature `json:"features"` + Features features `json:"features"` Icon string `json:"icon"` Name string `json:"name"` Repository string `json:"repository"` - Versions map[Tag]map[Service]ServiceMeta `json:"versions"` + 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}) +// EnsureExists checks whether a recipe has been cloned locally or not. +func (r Recipe) EnsureExists() error { + recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(r.Name)) + + if _, err := os.Stat(recipeDir); os.IsNotExist(err) { + 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 { 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)) +// EnsureVersion checks whether a specific version exists for a recipe. +func (r Recipe) EnsureVersion(version string) error { + recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(r.Name)) - repo, err := git.PlainOpen(appDir) + repo, err := git.PlainOpen(recipeDir) if err != nil { return err } @@ -109,41 +123,42 @@ func (a App) EnsureVersion(version string) error { return nil } -// LatestVersion returns the latest version of the app -func (a App) LatestVersion() string { +// LatestVersion returns the latest version of a recipe. +func (r Recipe) 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 latestVersion = tag } return latestVersion } +// Name represents a recipe name. type Name = string -type AppsCatalogue map[Name]App + +// RecipeCatalogue represents the entire recipe catalogue. +type RecipeCatalogue map[Name]Recipe // 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]) +func (r RecipeCatalogue) Flatten() []Recipe { + recipes := make([]Recipe, 0, len(r)) + for name := range r { + recipes = append(recipes, r[name]) } - return apps + return recipes } -type ByAppName []App +type ByRecipeName []Recipe -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) +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) } -var appsCatalogueURL = "https://apps.coopcloud.tech" - -func appsCatalogueFSIsLatest() (bool, error) { +func recipeCatalogueFSIsLatest() (bool, error) { httpClient := &http.Client{Timeout: web.Timeout} - res, err := httpClient.Head(appsCatalogueURL) + res, err := httpClient.Head(RecipeCatalogueURL) if err != nil { return false, err } @@ -171,50 +186,50 @@ func appsCatalogueFSIsLatest() (bool, error) { return true, nil } -func ReadAppsCatalogue() (AppsCatalogue, error) { - apps := make(AppsCatalogue) +func ReadRecipeCatalogue() (RecipeCatalogue, error) { + recipes := make(RecipeCatalogue) - appsFSIsLatest, err := appsCatalogueFSIsLatest() + recipeFSIsLatest, err := recipeCatalogueFSIsLatest() if err != nil { return nil, err } - if !appsFSIsLatest { - if err := readAppsCatalogueWeb(&apps); err != nil { + if !recipeFSIsLatest { + if err := readRecipeCatalogueWeb(&recipes); err != nil { 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 apps, nil + return recipes, nil } -func readAppsCatalogueFS(target interface{}) error { - appsJSONFS, err := ioutil.ReadFile(config.APPS_JSON) +func readRecipeCatalogueFS(target interface{}) error { + recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON) if err != nil { return err } - if err := json.Unmarshal(appsJSONFS, &target); err != nil { + if err := json.Unmarshal(recipesJSONFS, &target); err != nil { return err } return nil } -func readAppsCatalogueWeb(target interface{}) error { - if err := web.ReadJSON(appsCatalogueURL, &target); err != nil { +func readRecipeCatalogueWeb(target interface{}) error { + if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil { return err } - appsJSON, err := json.MarshalIndent(target, "", " ") + recipesJSON, err := json.MarshalIndent(target, "", " ") if err != nil { 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 } @@ -222,20 +237,20 @@ func readAppsCatalogueWeb(target interface{}) error { } func VersionsOfService(recipe, serviceName string) ([]string, error) { - catl, err := ReadAppsCatalogue() + catalogue, err := ReadRecipeCatalogue() if err != nil { return nil, err } - app, ok := catl[recipe] + rec, ok := catalogue[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 + for version := range rec.Versions { + appVersion := rec.Versions[version][serviceName].Tag if _, ok := alreadySeen[appVersion]; !ok { alreadySeen[appVersion] = true versions = append(versions, appVersion) diff --git a/cli/app/new.go b/cli/app/new.go index ce91ba3da..e017d5b7d 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -45,14 +45,14 @@ var newAppNameFlag = &cli.StringFlag{ } 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. This command does not deploy your app for you. You will need to run "abra app deploy " to do so. -You can see what apps can be created (i.e. values for the argument) by -running "abra recipe ls". +You can see what recipes are available (i.e. values for the argument) +by running "abra recipe ls". Passing the "--secrets/-S" flag will automatically generate secrets for your app and store them encrypted at rest on the chosen target server. These @@ -75,27 +75,29 @@ var appNewCommand = &cli.Command{ internal.PassFlag, internal.SecretsFlag, }, - ArgsUsage: "", + ArgsUsage: "", Action: action, } -func appLookup(appType string) (catalogue.App, error) { - catl, err := catalogue.ReadAppsCatalogue() +// getRecipe retrieves a recipe from the recipe catalogue. +func getRecipe(recipeName string) (catalogue.Recipe, error) { + catl, err := catalogue.ReadRecipeCatalogue() if err != nil { - return catalogue.App{}, err + return catalogue.Recipe{}, err } - app, ok := catl[appType] + recipe, ok := catl[recipeName] 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 { - return catalogue.App{}, err + if err := recipe.EnsureExists(); err != nil { + 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 { if domain == "" { prompt := &survey.Input{ @@ -108,7 +110,7 @@ func ensureDomainFlag() error { 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 { appFiles, err := config.LoadAppFiles(newAppServer) if err != nil { @@ -127,7 +129,7 @@ func ensureServerFlag() error { 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 { if newAppName == "" { prompt := &survey.Input{ @@ -141,6 +143,7 @@ func ensureAppNameFlag() error { return nil } +// createSecrets creates all secrets for a new app. func createSecrets(sanitisedAppName string) (secrets, error) { appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) appEnv, err := config.ReadEnv(appEnvPath) @@ -165,23 +168,24 @@ func createSecrets(sanitisedAppName string) (secrets, error) { return secrets, nil } +// action is the main command-line action for this package func action(c *cli.Context) error { - appType := c.Args().First() - if appType == "" { - internal.ShowSubcommandHelpAndError(c, errors.New("no app type provided")) + recipeName := c.Args().First() + if recipeName == "" { + internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided")) } if err := config.EnsureAbraDirExists(); err != nil { logrus.Fatal(err) } - app, err := appLookup(appType) + recipe, err := getRecipe(recipeName) if err != nil { logrus.Fatal(err) } - latestVersion := app.LatestVersion() - if err := app.EnsureVersion(latestVersion); err != nil { + latestVersion := recipe.LatestVersion() + if err := recipe.EnsureVersion(latestVersion); err != nil { logrus.Fatal(err) } @@ -203,7 +207,7 @@ func action(c *cli.Context) error { 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) } @@ -218,15 +222,13 @@ func action(c *cli.Context) error { for secret := range secrets { secretTable.Append([]string{secret, secrets[secret]}) } - // Defer secret table first so it is last no matter what defer secretTable.Render() } tableCol := []string{"Name", "Domain", "Type", "Server"} table := abraFormatter.CreateTable(tableCol) - table.Append([]string{sanitisedAppName, domain, appType, newAppServer}) + table.Append([]string{sanitisedAppName, domain, recipeName, newAppServer}) defer table.Render() return nil - } diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index aed75c389..52fdd7178 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -30,17 +30,17 @@ var recipeListCommand = &cli.Command{ Usage: "List all available recipes", Aliases: []string{"ls"}, Action: func(c *cli.Context) error { - catl, err := catalogue.ReadAppsCatalogue() + catl, err := catalogue.ReadRecipeCatalogue() if err != nil { logrus.Fatal(err.Error()) } - apps := catl.Flatten() - sort.Sort(catalogue.ByAppName(apps)) + recipes := catl.Flatten() + sort.Sort(catalogue.ByRecipeName(recipes)) tableCol := []string{"Name", "Category", "Status"} table := formatter.CreateTable(tableCol) - for _, app := range apps { - status := fmt.Sprintf("%v", app.Features.Status) - tableRow := []string{app.Name, app.Category, status} + for _, recipe := range recipes { + status := fmt.Sprintf("%v", recipe.Features.Status) + tableRow := []string{recipe.Name, recipe.Category, status} table.Append(tableRow) } table.Render() @@ -58,18 +58,18 @@ var recipeVersionCommand = &cli.Command{ internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided")) } - catalogue, err := catalogue.ReadAppsCatalogue() + catalogue, err := catalogue.ReadRecipeCatalogue() if err != nil { logrus.Fatal(err) return nil } - if app, ok := catalogue[recipe]; ok { + if recipe, ok := catalogue[recipe]; ok { tableCol := []string{"Version", "Service", "Image", "Digest"} table := formatter.CreateTable(tableCol) - for version := range app.Versions { - for service := range app.Versions[version] { - meta := app.Versions[version][service] + for version := range recipe.Versions { + for service := range recipe.Versions[version] { + meta := recipe.Versions[version][service] table.Append([]string{version, service, meta.Image, meta.Digest}) } }