Catalogue package had to be merged into the recipe package due to too many circular import errors. Also, use https url for cloning, assume folks don't have ssh setup by default (the whole reason for the refactor).
339 lines
7.5 KiB
Go
339 lines
7.5 KiB
Go
package lint
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/recipe"
|
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
"coopcloud.tech/tagcmp"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var Warn = "warn"
|
|
var Critical = "critical"
|
|
|
|
type LintFunction func(recipe.Recipe) (bool, error)
|
|
|
|
type LintRule struct {
|
|
Ref string
|
|
Level string
|
|
Description string
|
|
HowToResolve string
|
|
Function LintFunction
|
|
}
|
|
|
|
var LintRules = map[string][]LintRule{
|
|
"warn": []LintRule{
|
|
LintRule{
|
|
Ref: "R001",
|
|
Level: "warn",
|
|
Description: "compose config has expected version",
|
|
HowToResolve: "ensure 'version: \"3.8\"' in compose configs",
|
|
Function: LintComposeVersion,
|
|
},
|
|
LintRule{
|
|
Ref: "R002",
|
|
Level: "warn",
|
|
Description: "healthcheck enabled for all services",
|
|
HowToResolve: "wire up healthchecks",
|
|
Function: LintHealthchecks,
|
|
},
|
|
LintRule{
|
|
Ref: "R003",
|
|
Level: "warn",
|
|
Description: "all images use a tag",
|
|
HowToResolve: "use a tag for all images",
|
|
Function: LintAllImagesTagged,
|
|
},
|
|
LintRule{
|
|
Ref: "R004",
|
|
Level: "warn",
|
|
Description: "no unstable tags",
|
|
HowToResolve: "tag all images with stable tags",
|
|
Function: LintNoUnstableTags,
|
|
},
|
|
LintRule{
|
|
Ref: "R005",
|
|
Level: "warn",
|
|
Description: "tags use semver-like format",
|
|
HowToResolve: "use semver-like tags",
|
|
Function: LintSemverLikeTags,
|
|
},
|
|
LintRule{
|
|
Ref: "R006",
|
|
Level: "warn",
|
|
Description: "has published catalogue version",
|
|
HowToResolve: "publish a recipe version to the catalogue",
|
|
Function: LintHasPublishedVersion,
|
|
},
|
|
LintRule{
|
|
Ref: "R007",
|
|
Level: "warn",
|
|
Description: "README.md metadata filled in",
|
|
HowToResolve: "fill out all the metadata",
|
|
Function: LintMetadataFilledIn,
|
|
},
|
|
},
|
|
"error": []LintRule{
|
|
LintRule{
|
|
Ref: "R008",
|
|
Level: "error",
|
|
Description: ".env.sample provided",
|
|
HowToResolve: "create an example .env.sample",
|
|
Function: LintEnvConfigPresent,
|
|
},
|
|
LintRule{
|
|
Ref: "R009",
|
|
Level: "error",
|
|
Description: "one service named 'app'",
|
|
HowToResolve: "name a servce 'app'",
|
|
Function: LintAppService,
|
|
},
|
|
LintRule{
|
|
Ref: "R010",
|
|
Level: "error",
|
|
Description: "traefik routing enabled",
|
|
HowToResolve: "include \"traefik.enable=true\" deploy label",
|
|
Function: LintTraefikEnabled,
|
|
},
|
|
LintRule{
|
|
Ref: "R011",
|
|
Level: "error",
|
|
Description: "all services have images",
|
|
HowToResolve: "ensure \"image: ...\" set on all services",
|
|
Function: LintImagePresent,
|
|
},
|
|
LintRule{
|
|
Ref: "R012",
|
|
Level: "error",
|
|
Description: "config version are vendored",
|
|
HowToResolve: "vendor config versions in an abra.sh",
|
|
Function: LintAbraShVendors,
|
|
},
|
|
LintRule{
|
|
Ref: "R013",
|
|
Level: "error",
|
|
Description: "git.coopcloud.tech repo exists",
|
|
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
|
|
Function: LintHasRecipeRepo,
|
|
},
|
|
},
|
|
}
|
|
|
|
func LintForErrors(recipe recipe.Recipe) error {
|
|
logrus.Debugf("linting for critical errors in %s configs", recipe.Name)
|
|
|
|
for level := range LintRules {
|
|
if level != "error" {
|
|
continue
|
|
}
|
|
for _, rule := range LintRules[level] {
|
|
ok, err := rule.Function(recipe)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref)
|
|
}
|
|
}
|
|
}
|
|
|
|
logrus.Debugf("linting successful, %s is well configured", recipe.Name)
|
|
|
|
return nil
|
|
}
|
|
|
|
func LintComposeVersion(recipe recipe.Recipe) (bool, error) {
|
|
if recipe.Config.Version == "3.8" {
|
|
return true, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) {
|
|
envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name)
|
|
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func LintAppService(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
if service.Name == "app" {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
for label := range service.Deploy.Labels {
|
|
if label == "traefik.enable" {
|
|
if service.Deploy.Labels[label] == "true" {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
if service.HealthCheck == nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if reference.IsNameOnly(img) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var tag string
|
|
switch img.(type) {
|
|
case reference.NamedTagged:
|
|
tag = img.(reference.NamedTagged).Tag()
|
|
case reference.Named:
|
|
return false, nil
|
|
}
|
|
|
|
if tag == "latest" {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var tag string
|
|
switch img.(type) {
|
|
case reference.NamedTagged:
|
|
tag = img.(reference.NamedTagged).Tag()
|
|
case reference.Named:
|
|
return false, nil
|
|
}
|
|
|
|
if !tagcmp.IsParsable(tag) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
if service.Image == "" {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
|
|
catl, err := recipePkg.ReadRecipeCatalogue()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if len(versions) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
|
|
features, category, err := recipe.GetRecipeFeaturesAndCategory(r.Name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if category == "" {
|
|
return false, nil
|
|
}
|
|
|
|
if features.Backups == "" ||
|
|
features.Email == "" ||
|
|
features.Healthcheck == "" ||
|
|
features.Image.Image == "" ||
|
|
features.SSO == "" {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
|
|
for _, service := range recipe.Config.Services {
|
|
if len(service.Configs) > 0 {
|
|
abraSh := path.Join(config.RECIPES_DIR, recipe.Name, "abra.sh")
|
|
if _, err := os.Stat(abraSh); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, err
|
|
}
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
|
|
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe.Name)
|
|
|
|
res, err := http.Get(url)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|