pkg/tftarget/tftarget.go (141 lines of code) (raw):
// Copyright 2019 Google 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 tftarget is a constraint framework target for config-validator to use for integrating with the opa constraint framework.
package tftarget
import (
"fmt"
"regexp"
"strings"
"github.com/open-policy-agent/frameworks/constraint/pkg/core/constraints"
"github.com/open-policy-agent/frameworks/constraint/pkg/handler"
"github.com/open-policy-agent/frameworks/constraint/pkg/types"
"github.com/pkg/errors"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// Name is the target name for TFTarget
const Name = "validation.resourcechange.terraform.cloud.google.com"
// TFTarget is the constraint framework target for config-validator
type TFTarget struct {
}
var _ handler.TargetHandler = &TFTarget{}
// New returns a new TFTarget
func New() *TFTarget {
return &TFTarget{}
}
// ToMatcher implements client.ToMatcher
func (g *TFTarget) ToMatcher(constraint *unstructured.Unstructured) (constraints.Matcher, error) {
match, ok, err := unstructured.NestedMap(constraint.Object, "spec", "match")
if err != nil {
return nil, fmt.Errorf("unable to get spec.match: %w", err)
}
if !ok {
return &matcher{addresses: []string{"**"}, excludedAddresses: []string{}}, nil
}
include, ok, err := unstructured.NestedStringSlice(match, "addresses")
if err != nil {
return nil, fmt.Errorf("unable to get string slice from spec.match.addresses: %w", err)
}
if !ok {
include = []string{"**"}
}
exclude, ok, err := unstructured.NestedStringSlice(match, "excludedAddresses")
if err != nil {
return nil, fmt.Errorf("unable to get string slice from spec.match.excludedAddresses: %w", err)
}
if !ok {
exclude = []string{}
}
return &matcher{
addresses: include,
excludedAddresses: exclude,
}, nil
}
// MatchSchema implements client.MatchSchemaProvider
func (g *TFTarget) MatchSchema() apiextensions.JSONSchemaProps {
return apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"addresses": {
Type: "array",
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Type: "string",
},
},
},
"excludedAddresses": {
Type: "array",
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Type: "string",
},
},
},
},
}
}
// GetName implements handler.TargetHandler
func (g *TFTarget) GetName() string {
return Name
}
// ProcessData implements handler.TargetHandler
func (g *TFTarget) ProcessData(obj interface{}) (bool, []string, interface{}, error) {
return false, nil, nil, errors.Errorf("storing data for referential constraint eval is not supported at this time.")
}
// HandleReview implements handler.TargetHandler
func (g *TFTarget) HandleReview(obj interface{}) (bool, interface{}, error) {
switch resource := obj.(type) {
case map[string]interface{}:
if _, found, err := unstructured.NestedString(resource, "name"); !found || err != nil {
return false, nil, err
}
if _, found, err := unstructured.NestedString(resource, "address"); !found || err != nil {
return false, nil, err
}
if _, found, err := unstructured.NestedMap(resource, "change"); !found || err != nil {
return false, nil, err
}
if _, found, err := unstructured.NestedString(resource, "type"); !found || err != nil {
return false, nil, err
}
return true, resource, nil
}
return false, nil, nil
}
// HandleViolation implements handler.TargetHandler
func (g *TFTarget) HandleViolation(result *types.Result) error {
return nil
}
var partRegex = regexp.MustCompile(`[\w.\-_\[\]\d]+`)
// checkPathGlob
func checkPathGlob(expression string) error {
// check for path components / numbers
parts := strings.Split(expression, ".")
for i := 0; i < len(parts); i++ {
item := parts[i]
switch {
case item == "*":
case item == "**":
case partRegex.MatchString(item):
default:
return errors.Errorf("unexpected item %s element %d in %s", item, i, expression)
}
}
return nil
}
func checkPathGlobs(rs []string) error {
for idx, r := range rs {
if err := checkPathGlob(r); err != nil {
return errors.Wrapf(err, "idx: %d", idx)
}
}
return nil
}
// ValidateConstraint implements handler.TargetHandler
func (g *TFTarget) ValidateConstraint(constraint *unstructured.Unstructured) error {
includes, found, err := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "addresses")
if err != nil {
return errors.Errorf("invalid spec.match.addresses: %s", err)
}
if found {
if err := checkPathGlobs(includes); err != nil {
return errors.Wrapf(err, "invalid glob in target")
}
}
excludes, found, err := unstructured.NestedStringSlice(constraint.Object, "spec", "match", "excludedAddresses")
if err != nil {
return errors.Errorf("invalid spec.match.excludedAddresses: %s", err)
}
if found {
if err := checkPathGlobs(excludes); err != nil {
return errors.Wrapf(err, "invalid glob in exclude")
}
}
return nil
}