Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/health module #189

Merged
merged 22 commits into from
Aug 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
NAME = $(shell awk -F\" '/^const Name/ { print $$2 }' cmd/root.go)
VERSION = $(shell awk -F\" '/^const Version/ { print $$2 }' cmd/version.go)
TOLINT = $(shell find . -name '*.go' -exec dirname \{\} \; | grep -v vendor | grep -v -e '^\.$$' | uniq)
TOLINT = $(shell find . -type f \( -not -ipath './vendor*' -not -iname 'main.go' -iname '*.go' \) -exec dirname {} \; | sort -u)
TESTDIRS = $(shell find . -name '*_test.go' -exec dirname \{\} \; | grep -v vendor | uniq)
NONVENDOR = ${shell find . -name '*.go' | grep -v vendor}

BENCHDIRS= $(shell find . -name '*_test.go' | grep -v vendor | xargs grep '*testing.B' | cut -d: -f1 | xargs dirname | uniq)
BENCH = .

Expand All @@ -19,7 +18,11 @@ test: converge gotest samples/*.hcl samples/errors/*.hcl blackbox/*.sh
./converge fmt --check samples/*.hcl

gotest:
go test -v ${TESTDIRS}
go test ${TESTDIRS}

license-check:
@echo "=== Missing License Files ==="
@./check_license.sh

bench:
go test -run='^$$' -bench=${BENCH} -benchmem ${BENCHDIRS}
Expand Down Expand Up @@ -91,4 +94,4 @@ package: xcompile
echo $$f; \
done

.PHONY: test gotest vendor-update xcompile package samples/errors/*.hcl blackbox/*.sh lint bench
.PHONY: test gotest vendor-update xcompile package samples/errors/*.hcl blackbox/*.sh lint bench license-check
4 changes: 2 additions & 2 deletions apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func Apply(ctx context.Context, in *graph.Graph) (*graph.Graph, error) {

var newResult *Result

if result.Status.Changes() {
if result.Status.HasChanges() {
log.Printf("[DEBUG] applying %q\n", id)

err := result.Task.Apply()
Expand All @@ -77,7 +77,7 @@ func Apply(ctx context.Context, in *graph.Graph) (*graph.Graph, error) {
status, err = result.Task.Check()
if err != nil {
err = errors.Wrapf(err, "error checking %s", id)
} else if status.Changes() {
} else if status.HasChanges() {
err = fmt.Errorf("%s still needs to be changed after application", id)
}
}
Expand Down
3 changes: 3 additions & 0 deletions apply/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ func (r *Result) HasChanges() bool { return r.Ran }

// Error returns the error assigned to this Result, if any
func (r *Result) Error() error { return r.Err }

// GetStatus returns the current task status
func (r *Result) GetStatus() resource.TaskStatus { return r.Status }
44 changes: 44 additions & 0 deletions check_license.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash

# Copyright © 2016 Asteris, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

SRC_DIRS=$(find $(realpath .) -type d -maxdepth 1 -not -ipath '*/vendor' -not -ipath '*/.git' -not -ipath $(realpath .))
FILES=$(find ${SRC_DIRS} -type f -name '*.go')

LICENSE_HEADER=$(cat <<EOF
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
EOF
)

HEADER_LEN=$(echo "${LICENSE_HEADER}" | wc -l)

for i in ${FILES}; do
diff <(head -n ${HEADER_LEN} $i) <(echo "${LICENSE_HEADER}") > /dev/null
if [[ $? -ne 0 ]]; then
echo $i
fi
done
103 changes: 103 additions & 0 deletions cmd/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"errors"
"fmt"
"log"
"os"

"github.com/asteris-llc/converge/graph"
"github.com/asteris-llc/converge/healthcheck"
"github.com/asteris-llc/converge/load"
"github.com/asteris-llc/converge/plan"
"github.com/asteris-llc/converge/render"
"github.com/spf13/cobra"
)

// healthcheckCmd represents the 'healthcheck' command
var healthcheckCmd = &cobra.Command{
Use: "healthcheck",
Short: "display a system health check",
Long: `Health checks determine the health status of your system. Health
checks are similar to 'plan' but will not calculate potential deltas, and will
not display healthy checks.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("Need at least one module filename as argument, got 0")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
// set up execution context
ctx, cancel := context.WithCancel(context.Background())
GracefulExit(cancel)

// params
params, err := getParamsFromFlags(cmd.Flags())
if err != nil {
log.Fatalf("[FATAL] could not read params: %s\n", err)
}

for _, fname := range args {
log.Printf("[INFO] planning %s\n", fname)

loaded, err := load.Load(ctx, fname)
if err != nil {
log.Fatalf("[FATAL] %s: could not parse file: %s\n", fname, err)
}

rendered, err := render.Render(ctx, loaded, params)
if err != nil {
log.Fatalf("[FATAL] %s: could not render: %s\n", fname, err)
}

merged, err := graph.MergeDuplicates(ctx, rendered, graph.SkipModuleAndParams)
if err != nil {
log.Fatalf("[FATAL] %s: could not merge duplicates: %s\n", fname, err)
}

planned, err := plan.Plan(ctx, merged)
if err != nil && err != plan.ErrTreeContainsErrors {
log.Fatalf("[FATAL] %s: planning failed: %s\n", fname, err)
}

results, err := healthcheck.CheckGraph(ctx, planned)
if err != nil {
log.Fatalf("[FATAL] %s: checking failed: %s\n", fname, err)
}

out, perr := healthPrinter().Show(ctx, results)
if perr != nil {
log.Fatalf("[FATAL] %s: failed printing results: %s\n", fname, err)
}

fmt.Print("\n")
fmt.Print(out)
if err != nil {
os.Exit(1)
}
}
},
}

func init() {
healthcheckCmd.Flags().Bool("quiet", false, "show only a short summary of the status")
addParamsArguments(healthcheckCmd.PersistentFlags())
viperBindPFlags(healthcheckCmd.Flags())
RootCmd.AddCommand(healthcheckCmd)
}
21 changes: 18 additions & 3 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import (
"runtime"

"github.com/asteris-llc/converge/prettyprinters"
"github.com/asteris-llc/converge/prettyprinters/health"
"github.com/asteris-llc/converge/prettyprinters/human"
"github.com/asteris-llc/converge/render"
"github.com/asteris-llc/converge/resource"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand All @@ -35,8 +37,7 @@ func viperBindPFlags(flags *pflag.FlagSet) {
}
}

func getPrinter() prettyprinters.Printer {
filter := human.ShowEverything
func humanProvider(filter human.FilterFunc) *human.Printer {
if !viper.GetBool("show-meta") {
filter = human.HideByKind("module", "param", "root")
}
Expand All @@ -47,8 +48,22 @@ func getPrinter() prettyprinters.Printer {
printer := human.NewFiltered(filter)
printer.Color = UseColor()
printer.InitColors()
return printer
}

func getPrinter() prettyprinters.Printer {
return prettyprinters.New(humanProvider(human.ShowEverything))
}

return prettyprinters.New(printer)
func healthPrinter() prettyprinters.Printer {
showHealthNodes := func(id string, value human.Printable) bool {
_, ok := value.(*resource.HealthStatus)
return ok
}
provider := humanProvider(showHealthNodes)
health := health.NewWithPrinter(provider)
health.Summary = viper.GetBool("quiet")
return prettyprinters.New(health)
}

// UseColor tells us whether or not to print colors using ANSI escape sequences
Expand Down
21 changes: 21 additions & 0 deletions graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ func (g *Graph) Descendents(id string) (out []string) {
return out
}

// Dependencies gets a list of all dependencies without relying on the ID
// functions and will work for dependencies that have been added during
// load.ResolveDependencies
func (g *Graph) Dependencies(id string) []string {
var uniq []string
for key := range g.dependencies(id, make(map[string]struct{})) {
uniq = append(uniq, key)
}
return uniq
}

// internal version of dependencies with a carry map
func (g *Graph) dependencies(id string, carry map[string]struct{}) map[string]struct{} {
for _, edge := range g.DownEdges(id) {
elem := edge.Target().(string)
carry[elem] = struct{}{}
carry = g.dependencies(elem, carry)
}
return carry
}

// Walk the graph leaf-to-root
func (g *Graph) Walk(ctx context.Context, cb WalkFunc) error {
return dependencyWalk(ctx, g, cb)
Expand Down
87 changes: 87 additions & 0 deletions healthcheck/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package healthcheck

import (
"context"
"errors"

"github.com/asteris-llc/converge/graph"
"github.com/asteris-llc/converge/resource"
)

// Check defines the interface for a health check
type Check interface {
FailingDep(string, resource.TaskStatus)
HealthCheck() (*resource.HealthStatus, error)
}

// CheckGraph walks a graph and runs health checks on each health-checkable node
func CheckGraph(ctx context.Context, in *graph.Graph) (*graph.Graph, error) {
return in.Transform(ctx, func(id string, out *graph.Graph) error {
task, err := unboxNode(out.Get(id))
if err != nil {
return err
}
asCheck, ok := task.(Check)
if !ok {
return nil
}
for _, dep := range out.Dependencies(id) {
depStatus, ok := out.Get(dep).(resource.TaskStatus)
if !ok {
continue
}

if isFailure, failErr := isFailingStatus(depStatus); failErr != nil {
return failErr
} else if isFailure {
asCheck.FailingDep(dep, depStatus)
}
}
status, err := asCheck.HealthCheck()
if err != nil {
return err
}
out.Add(id, status)
return nil
})
}

// unboxNode will remove a resource.TaskStatus from a plan.Result or apply.Result
func unboxNode(i interface{}) (resource.TaskStatus, error) {
type statusWrapper interface {
GetStatus() resource.TaskStatus
}
switch result := i.(type) {
case statusWrapper:
return result.GetStatus(), nil
case resource.TaskStatus:
return result, nil
default:
return nil, errors.New("cannot get task status from node")
}
}

func isFailingStatus(stat resource.TaskStatus) (bool, error) {
if check, ok := stat.(Check); ok {
checkStatus, err := check.HealthCheck()
if err != nil {
return true, err
}
return checkStatus.ShouldDisplay(), nil
}
return stat.HasChanges(), nil
}
2 changes: 1 addition & 1 deletion load/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func SetResources(ctx context.Context, g *graph.Graph) (*graph.Graph, error) {
case "module":
dest = new(module.Preparer)

case "task":
case "task", "healthcheck.task":
dest = new(shell.Preparer)

case "file.content":
Expand Down
2 changes: 1 addition & 1 deletion plan/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestPlanNoOp(t *testing.T) {

result := getResult(t, planned, "root")
assert.Equal(t, task.Status, result.Status.Messages()[0])
assert.Equal(t, task.WillChange, result.Status.Changes())
assert.Equal(t, task.WillChange, result.Status.HasChanges())
assert.Equal(t, task, result.Task)
}

Expand Down
5 changes: 4 additions & 1 deletion plan/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ func (r *Result) Messages() []string { return r.Status.Messages() }
func (r *Result) Changes() map[string]resource.Diff { return r.Status.Diffs() }

// HasChanges indicates if this result will change
func (r *Result) HasChanges() bool { return r.Status.Changes() }
func (r *Result) HasChanges() bool { return r.Status.HasChanges() }

// Error returns the error assigned to this Result, if any
func (r *Result) Error() error { return r.Err }

// GetStatus returns the current task status
func (r *Result) GetStatus() resource.TaskStatus { return r.Status }
Loading