Skip to content

Commit

Permalink
Added running haproxy binary in check mode, release 0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
abulimov committed Apr 8, 2016
1 parent 62ae163 commit 9251ebc
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 32 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Unreleased
## v0.3.0 [2016-04-08]

- Added deprecated rules check
- Fixed parser GetUsage for 'no option'
- Added ability to run HAProxy binary in check mode and return parsed warnings

## v0.2.1 [2016-04-07]

Expand Down
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ There is official [Atom](http://atom.io) plugin - [linter-haproxy](https://atom.
Grab latest release on [releases page](https://github.com/abulimov/haproxy-lint/releases),
or build from source.

**To get more warnings you need a local HAProxy executable.**
Install it with [Homebrew](http://brew.sh) on OS X or package manager of your choice on Linux.


### Building from source

You need working Go compiler.
Tested against Go 1.5+

On Linux/OSX:

```
```console
# set GOPATH to some valid path
export GOPATH=~/go && mkdir -p ~/go
go get github.com/abulimov/haproxy-lint
Expand All @@ -34,14 +38,17 @@ Now you have *haproxy-lint* binary.
You can specify switch to JSON output
with `--json`flag (useful for editor plugins integration).

Also you can manually disable running local HAProxy binary in check mode with
`--run-haproxy=false` flag.

```console
$ haproxy-lint /etc/haproxy/haproxy.cfg
haproxy-lint /etc/haproxy/haproxy.cfg
24:0:warning: ACL h_some declared but not used
18:0:warning: backend unused-servers declared but not used
25:0:critical: backend undefined-servers used but not declared


$ haproxy-lint --json /etc/haproxy/haproxy.cfg
haproxy-lint --json /etc/haproxy/haproxy.cfg
[
{
"col": 0,
Expand All @@ -64,18 +71,26 @@ $ haproxy-lint --json /etc/haproxy/haproxy.cfg
]
```

## Rules
## HAProxy check mode

In case if you have locally installed HAProxy,
it gets run with `-c` flag to check specified file,
and it's output gets parsed and returned as a linter warning.

If locally installed HAProxy is found, some of Native rules does not get
executed, as they just duplicate HAProxy's own checks.

| # | Severity | Rule |
|-----|----------|-----------------------------------------------|
| 001 | critical | backend used but not declared |
| 002 | warning | backend declared but not used |
| 003 | warning | acl declared but not used |
| 004 | critical | acl used but not declared |
| 005 | warning | rule order masking real evaluation precedence |
| 006 | warning | duplicate rules found |
| 007 | warning | deprecated keywords found |
## Native Rules

| # | Severity | Rule | Runs when local HAProxy found |
|-----|----------|-----------------------------------------------|-------------------------------|
| 001 | critical | backend used but not declared | yes |
| 002 | warning | backend declared but not used | yes |
| 003 | warning | acl declared but not used | yes |
| 004 | critical | acl used but not declared | no |
| 005 | warning | rule order masking real evaluation precedence | no |
| 006 | warning | duplicate rules found | yes |
| 007 | warning | deprecated keywords found | no |

## License

Expand Down
40 changes: 28 additions & 12 deletions checks/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,42 @@ package checks
import "github.com/abulimov/haproxy-lint/lib"

// Run runs all checks on Sections
func Run(sections []*lib.Section) []lib.Problem {
var sectionChecks = []lib.SectionCheck{
CheckUnusedACL,
CheckUnknownACLs,
CheckPrecedence,
CheckDuplicates,
CheckDeprecations,
func Run(sections []*lib.Section, extrasOnly bool) []lib.Problem {
type secCheck struct {
f lib.SectionCheck
// extra indicates that this check is not implemented in HAProxy binary
extra bool
}
var globalChecks = []lib.GlobalCheck{
CheckUnusedBackends,
CheckUnknownBackends,
type globCheck struct {
f lib.GlobalCheck
// extra indicates that this check is not implemented in HAProxy binary
extra bool
}
var sectionChecks = []secCheck{
{CheckUnusedACL, true},
{CheckUnknownACLs, false},
{CheckPrecedence, false},
{CheckDuplicates, true},
{CheckDeprecations, false},
}
var globalChecks = []globCheck{
{CheckUnusedBackends, true},
// while check for unknown backend is implemented in HAProxy,
// alert message for it doesn't show the line with backend usage
{CheckUnknownBackends, true},
}
var problems []lib.Problem
for _, s := range sections {
for _, r := range sectionChecks {
problems = append(problems, r(s)...)
if !extrasOnly || r.extra {
problems = append(problems, r.f(s)...)
}
}
}
for _, r := range globalChecks {
problems = append(problems, r(sections)...)
if !extrasOnly || r.extra {
problems = append(problems, r.f(sections)...)
}
}
return problems
}
29 changes: 23 additions & 6 deletions haproxy-lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ package main
import (
"flag"
"fmt"
"log"
"os"

"github.com/abulimov/haproxy-lint/checks"
"github.com/abulimov/haproxy-lint/lib"
)

var version = "0.2.1"
var version = "0.3.0"

func myUsage() {
fmt.Printf("Usage: %s [OPTIONS] haproxy.cfg\n", os.Args[0])
flag.PrintDefaults()
}

func main() {
argJSON := flag.Bool("json", false, "Output in json")
jsonFlag := flag.Bool("json", false, "Output in json")
haproxyFlag := flag.Bool("run-haproxy", true, "Try to run HAProxy binary in check mode")
versionFlag := flag.Bool("version", false, "print haproxy-lint version and exit")

flag.Usage = myUsage
Expand All @@ -34,18 +36,33 @@ func main() {
}
filePath := flag.Args()[0]

var problems []lib.Problem
useHAProxy := *haproxyFlag
if useHAProxy {
haproxyProblems, err := lib.RunHAProxyCheck(filePath)
if err != nil {
log.Println(err)
useHAProxy = false
} else {
problems = append(problems, haproxyProblems...)
}
}

config, err := lib.ReadConfigFile(filePath)
if err != nil {
fmt.Println(err)
os.Exit(1)
log.Fatal(err)
}

sections := lib.GetSections(config)

problems := checks.Run(sections)
// if we have local haproxy executable we shouldn't run
// checks that are implemented in haproxy itself.
nativeProblems := checks.Run(sections, useHAProxy)

problems = append(problems, nativeProblems...)

if len(problems) != 0 {
if *argJSON {
if *jsonFlag {
fmt.Println(lib.ReportProblemsJSON(problems))
} else {
fmt.Print(lib.ReportProblems(problems))
Expand Down
84 changes: 84 additions & 0 deletions lib/haproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package lib

import (
"bytes"
"io"
"io/ioutil"
"os/exec"
"regexp"
"strconv"
"strings"
)

// ParseHaproxyLine parses line of HAProxy config check output
func ParseHaproxyLine(line string) *Problem {
regexps := []*regexp.Regexp{
regexp.MustCompile(`\[(?P<severity>\w+)\]\s+\d+\/\d+\s+\(\d+\)\s+:\s+parsing\s\[.+:(?P<line>\d+)\]\s+:\s+(?P<message>.+)`),
regexp.MustCompile(`\[(?P<severity>\w+)\]\s+\d+\/\d+\s+\(\d+\)\s+:\s+(?P<message>.+)\s+at\s\[.+:(?P<line>\d+)\]\s+.+`),
}
stopWords := regexp.MustCompile(`unable to load SSL private key|file`)
if stopWords.MatchString(line) {
return nil
}
for _, re := range regexps {
matches := re.FindAllStringSubmatch(line, -1)
pos := map[string]int{}
for i, name := range re.SubexpNames() {
pos[name] = i
}
if len(matches) == 1 {
if len(matches[0]) == 4 {
lineNum, err := strconv.Atoi(matches[0][pos["line"]])
if err != nil {
return nil
}
severity := "critical"
if matches[0][pos["severity"]] == "WARNING" {
severity = "warning"
}
return &Problem{
Line: lineNum,
Col: 0,
Severity: severity,
Message: matches[0][pos["message"]],
}
}
}
}
return nil
}

// ParseHaproxyOutput parses whole HAProxy config check output
func ParseHaproxyOutput(f io.Reader) ([]Problem, error) {
b, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
var problems []Problem
lines := strings.Split(string(b), "\n")
for _, line := range lines {
p := ParseHaproxyLine(line)
if p != nil {
problems = append(problems, *p)
}
}
return problems, nil
}

// RunHAProxyCheck executes HAProxy binary in check mode and parses it's output
func RunHAProxyCheck(filePath string) ([]Problem, error) {
cmd := exec.Command("haproxy", "-c", "-f", filePath)
var out bytes.Buffer
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
// haproxy exits with exit code 1 on any problems with config,
// but if we don't have haproxy executable we won't get exit code.
// Here we check it.
if _, ok := err.(*exec.ExitError); !ok {
return nil, err
}

}
return ParseHaproxyOutput(&out)
}
71 changes: 71 additions & 0 deletions lib/haproxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package lib

import (
"os"
"reflect"
"testing"
)

func TestParseHaproxyLine(t *testing.T) {
tests := []struct {
// Test description.
name string
// Parameters.
line string
// Expected results.
want *Problem
}{
{
name: "Warning output",
line: "[WARNING] 098/114746 (5447) : parsing [/tmp/tmp.cfg:42] : a 'redirect' rule placed after a 'use_backend' rule will still be processed before.",
want: &Problem{Line: 42, Col: 0, Severity: "warning", Message: "a 'redirect' rule placed after a 'use_backend' rule will still be processed before."},
},
{
name: "Alert output",
line: "[ALERT] 098/114746 (5447) : parsing [/tmp/tmp.cfg:36] : unknown keyword 'deffault_backend' in 'frontend' section",
want: &Problem{Line: 36, Col: 0, Severity: "critical", Message: "unknown keyword 'deffault_backend' in 'frontend' section"},
},
{
name: "Summary output",
line: "[ALERT] 098/114746 (5447) : Fatal errors found in configuration.",
want: nil,
},
{
name: "Bad output",
line: " [ ALL] accept-proxy",
want: nil,
},
{
name: "SSL cert output",
line: "[ALERT] 098/131824 (13707) : parsing [/tmp/tmp.cfg:34] : 'bind :443' : unable to load SSL private key from PEM file '/cert/cert.pem'.",
want: nil,
},
{
name: "SSL cert problem",
line: "[ALERT] 098/131824 (13707) : Proxy 'secured': no SSL certificate specified for bind ':443' at [/tmp/tmp.cfg:34] (use 'crt').",
want: &Problem{Line: 34, Col: 0, Severity: "critical", Message: "Proxy 'secured': no SSL certificate specified for bind ':443'"},
},
}
for _, tt := range tests {
if got := ParseHaproxyLine(tt.line); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. ParseHaproxyLine() = %v, want %v", tt.name, got, tt.want)
}
}
}

func TestParseHaproxyOutput(t *testing.T) {
f, err := os.Open("../testdata/example_output.txt")
if err != nil {
t.Fatalf("Unexpected error while opening test output file: %v", err)
}
defer f.Close()
problems, err := ParseHaproxyOutput(f)
if err != nil {
t.Fatalf("Unexpected error while parsing test output: %v", err)
}

if len(problems) != 6 {
t.Errorf("Expected %d problems, got %d", 6, len(problems))
}

}
Loading

0 comments on commit 9251ebc

Please sign in to comment.