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": { { Ref: "R001", Level: "warn", Description: "compose config has expected version", HowToResolve: "ensure 'version: \"3.8\"' in compose configs", Function: LintComposeVersion, }, { Ref: "R002", Level: "warn", Description: "healthcheck enabled for all services", HowToResolve: "wire up healthchecks", Function: LintHealthchecks, }, { Ref: "R003", Level: "warn", Description: "all images use a tag", HowToResolve: "use a tag for all images", Function: LintAllImagesTagged, }, { Ref: "R004", Level: "warn", Description: "no unstable tags", HowToResolve: "tag all images with stable tags", Function: LintNoUnstableTags, }, { Ref: "R005", Level: "warn", Description: "tags use semver-like format", HowToResolve: "use semver-like tags", Function: LintSemverLikeTags, }, { Ref: "R006", Level: "warn", Description: "has published catalogue version", HowToResolve: "publish a recipe version to the catalogue", Function: LintHasPublishedVersion, }, { Ref: "R007", Level: "warn", Description: "README.md metadata filled in", HowToResolve: "fill out all the metadata", Function: LintMetadataFilledIn, }, { Ref: "R013", Level: "warn", Description: "git.coopcloud.tech repo exists", HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...", Function: LintHasRecipeRepo, }, }, "error": { { Ref: "R008", Level: "error", Description: ".env.sample provided", HowToResolve: "create an example .env.sample", Function: LintEnvConfigPresent, }, { Ref: "R009", Level: "error", Description: "one service named 'app'", HowToResolve: "name a servce 'app'", Function: LintAppService, }, { Ref: "R010", Level: "error", Description: "traefik routing enabled", HowToResolve: "include \"traefik.enable=true\" deploy label", Function: LintTraefikEnabled, }, { Ref: "R011", Level: "error", Description: "all services have images", HowToResolve: "ensure \"image: ...\" set on all services", Function: LintImagePresent, }, { Ref: "R012", Level: "error", Description: "config version are vendored", HowToResolve: "vendor config versions in an abra.sh", Function: LintAbraShVendors, }, }, } 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 }