Skip to content
Open
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
37 changes: 37 additions & 0 deletions api/kptfile/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ type Function struct {
// `ConfigMap` is a convenient way to specify a function config of kind ConfigMap.
ConfigMap map[string]string `yaml:"configMap,omitempty" json:"configMap,omitempty"`

// `ConfigRef` references an existing resource in the package as the function config.
// The resource is identified by apiVersion, kind, and name. This is mutually exclusive
// with `configPath` and `configMap`.
ConfigRef *ResourceReference `yaml:"configRef,omitempty" json:"configRef,omitempty"`

// `Name` is used to uniquely identify the function declaration
// this is primarily used for merging function declaration with upstream counterparts
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Expand All @@ -363,6 +368,38 @@ type Function struct {
Exclusions []Selector `yaml:"exclude,omitempty" json:"exclude,omitempty"`
}

// ResourceReference identifies a resource within the package by its API identity.
type ResourceReference struct {
// APIVersion of the referenced resource (e.g. "v1"). Optional; when omitted,
// matches resources regardless of apiVersion.
APIVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"`
// Kind of the referenced resource (e.g. "ConfigMap"). Required.
Kind string `yaml:"kind" json:"kind"`
// Name of the referenced resource. Required.
Name string `yaml:"name" json:"name"`
// Namespace of the referenced resource. Optional; when omitted, matches
// resources regardless of namespace.
Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`
}

// Matches reports whether the given resource metadata satisfies this reference.
// Optional fields (APIVersion, Namespace) only filter when non-empty.
func (r *ResourceReference) Matches(meta yaml.ResourceMeta) bool {
if r.APIVersion != "" && meta.APIVersion != r.APIVersion {
return false
}
if meta.Kind != r.Kind {
return false
}
if meta.Name != r.Name {
return false
}
if r.Namespace != "" && meta.Namespace != r.Namespace {
return false
}
return true
}

// Selector specifies the selection criteria
// please update IsEmpty method if more properties are added
type Selector struct {
Expand Down
88 changes: 71 additions & 17 deletions api/kptfile/v1/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ func (p *Pipeline) validate(fsys filesys.FileSystem, pkgPath UniquePath) error {
}

func (f *Function) validate(fsys filesys.FileSystem, fnType string, idx int, pkgPath UniquePath) error {
if err := f.validateExecutor(fnType, idx); err != nil {
return err
}
if err := f.validateConfigSources(fnType, idx); err != nil {
return err
}
if f.ConfigRef != nil {
if err := f.ConfigRef.validate(fnType, idx); err != nil {
return err
}
}
if f.ConfigPath != "" {
if err := f.validateConfigPath(fsys, fnType, idx, pkgPath); err != nil {
return err
}
}
return nil
}

// validateExecutor checks that exactly one of Image or Exec is specified and that
// the image reference is syntactically valid.
func (f *Function) validateExecutor(fnType string, idx int) error {
if f.Image == "" && f.Exec == "" {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
Expand All @@ -79,8 +101,7 @@ func (f *Function) validate(fsys filesys.FileSystem, fnType string, idx int, pkg
}
}
if f.Image != "" {
err := ValidateFunctionImageURL(f.Image)
if err != nil {
if err := ValidateFunctionImageURL(f.Image); err != nil {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].image", fnType, idx),
Value: f.Image,
Expand All @@ -89,28 +110,44 @@ func (f *Function) validate(fsys filesys.FileSystem, fnType string, idx int, pkg
}
}
// TODO(droot): validate the exec
return nil
}

if len(f.ConfigMap) != 0 && f.ConfigPath != "" {
// validateConfigSources ensures at most one of configMap, configPath, or configRef is set.
func (f *Function) validateConfigSources(fnType string, idx int) error {
configSources := 0
if len(f.ConfigMap) != 0 {
configSources++
}
if f.ConfigPath != "" {
configSources++
}
if f.ConfigRef != nil {
configSources++
}
if configSources > 1 {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
Reason: "functionConfig must not specify both `configMap` and `configPath` at the same time",
Reason: "functionConfig must specify at most one of `configMap`, `configPath`, or `configRef`",
}
}
return nil
}

if f.ConfigPath != "" {
if err := validateFnConfigPathSyntax(f.ConfigPath); err != nil {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
Value: f.ConfigPath,
Reason: err.Error(),
}
// validateConfigPath validates the configPath syntax and verifies the referenced file exists.
func (f *Function) validateConfigPath(fsys filesys.FileSystem, fnType string, idx int, pkgPath UniquePath) error {
if err := validateFnConfigPathSyntax(f.ConfigPath); err != nil {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
Value: f.ConfigPath,
Reason: err.Error(),
}
if _, err := GetValidatedFnConfigFromPath(fsys, pkgPath, f.ConfigPath); err != nil {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
Value: f.ConfigPath,
Reason: err.Error(),
}
}
if _, err := GetValidatedFnConfigFromPath(fsys, pkgPath, f.ConfigPath); err != nil {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
Value: f.ConfigPath,
Reason: err.Error(),
}
}
return nil
Expand Down Expand Up @@ -154,6 +191,23 @@ func validateFnConfigPathSyntax(p string) error {
return nil
}

// validate checks that the ResourceReference has the required fields set.
func (r *ResourceReference) validate(fnType string, idx int) error {
if r.Kind == "" {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configRef.kind", fnType, idx),
Reason: "configRef must specify `kind`",
}
}
if r.Name == "" {
return &ValidateError{
Field: fmt.Sprintf("pipeline.%s[%d].configRef.name", fnType, idx),
Reason: "configRef must specify `name`",
}
}
return nil
}

// GetValidatedFnConfigFromPath validates the functionConfig at the path specified by
// the package path (pkgPath) and configPath, returning the functionConfig as an
// RNode if the validation is successful.
Expand Down
167 changes: 167 additions & 0 deletions api/kptfile/v1/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,170 @@ metadata:
})
}
}

func TestConfigRefValidation(t *testing.T) {
type input struct {
name string
kptfile KptFile
valid bool
}

cases := []input{
{
name: "configRef: valid with kind and name",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
Name: "my-config",
},
},
},
},
},
valid: true,
},
{
name: "configRef: valid with all fields",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
APIVersion: "v1",
Kind: "ConfigMap",
Name: "my-config",
Namespace: "default",
},
},
},
},
},
valid: true,
},
{
name: "configRef: missing kind",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Name: "my-config",
},
},
},
},
},
valid: false,
},
{
name: "configRef: missing name",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
},
},
},
},
},
valid: false,
},
{
name: "configRef: mutual exclusivity with configMap",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
Name: "my-config",
},
ConfigMap: map[string]string{
"foo": "bar",
},
},
},
},
},
valid: false,
},
{
name: "configRef: mutual exclusivity with configPath",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
Name: "my-config",
},
ConfigPath: "./config.yaml",
},
},
},
},
valid: false,
},
{
name: "configRef: mutual exclusivity all three",
kptfile: KptFile{
Pipeline: &Pipeline{
Mutators: []Function{
{
Image: "set-namespace",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
Name: "my-config",
},
ConfigPath: "./config.yaml",
ConfigMap: map[string]string{
"foo": "bar",
},
},
},
},
},
valid: false,
},
{
name: "configRef: valid in validators",
kptfile: KptFile{
Pipeline: &Pipeline{
Validators: []Function{
{
Image: "gatekeeper",
ConfigRef: &ResourceReference{
Kind: "ConfigMap",
Name: "policy-config",
},
},
},
},
},
valid: true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := c.kptfile.Validate(filesys.FileSystemOrOnDisk{}, "")
if c.valid && err != nil {
t.Fatalf("kptfile should be valid, %s", err)
}
if !c.valid && err == nil {
t.Fatal("kptfile should not be valid")
}
})
}
}
20 changes: 20 additions & 0 deletions api/kptfile/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading