-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add checks package for runtime condition verification
Summary: This feature will power verification of: * TTP Prerequisites (right tools being installed, running in correct environment, etc) * Verification that individual steps return the correct results I'm adding it as an isolated package for now, in a subsequent diff I will hook it into `step.go` like this: ``` // CommonStepFields contains the fields // common to every type of step (such as Name). // It centralizes validation to simplify the code type CommonStepFields struct { Name string `yaml:"name,omitempty"` Description string `yaml:"description,omitempty"` Checks []checks.Check `yaml:"checks,omitempty"` ``` Note that the initial diff only Reviewed By: cedowens Differential Revision: D51428503 fbshipit-source-id: 78b873cf65082328c97a2a4cc1474930415da18c
- Loading branch information
1 parent
d05be95
commit 4011cf1
Showing
6 changed files
with
246 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package checks | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/spf13/afero" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// CommonCheckFields are common fields across all check types | ||
type CommonCheckFields struct { | ||
Msg string `yaml:"msg"` | ||
} | ||
|
||
// Check is wrapper struct around a Condition. | ||
// This wrapping setup is used so that we only | ||
// need to implement the UnmarshalYAML method once (in Check) | ||
// instead of having to implement it in each individual condition type. | ||
// This is similar to what we do with ParseAction | ||
// for decoding the actions associated with steps. | ||
type Check struct { | ||
CommonCheckFields | ||
condition Condition | ||
} | ||
|
||
// Verify wraps the Verify method from the underlying condition | ||
func (c *Check) Verify(ctx VerificationContext) error { | ||
if ctx.FileSystem == nil { | ||
ctx.FileSystem = afero.NewOsFs() | ||
} | ||
return c.condition.Verify(ctx) | ||
} | ||
|
||
// UnmarshalYAML implements custom deserialization | ||
// process to ensure that the check is decoded | ||
// into the correct struct type | ||
func (c *Check) UnmarshalYAML(node *yaml.Node) error { | ||
|
||
// Decode all of the shared fields. | ||
// Use of this auxiliary type prevents infinite recursion | ||
var ccf CommonCheckFields | ||
err := node.Decode(&ccf) | ||
if err != nil { | ||
return err | ||
} | ||
c.CommonCheckFields = ccf | ||
|
||
if c.Msg == "" { | ||
return errors.New("no msg specified for check") | ||
} | ||
|
||
candidateTypeInstances := []Condition{ | ||
&PathExists{}, | ||
} | ||
for _, candidateTypeInstance := range candidateTypeInstances { | ||
err := node.Decode(candidateTypeInstance) | ||
if err == nil { | ||
if c.condition != nil { | ||
// Must catch conditions with ambiguous types, such as: | ||
// - path_exists: foo | ||
// command_succeeds: bar | ||
// | ||
// This is a problem because we can't tell into | ||
// which concrete type we should decode | ||
return fmt.Errorf("check %q has ambiguous type", c.Msg) | ||
} | ||
c.condition = candidateTypeInstance | ||
} | ||
} | ||
if c.condition == nil { | ||
return fmt.Errorf("condition with msg %q did not match any valid condition type", c.Msg) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package checks | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/facebookincubator/ttpforge/pkg/testutils" | ||
"github.com/stretchr/testify/require" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
func TestCheckVerify(t *testing.T) { | ||
|
||
testCases := []struct { | ||
name string | ||
contentStr string | ||
fsysContents map[string][]byte | ||
expectUnmarshalError bool | ||
expectVerifyError bool | ||
}{ | ||
{ | ||
name: "Check if Regular File Exists (Yes)", | ||
contentStr: `msg: File does not exist, | ||
path_exists: should-exist.txt`, | ||
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")}, | ||
}, | ||
{ | ||
name: "Check if Regular File Exists (No)", | ||
contentStr: `msg: File does not exist, | ||
path_exists: does-not-exist.txt`, | ||
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")}, | ||
expectVerifyError: true, | ||
}, | ||
{ | ||
name: "path_exists + Checksum Verification (Success)", | ||
contentStr: `msg: File does not exists or does not have expected content, | ||
path_exists: should-exist.txt | ||
checksum: | ||
sha256: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae`, | ||
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")}, | ||
expectUnmarshalError: false, | ||
expectVerifyError: false, | ||
}, | ||
{ | ||
name: "Check if Checksum is Correct (Yes)", | ||
contentStr: `msg: File does not exists or does not have expected content, | ||
path_exists: has-correct-hash.txt | ||
checksum: | ||
sha256: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae`, | ||
fsysContents: map[string][]byte{"has-correct-hash.txt": []byte("foo")}, | ||
}, | ||
{ | ||
name: "Check if Checksum is Correct (Yes)", | ||
contentStr: `msg: File does not exists or does not have expected content, | ||
path_exists: incorrect-hash.txt | ||
checksum: | ||
sha256: "absolutely wrong"`, | ||
fsysContents: map[string][]byte{"incorrect-hash.txt": []byte("foo")}, | ||
expectVerifyError: true, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
// prep filesystem | ||
fsys, err := testutils.MakeAferoTestFs(tc.fsysContents) | ||
require.NoError(t, err) | ||
|
||
// decode the check | ||
var check Check | ||
err = yaml.Unmarshal([]byte(tc.contentStr), &check) | ||
if tc.expectUnmarshalError { | ||
require.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
|
||
// run verification | ||
err = check.Verify(VerificationContext{FileSystem: fsys}) | ||
if tc.expectVerifyError { | ||
require.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
}) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package checks | ||
|
||
import ( | ||
"crypto/sha256" | ||
"fmt" | ||
) | ||
|
||
// Checksum is a struct that contains different types | ||
// of checksums against which a file can be verified. | ||
// Right now we just support SHA256 checksums, but in the future | ||
// others such as MD5 can be added if needed. | ||
type Checksum struct { | ||
SHA256 string `yaml:"sha256"` | ||
} | ||
|
||
// Verify computes the checksum of the contents | ||
// and compares it to the expected value | ||
func (c *Checksum) Verify(contents []byte) error { | ||
if c.SHA256 == "" { | ||
return fmt.Errorf("Checksum is empty") | ||
} | ||
rawResult := sha256.Sum256(contents) | ||
if fmt.Sprintf("%x", rawResult) != c.SHA256 { | ||
return fmt.Errorf("contents do not match checksum") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package checks | ||
|
||
// Condition is the common interface | ||
// implemented by all condition types | ||
type Condition interface { | ||
Verify(ctx VerificationContext) error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package checks | ||
|
||
import ( | ||
"github.com/spf13/afero" | ||
) | ||
|
||
// VerificationContext contains contextual | ||
// information required to verify conditions | ||
// of various types | ||
type VerificationContext struct { | ||
FileSystem afero.Fs | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package checks | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/spf13/afero" | ||
) | ||
|
||
// PathExists is a condition that verifies that a file exists at a given path | ||
// It can also verify the contents of the file against a checksum | ||
type PathExists struct { | ||
Path string `yaml:"path_exists"` | ||
Checksum *Checksum `yaml:"checksum"` | ||
} | ||
|
||
// Verify checks the condition and returns an error if it fails | ||
func (c *PathExists) Verify(ctx VerificationContext) error { | ||
fsys := ctx.FileSystem | ||
|
||
// basic existence check | ||
exists, err := afero.Exists(fsys, c.Path) | ||
if err != nil { | ||
return err | ||
} | ||
if !exists { | ||
return fmt.Errorf("file %q does not exist", c.Path) | ||
} | ||
|
||
// verify the checksum if provided | ||
if c.Checksum != nil { | ||
contentBytes, err := afero.ReadFile(fsys, c.Path) | ||
if err != nil { | ||
return err | ||
} | ||
return c.Checksum.Verify(contentBytes) | ||
} | ||
return nil | ||
} |