pkg/gcv/oldconfigs/config.go (175 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 oldconfigs
import (
"encoding/json"
"fmt"
"github.com/GoogleCloudPlatform/config-validator/pkg/api/validator"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/smallfish/simpleyaml"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
)
type yamlFile struct {
source string // helpful information to rediscover this data
yaml *simpleyaml.Yaml
fileContents []byte
}
const (
validTemplateGroup = "templates.gatekeeper.sh/v1alpha1"
validConstraintGroup = "constraints.gatekeeper.sh/v1alpha1"
expectedTarget = "validation.gcp.forsetisecurity.org"
)
// UnclassifiedConfig stores loosely parsed information not specific to constraints or templates.
type UnclassifiedConfig struct {
Group string
MetadataName string
Kind string
Yaml *simpleyaml.Yaml
// keep the file path to help debug logging
FilePath string
// Preserve the raw user data to forward into rego
// This prevents any data loss issues from going though parsing libraries.
RawFile string
}
// ConstraintTemplate stores parsed information including the raw data.
type ConstraintTemplate struct {
Confg *UnclassifiedConfig
// This is the kind that this template generates.
GeneratedKind string
Rego string
}
// Constraint stores parsed information including the raw data.
type Constraint struct {
Confg *UnclassifiedConfig
}
// AsInterface returns the the config data as a structured golang object. This uses yaml.Unmarshal to create this object.
func (c *UnclassifiedConfig) AsInterface() (interface{}, error) {
// Use yaml.Unmarshal to create a proper golang object that maintains the same structure
var f interface{}
if err := yaml.Unmarshal([]byte(c.RawFile), &f); err != nil {
return nil, errors.Wrap(err, "converting from yaml")
}
return f, nil
}
// asConstraint attempts to convert to constraint
// Returns:
//
// *Constraint: only set if valid constraint
// bool: (always set) if this is a constraint
func asConstraint(data *UnclassifiedConfig) (*Constraint, error) {
// There is no validation matching this constraint to the template here that happens after
// basic parsing has happened when we have more context.
if data.Group != validConstraintGroup {
return nil, fmt.Errorf("group expected to be %s not %s", validConstraintGroup, data.Group)
}
if data.Kind == "ConstraintTemplate" {
return nil, fmt.Errorf("kind should not be ConstraintTemplate")
}
return &Constraint{
Confg: data,
}, nil
}
// AsProto returns the constraint a Kubernetes proto
func (c *Constraint) AsProto() (*validator.Constraint, error) {
ci, err := c.Confg.AsInterface()
if err != nil {
return nil, errors.Wrap(err, "converting to proto")
}
cp := &validator.Constraint{}
ciMap := ci.(map[string]interface{})
cp.ApiVersion = fmt.Sprintf("%s", ciMap["apiVersion"])
cp.Kind = fmt.Sprintf("%s", ciMap["kind"])
metadata, err := convertToProtoVal(ciMap["metadata"])
if err != nil {
return nil, errors.Wrap(err, "converting metadata to proto")
}
cp.Metadata = metadata
spec, err := convertToProtoVal(ciMap["spec"])
if err != nil {
return nil, errors.Wrap(err, "converting spec to proto")
}
cp.Spec = spec
return cp, nil
}
// asConstraintTemplate attempts to convert to template
// Returns:
//
// *ConstraintTemplate: only set if valid template
// bool: (always set) if this is a template
func asConstraintTemplate(data *UnclassifiedConfig) (*ConstraintTemplate, error) {
if data.Group != validTemplateGroup {
return nil, fmt.Errorf("group expected to be %s not %s", validTemplateGroup, data.Group)
}
if data.Kind != "ConstraintTemplate" {
return nil, fmt.Errorf("kind expected to be ConstraintTemplate not %s", data.Kind)
}
generatedKind, err := data.Yaml.GetPath("spec", "crd", "spec", "names", "kind").String()
if err != nil {
return nil, err // field expected to exist
}
rego, err := extractRego(data.Yaml)
if err != nil {
return nil, err // field expected to exist
}
return &ConstraintTemplate{
Confg: data,
GeneratedKind: generatedKind,
Rego: rego,
}, nil
}
func extractRego(yaml *simpleyaml.Yaml) (string, error) {
targets := yaml.GetPath("spec", "targets")
if !targets.IsArray() {
// Old format looks like the following
// targets:
// validation.gcp.forsetisecurity.org:
// rego:
return targets.GetPath(expectedTarget, "rego").String()
}
// New format looks like the following
// targets:
// - target: validation.gcp.forsetisecurity.org
// rego:
size, err := targets.GetArraySize()
if err != nil {
return "", err
}
for i := 0; i < size; i++ {
target := targets.GetIndex(i)
targetString, err := target.Get("target").String()
if err != nil {
return "", err
}
if targetString == expectedTarget {
return target.Get("rego").String()
}
}
return "", status.Error(codes.InvalidArgument, "Unable to locate rego field in constraint template")
}
// convertYAMLToUnclassifiedConfig converts yaml file to an unclassified config, if expected fields don't exist, a log message is printed and the config is skipped.
func convertYAMLToUnclassifiedConfig(config *yamlFile) (*UnclassifiedConfig, error) {
kind, err := config.yaml.Get("kind").String()
if err != nil {
return nil, fmt.Errorf("error in converting %s: %v", config.source, err)
}
group, err := config.yaml.Get("apiVersion").String()
if err != nil {
return nil, fmt.Errorf("error in converting %s: %v", config.source, err)
}
metadataName, err := config.yaml.GetPath("metadata", "name").String()
if err != nil {
return nil, fmt.Errorf("error in converting %s: %v", config.source, err)
}
convertedConfig := &UnclassifiedConfig{
Group: group,
MetadataName: metadataName,
Kind: kind,
Yaml: config.yaml,
FilePath: config.source,
RawFile: string(config.fileContents),
}
return convertedConfig, nil
}
// Returns either a *ConstraintTemplate or a *Constraint or an error
// dataSource should be helpful documentation to help rediscover the source of this information.
func CategorizeYAMLFile(data []byte, dataSource string) (interface{}, error) {
y, err := simpleyaml.NewYaml(data)
if err != nil {
return nil, err
}
unclassified, err := convertYAMLToUnclassifiedConfig(&yamlFile{
yaml: y,
fileContents: data,
source: dataSource,
})
if err != nil {
return nil, err
}
switch unclassified.Group {
case validTemplateGroup:
return asConstraintTemplate(unclassified)
case validConstraintGroup:
return asConstraint(unclassified)
}
return nil, fmt.Errorf("unable to determine configuration type for data %s", dataSource)
}
func convertToProtoVal(from interface{}) (*structpb.Value, error) {
to := &structpb.Value{}
jsn, err := json.Marshal(from)
if err != nil {
return nil, errors.Wrap(err, "marshalling to json")
}
if err := protojson.Unmarshal(jsn, to); err != nil {
return nil, errors.Wrap(err, "unmarshalling to proto")
}
return to, nil
}