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

feat: stats in webhook, audit & gator #2686

Merged
merged 15 commits into from
May 9, 2023
35 changes: 10 additions & 25 deletions cmd/gator/expand/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"sort"

"github.com/open-policy-agent/gatekeeper/cmd/gator/util"
"github.com/open-policy-agent/gatekeeper/pkg/gator/expand"
"github.com/open-policy-agent/gatekeeper/pkg/gator/reader"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -68,15 +69,15 @@ func init() {
func run(cmd *cobra.Command, args []string) {
unstrucs, err := reader.ReadSources(flagFilenames, flagImages, flagTempDir)
if err != nil {
errFatalf("reading: %v\n", err)
util.ErrFatalf("reading: %v\n", err)
}
if len(unstrucs) == 0 {
errFatalf("no input data identified\n")
util.ErrFatalf("no input data identified\n")
}

resultants, err := expand.Expand(unstrucs)
if err != nil {
errFatalf("error expanding resources: %v", err)
util.ErrFatalf("error expanding resources: %v", err)
}
// Sort resultants for deterministic output
sortUnstructs(resultants)
Expand All @@ -86,7 +87,7 @@ func run(cmd *cobra.Command, args []string) {
fmt.Println(output)
} else {
fmt.Printf("Writing output to file: %s\n", flagOutput)
stringToFile(output, flagOutput)
util.WriteToFile(output, flagOutput)
}

os.Exit(0)
Expand All @@ -95,27 +96,27 @@ func run(cmd *cobra.Command, args []string) {
func resourcetoYAMLString(resource *unstructured.Unstructured) string {
jsonb, err := json.Marshal(resource)
if err != nil {
errFatalf("pre-marshaling results to json: %v", err)
util.ErrFatalf("pre-marshaling results to json: %v", err)
}

unmarshalled := map[string]interface{}{}
err = json.Unmarshal(jsonb, &unmarshalled)
if err != nil {
errFatalf("pre-unmarshaling results from json: %v", err)
util.ErrFatalf("pre-unmarshaling results from json: %v", err)
}

var b bytes.Buffer
yamlEncoder := yaml.NewEncoder(&b)
if err := yamlEncoder.Encode(unmarshalled); err != nil {
errFatalf("marshaling validation yaml results: %v", err)
util.ErrFatalf("marshaling validation yaml results: %v", err)
}
return b.String()
}

func resourceToJSONString(resource *unstructured.Unstructured) string {
b, err := json.MarshalIndent(resource, "", " ")
if err != nil {
errFatalf("marshaling validation json results: %v", err)
util.ErrFatalf("marshaling validation json results: %v", err)
}
return string(b)
}
Expand All @@ -128,7 +129,7 @@ func resourcesToString(resources []*unstructured.Unstructured, format string) st
case stringJSON:
conversionFunc = resourceToJSONString
default:
errFatalf("unrecognized value for %s flag: %s", flagNameFormat, format)
util.ErrFatalf("unrecognized value for %s flag: %s", flagNameFormat, format)
}

output := ""
Expand All @@ -141,17 +142,6 @@ func resourcesToString(resources []*unstructured.Unstructured, format string) st
return output
}

func stringToFile(s string, path string) {
file, err := os.Create(path)
if err != nil {
errFatalf("error creating file at path %s: %v", path, err)
}

if _, err = fmt.Fprint(file, s); err != nil {
errFatalf("error writing to file at path %s: %s", path, err)
}
}

func sortUnstructs(objs []*unstructured.Unstructured) {
sortKey := func(o *unstructured.Unstructured) string {
return o.GetName() + o.GetAPIVersion() + o.GetKind()
Expand All @@ -160,8 +150,3 @@ func sortUnstructs(objs []*unstructured.Unstructured) {
return sortKey(objs[i]) > sortKey(objs[j])
})
}

func errFatalf(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", a...)
os.Exit(1)
}
2 changes: 1 addition & 1 deletion cmd/gator/test/gatortest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func Test_formatOutput(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output := formatOutput(tc.inputFormat, tc.input)
output := formatOutput(tc.inputFormat, tc.input, nil)
if diff := cmp.Diff(tc.expectedOutput, output); diff != "" {
t.Fatal(diff)
}
Expand Down
96 changes: 68 additions & 28 deletions cmd/gator/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os"
"strings"

"github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation"
cmdutils "github.com/open-policy-agent/gatekeeper/cmd/gator/util"
"github.com/open-policy-agent/gatekeeper/pkg/gator/reader"
"github.com/open-policy-agent/gatekeeper/pkg/gator/test"
"github.com/open-policy-agent/gatekeeper/pkg/util"
Expand Down Expand Up @@ -47,6 +49,7 @@ var (
flagFilenames []string
flagOutput string
flagIncludeTrace bool
flagGatherStats bool
flagImages []string
flagTempDir string
)
Expand All @@ -60,32 +63,35 @@ const (
stringJSON = "json"
stringYAML = "yaml"
stringHumanFriendly = "default"

fourSpaceTab = " "
)

func init() {
Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.")
Cmd.Flags().StringVarP(&flagOutput, flagNameOutput, "o", "", fmt.Sprintf("Output format. One of: %s|%s.", stringJSON, stringYAML))
Cmd.Flags().BoolVarP(&flagIncludeTrace, "trace", "t", false, `include a trace for the underlying constraint framework evaluation`)
Cmd.Flags().BoolVarP(&flagIncludeTrace, "trace", "t", false, "include a trace for the underlying Constraint Framework evaluation.")
Copy link
Member

@sozercan sozercan May 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a doc that describes usage? can be a follow up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes for stats feature I was going to do a follow up docs PR.

Cmd.Flags().BoolVarP(&flagGatherStats, "stats", "", false, "include performance stats returned from the Constraint Framework.")
Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.")
Cmd.Flags().StringVarP(&flagTempDir, flagNameTempDir, "d", "", fmt.Sprintf("Specifies the temporary directory to download and unpack images to, if using the --%s flag. Optional.", flagNameImage))
}

func run(cmd *cobra.Command, args []string) {
unstrucs, err := reader.ReadSources(flagFilenames, flagImages, flagTempDir)
if err != nil {
errFatalf("reading: %v", err)
cmdutils.ErrFatalf("reading: %v", err)
}
if len(unstrucs) == 0 {
errFatalf("no input data identified")
cmdutils.ErrFatalf("no input data identified")
}

responses, err := test.Test(unstrucs, flagIncludeTrace)
responses, err := test.Test(unstrucs, test.Opts{IncludeTrace: flagIncludeTrace, GatherStats: flagGatherStats})
if err != nil {
errFatalf("auditing objects: %v\n", err)
cmdutils.ErrFatalf("auditing objects: %v", err)
}
results := responses.Results()

fmt.Print(formatOutput(flagOutput, results))
fmt.Print(formatOutput(flagOutput, results, responses.StatsEntries))

// Whether or not we return non-zero depends on whether we have a `deny`
// enforcementAction on one of the violated constraints
Expand All @@ -96,31 +102,70 @@ func run(cmd *cobra.Command, args []string) {
os.Exit(exitCode)
}

func formatOutput(flagOutput string, results []*test.GatorResult) string {
func formatOutput(flagOutput string, results []*test.GatorResult, stats []*instrumentation.StatsEntry) string {
switch strings.ToLower(flagOutput) {
case stringJSON:
b, err := json.MarshalIndent(results, "", " ")
if err != nil {
errFatalf("marshaling validation json results: %v", err)
var jsonB []byte
var err error

if stats != nil {
statsAndResults := map[string]interface{}{"results": results, "stats": stats}
jsonB, err = json.MarshalIndent(statsAndResults, "", fourSpaceTab)
if err != nil {
cmdutils.ErrFatalf("marshaling validation json results and stats: %v", err)
}
} else {
jsonB, err = json.MarshalIndent(results, "", fourSpaceTab)
if err != nil {
cmdutils.ErrFatalf("marshaling validation json results: %v", err)
}
}
return string(b)

return string(jsonB)
case stringYAML:
yamlResults := test.GetYamlFriendlyResults(results)
jsonb, err := json.Marshal(yamlResults)
if err != nil {
errFatalf("pre-marshaling results to json: %v", err)
}
var yamlb []byte

unmarshalled := []*test.YamlGatorResult{}
err = json.Unmarshal(jsonb, &unmarshalled)
if err != nil {
errFatalf("pre-unmarshaling results from json: %v", err)
}
if stats != nil {
statsAndResults := map[string]interface{}{"results": yamlResults, "stats": stats}
acpana marked this conversation as resolved.
Show resolved Hide resolved

statsJSONB, err := json.Marshal(statsAndResults)
if err != nil {
cmdutils.ErrFatalf("pre-marshaling stats to json: %v", err)
}

statsAndResultsUnmarshalled := struct {
Results []*test.YamlGatorResult
Stats []*instrumentation.StatsEntry
}{}

err = json.Unmarshal(statsJSONB, &statsAndResultsUnmarshalled)
if err != nil {
cmdutils.ErrFatalf("pre-unmarshaling stats from json: %v", err)
}

yamlb, err = yaml.Marshal(statsAndResultsUnmarshalled)
if err != nil {
cmdutils.ErrFatalf("marshaling validation yaml results and stats: %v", err)
}
} else {
jsonb, err := json.Marshal(yamlResults)
if err != nil {
cmdutils.ErrFatalf("pre-marshaling results to json: %v", err)
}

unmarshalled := []*test.YamlGatorResult{}
err = json.Unmarshal(jsonb, &unmarshalled)
if err != nil {
cmdutils.ErrFatalf("pre-unmarshaling results from json: %v", err)
}

yamlb, err := yaml.Marshal(unmarshalled)
if err != nil {
errFatalf("marshaling validation yaml results: %v", err)
yamlb, err = yaml.Marshal(unmarshalled)
if err != nil {
cmdutils.ErrFatalf("marshaling validation yaml results: %v", err)
}
}

return string(yamlb)
case stringHumanFriendly:
default:
Expand Down Expand Up @@ -149,8 +194,3 @@ func enforceableFailure(results []*test.GatorResult) bool {

return false
}

func errFatalf(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
os.Exit(1)
}
22 changes: 22 additions & 0 deletions cmd/gator/util/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package util

import (
"fmt"
"os"
)

func ErrFatalf(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", a...)
os.Exit(1)
}

func WriteToFile(s string, path string) {
file, err := os.Create(path)
if err != nil {
ErrFatalf("error creating file at path %s: %v", path, err)
}

if _, err = fmt.Fprint(file, s); err != nil {
ErrFatalf("error writing to file at path %s: %s", path, err)
}
}
20 changes: 20 additions & 0 deletions pkg/audit/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ var (
auditMatchKindOnly = flag.Bool("audit-match-kind-only", false, "only use kinds specified in all constraints for auditing cluster resources. if kind is not specified in any of the constraints, it will audit all resources (same as setting this flag to false)")
apiCacheDir = flag.String("api-cache-dir", defaultAPICacheDir, "The directory where audit from api server cache are stored, defaults to /tmp/audit")
emptyAuditResults []updateListEntry
logStatsAudit = flag.Bool("log-stats-audit", false, "(alpha) log stats metrics for the audit run")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a doc to describe usage? can be a follow-up

)

// Manager allows us to audit resources periodically.
Expand Down Expand Up @@ -491,6 +492,15 @@ func (am *Manager) auditFromCache(ctx context.Context) ([]Result, []error) {
continue
}

if *logStatsAudit {
logging.LogStatsEntries(
am.opa,
am.log,
resp.StatsEntries,
"audit from cache review request stats",
)
}

for _, r := range resp.Results() {
results = append(results, Result{
Result: r,
Expand Down Expand Up @@ -597,6 +607,16 @@ func (am *Manager) reviewObjects(ctx context.Context, kind string, folderCount i
}
expansion.OverrideEnforcementAction(resultant.EnforcementAction, resultantResp)
expansion.AggregateResponses(resultant.TemplateName, resp, resultantResp)
expansion.AggregateStats(resultant.TemplateName, resp, resultantResp)
}

if *logStatsAudit {
logging.LogStatsEntries(
am.opa,
am.log,
resp.StatsEntries,
"audit review request stats",
)
}

if len(resp.Results()) > 0 {
Expand Down
23 changes: 22 additions & 1 deletion pkg/expansion/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ package expansion
import (
"fmt"

"github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation"
"github.com/open-policy-agent/frameworks/constraint/pkg/types"
)

const childMsgPrefix = "[Implied by %s]"
const (
childMsgPrefix = "[Implied by %s]"

ChildStatLabel = "Implied by"
)

// AggregateResponses aggregates all responses from children into the parent.
// Child result messages will be prefixed with a string to indicate the msg
Expand All @@ -22,6 +27,22 @@ func AggregateResponses(templateName string, parent *types.Responses, child *typ
}
}

// AggregateStats aggregates all stats from the child Responses.StatsEntry
// into the parent Responses.StatsEntry. Child Stats will have a label to
// indicate that they come from an ExpansionTemplate usage.
func AggregateStats(templateName string, parent *types.Responses, child *types.Responses) {
childStatsEntries := child.StatsEntries

for _, se := range childStatsEntries {
se.Labels = append(se.Labels, []*instrumentation.Label{{
Name: ChildStatLabel,
Value: templateName,
}}...)
}

parent.StatsEntries = append(parent.StatsEntries, child.StatsEntries...)
}

func OverrideEnforcementAction(action string, resps *types.Responses) {
// If the enforcement action is empty, do not override
if action == "" {
Expand Down
Loading