From 532bb8a336ea2a0cea4b79fd80c61e51312c505d Mon Sep 17 00:00:00 2001 From: decentral1se Date: Tue, 3 Aug 2021 19:25:32 +0200 Subject: [PATCH] WIP: recipe lint command --- cli/recipe/recipe.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + 2 files changed, 86 insertions(+) diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index c2b7bf34..4bf1d5a4 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -5,14 +5,19 @@ import ( "fmt" "os" "path" + "path/filepath" "sort" + "strconv" "text/template" "coopcloud.tech/abra/catalogue" "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" + loader "coopcloud.tech/abra/client/stack" "coopcloud.tech/abra/config" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/distribution/reference" "github.com/go-git/go-git/v5" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" @@ -144,6 +149,85 @@ var recipeCreateCommand = &cli.Command{ }, } +var recipeLintCommand = &cli.Command{ + Name: "lint", + Usage: "Recipe configuration linter", + ArgsUsage: "", + Action: func(c *cli.Context) error { + recipe := c.Args().First() + if recipe == "" { + internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided")) + } + + pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, recipe) + composeFiles, err := filepath.Glob(pattern) + if err != nil { + logrus.Fatal(err) + } + + opts := options.Deploy{Composefiles: composeFiles} + config, err := loader.LoadComposefile(opts) + if err != nil { + logrus.Fatal(err) + } + + expectedVersion := false + if config.Version == "3.8" { + expectedVersion = true + } + + serviceNamedApp := false + traefikEnabled := false + healthChecksForAllServices := true + allImagesTagged := true + noUnstableTags := true + for _, service := range config.Services { + if service.Name == "app" { + serviceNamedApp = true + } + + for label := range service.Deploy.Labels { + if label == "traefik.enable" { + if service.Deploy.Labels[label] == "true" { + traefikEnabled = true + } + } + } + + img, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + logrus.Fatal(err) + } + if reference.IsNameOnly(img) { + allImagesTagged = false + } + + if img.(reference.NamedTagged).Tag() == "latest" { + noUnstableTags = false + } + + if service.HealthCheck == nil { + healthChecksForAllServices = false + } + } + + // TODO + // check for .env.sample + + tableCol := []string{"Rule", "Satisfied"} + table := formatter.CreateTable(tableCol) + table.Append([]string{"Compose files have the expected version", strconv.FormatBool(expectedVersion)}) + table.Append([]string{"Recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)}) + table.Append([]string{"Traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)}) + table.Append([]string{"All services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)}) + table.Append([]string{"All images are using a tag", strconv.FormatBool(allImagesTagged)}) + table.Append([]string{"No usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)}) + table.Render() + + return nil + }, +} + // RecipeCommand defines the `abra recipe` command and ets subcommands var RecipeCommand = &cli.Command{ Name: "recipe", @@ -162,5 +246,6 @@ how reliable this app is to deploy and maintain in its current state. recipeListCommand, recipeVersionCommand, recipeCreateCommand, + recipeLintCommand, }, } diff --git a/go.mod b/go.mod index 5095d62f..08e957bd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 github.com/containerd/containerd v1.5.5 // indirect github.com/docker/cli v20.10.7+incompatible + github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v20.10.7+incompatible github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go-units v0.4.0