working commit
This commit is contained in:
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package comments
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
|
||||
)
|
||||
|
||||
// CopyComments recursively copies the comments on fields in from to fields in to
|
||||
func CopyComments(from, to *yaml.RNode) error {
|
||||
// from node should not be modified, it should be just used as a reference
|
||||
fromCopy := from.Copy()
|
||||
copyFieldComments(fromCopy, to)
|
||||
// walk the fields copying comments
|
||||
_, err := walk.Walker{
|
||||
Sources: []*yaml.RNode{fromCopy, to},
|
||||
Visitor: &copier{},
|
||||
VisitKeysAsScalars: true}.Walk()
|
||||
return err
|
||||
}
|
||||
|
||||
// copier implements walk.Visitor, and copies comments to fields shared between 2 instances
|
||||
// of a resource
|
||||
type copier struct{}
|
||||
|
||||
func (c *copier) VisitMap(s walk.Sources, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
|
||||
copyFieldComments(s.Dest(), s.Origin())
|
||||
return s.Dest(), nil
|
||||
}
|
||||
|
||||
func (c *copier) VisitScalar(s walk.Sources, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
|
||||
to := s.Origin()
|
||||
// TODO: File a bug with upstream yaml to handle comments for FoldedStyle scalar nodes
|
||||
// Hack: convert FoldedStyle scalar node to DoubleQuotedStyle as the line comments are
|
||||
// being serialized without space
|
||||
// https://github.com/GoogleContainerTools/kpt/issues/766
|
||||
if to != nil && to.Document().Style == yaml.FoldedStyle {
|
||||
to.Document().Style = yaml.DoubleQuotedStyle
|
||||
}
|
||||
|
||||
copyFieldComments(s.Dest(), to)
|
||||
return s.Dest(), nil
|
||||
}
|
||||
|
||||
func (c *copier) VisitList(s walk.Sources, _ *openapi.ResourceSchema, _ walk.ListKind) (
|
||||
*yaml.RNode, error) {
|
||||
copyFieldComments(s.Dest(), s.Origin())
|
||||
destItems := s.Dest().Content()
|
||||
originItems := s.Origin().Content()
|
||||
|
||||
for i := 0; i < len(destItems) && i < len(originItems); i++ {
|
||||
dest := destItems[i]
|
||||
origin := originItems[i]
|
||||
|
||||
if dest.Value == origin.Value {
|
||||
// We copy the comments recursively on each node in the list.
|
||||
if err := CopyComments(yaml.NewRNode(dest), yaml.NewRNode(origin)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.Dest(), nil
|
||||
}
|
||||
|
||||
// copyFieldComments copies the comment from one field to another
|
||||
func copyFieldComments(from, to *yaml.RNode) {
|
||||
if from == nil || to == nil {
|
||||
return
|
||||
}
|
||||
if to.Document().LineComment == "" {
|
||||
to.Document().LineComment = from.Document().LineComment
|
||||
}
|
||||
if to.Document().HeadComment == "" {
|
||||
to.Document().HeadComment = from.Document().HeadComment
|
||||
}
|
||||
if to.Document().FootComment == "" {
|
||||
to.Document().FootComment = from.Document().FootComment
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package errors provides libraries for working with the go-errors/errors library.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
goerrors "github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// Wrap returns err wrapped in a go-error. If err is nil, returns nil.
|
||||
func Wrap(err interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return goerrors.Wrap(err, 1)
|
||||
}
|
||||
|
||||
// WrapPrefixf returns err wrapped in a go-error with a message prefix. If err is nil, returns nil.
|
||||
func WrapPrefixf(err interface{}, msg string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return goerrors.WrapPrefix(err, fmt.Sprintf(msg, args...), 1)
|
||||
}
|
||||
|
||||
// Errorf returns a new go-error.
|
||||
func Errorf(msg string, args ...interface{}) error {
|
||||
return goerrors.Wrap(fmt.Errorf(msg, args...), 1)
|
||||
}
|
||||
|
||||
// As finds the targeted error in any wrapped error.
|
||||
func As(err error, target interface{}) bool {
|
||||
return goerrors.As(err, target)
|
||||
}
|
||||
|
||||
// Is detects whether the error is equal to a given error.
|
||||
func Is(err error, target error) bool {
|
||||
return goerrors.Is(err, target)
|
||||
}
|
||||
|
||||
// GetStack returns a stack trace for the error if it has one
|
||||
func GetStack(err error) string {
|
||||
if e, ok := err.(*goerrors.Error); ok {
|
||||
return string(e.Stack())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ext
|
||||
|
||||
// IgnoreFileName returns the name for ignore files in
|
||||
// packages. It can be overridden by tools using this library.
|
||||
var IgnoreFileName = func() string {
|
||||
return ".krmignore"
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package fieldmeta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// FieldMeta contains metadata that may be attached to fields as comments
|
||||
type FieldMeta struct {
|
||||
Schema spec.Schema
|
||||
|
||||
Extensions XKustomize
|
||||
|
||||
SettersSchema *spec.Schema
|
||||
}
|
||||
|
||||
type XKustomize struct {
|
||||
SetBy string `yaml:"setBy,omitempty" json:"setBy,omitempty"`
|
||||
PartialFieldSetters []PartialFieldSetter `yaml:"partialSetters,omitempty" json:"partialSetters,omitempty"`
|
||||
FieldSetter *PartialFieldSetter `yaml:"setter,omitempty" json:"setter,omitempty"`
|
||||
}
|
||||
|
||||
// PartialFieldSetter defines how to set part of a field rather than the full field
|
||||
// value. e.g. the tag part of an image field
|
||||
type PartialFieldSetter struct {
|
||||
// Name is the name of this setter.
|
||||
Name string `yaml:"name" json:"name"`
|
||||
|
||||
// Value is the current value that has been set.
|
||||
Value string `yaml:"value" json:"value"`
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the FieldMeta has any empty Schema
|
||||
func (fm *FieldMeta) IsEmpty() bool {
|
||||
if fm == nil {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(fm.Schema, spec.Schema{})
|
||||
}
|
||||
|
||||
// Read reads the FieldMeta from a node
|
||||
func (fm *FieldMeta) Read(n *yaml.RNode) error {
|
||||
// check for metadata on head and line comments
|
||||
comments := []string{n.YNode().LineComment, n.YNode().HeadComment}
|
||||
for _, c := range comments {
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
c := strings.TrimLeft(c, "#")
|
||||
|
||||
// check for new short hand notation or fall back to openAPI ref format
|
||||
if !fm.processShortHand(c) {
|
||||
// if it doesn't Unmarshal that is fine, it means there is no metadata
|
||||
// other comments are valid, they just don't parse
|
||||
// TODO: consider more sophisticated parsing techniques similar to what is used
|
||||
// for go struct tags.
|
||||
if err := fm.Schema.UnmarshalJSON([]byte(c)); err != nil {
|
||||
// note: don't return an error if the comment isn't a fieldmeta struct
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fe := fm.Schema.VendorExtensible.Extensions["x-kustomize"]
|
||||
if fe == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(fe)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
return json.Unmarshal(b, &fm.Extensions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processShortHand parses the comment for short hand ref, loads schema to fm
|
||||
// and returns true if successful, returns false for any other cases and not throw
|
||||
// error, as the comment might not be a setter ref
|
||||
func (fm *FieldMeta) processShortHand(comment string) bool {
|
||||
input := map[string]string{}
|
||||
err := json.Unmarshal([]byte(comment), &input)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
name := input[shortHandRef]
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if setter with the name exists, else check for a substitution
|
||||
// setter and substitution can't have same name in shorthand
|
||||
|
||||
setterRef, err := spec.NewRef(DefinitionsPrefix + SetterDefinitionPrefix + name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
setterRefBytes, err := setterRef.MarshalJSON()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := openapi.Resolve(&setterRef, fm.SettersSchema); err == nil {
|
||||
setterErr := fm.Schema.UnmarshalJSON(setterRefBytes)
|
||||
return setterErr == nil
|
||||
}
|
||||
|
||||
substRef, err := spec.NewRef(DefinitionsPrefix + SubstitutionDefinitionPrefix + name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
substRefBytes, err := substRef.MarshalJSON()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := openapi.Resolve(&substRef, fm.SettersSchema); err == nil {
|
||||
substErr := fm.Schema.UnmarshalJSON(substRefBytes)
|
||||
return substErr == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isExtensionEmpty(x XKustomize) bool {
|
||||
if x.FieldSetter != nil {
|
||||
return false
|
||||
}
|
||||
if x.SetBy != "" {
|
||||
return false
|
||||
}
|
||||
if len(x.PartialFieldSetters) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Write writes the FieldMeta to a node
|
||||
func (fm *FieldMeta) Write(n *yaml.RNode) error {
|
||||
if !isExtensionEmpty(fm.Extensions) {
|
||||
return fm.WriteV1Setters(n)
|
||||
}
|
||||
|
||||
// Ref is removed when a setter is deleted, so the Ref string could be empty.
|
||||
if fm.Schema.Ref.String() != "" {
|
||||
// Ex: {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} should be converted to
|
||||
// {"$openAPI":"replicas"} and added to the line comment
|
||||
ref := fm.Schema.Ref.String()
|
||||
var shortHandRefValue string
|
||||
switch {
|
||||
case strings.HasPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix):
|
||||
shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix)
|
||||
case strings.HasPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix):
|
||||
shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix)
|
||||
default:
|
||||
return fmt.Errorf("unexpected ref format: %s", ref)
|
||||
}
|
||||
n.YNode().LineComment = fmt.Sprintf(`{"%s":"%s"}`, shortHandRef,
|
||||
shortHandRefValue)
|
||||
} else {
|
||||
n.YNode().LineComment = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteV1Setters is the v1 setters way of writing setter definitions
|
||||
// TODO: pmarupaka - remove this method after migration
|
||||
func (fm *FieldMeta) WriteV1Setters(n *yaml.RNode) error {
|
||||
fm.Schema.VendorExtensible.AddExtension("x-kustomize", fm.Extensions)
|
||||
b, err := json.Marshal(fm.Schema)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
n.YNode().LineComment = string(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FieldValueType defines the type of input to register
|
||||
type FieldValueType string
|
||||
|
||||
const (
|
||||
// String defines a string flag
|
||||
String FieldValueType = "string"
|
||||
// Bool defines a bool flag
|
||||
Bool = "boolean"
|
||||
// Int defines an int flag
|
||||
Int = "integer"
|
||||
)
|
||||
|
||||
func (it FieldValueType) String() string {
|
||||
if it == "" {
|
||||
return "string"
|
||||
}
|
||||
return string(it)
|
||||
}
|
||||
|
||||
func (it FieldValueType) Validate(value string) error {
|
||||
switch it {
|
||||
case Int:
|
||||
if _, err := strconv.Atoi(value); err != nil {
|
||||
return errors.WrapPrefixf(err, "value must be an int")
|
||||
}
|
||||
case Bool:
|
||||
if _, err := strconv.ParseBool(value); err != nil {
|
||||
return errors.WrapPrefixf(err, "value must be a bool")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (it FieldValueType) Tag() string {
|
||||
switch it {
|
||||
case String:
|
||||
return yaml.NodeTagString
|
||||
case Bool:
|
||||
return yaml.NodeTagBool
|
||||
case Int:
|
||||
return yaml.NodeTagInt
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (it FieldValueType) TagForValue(value string) string {
|
||||
switch it {
|
||||
case String:
|
||||
return yaml.NodeTagString
|
||||
case Bool:
|
||||
if _, err := strconv.ParseBool(string(it)); err != nil {
|
||||
return ""
|
||||
}
|
||||
return yaml.NodeTagBool
|
||||
case Int:
|
||||
if _, err := strconv.ParseInt(string(it), 0, 32); err != nil {
|
||||
return ""
|
||||
}
|
||||
return yaml.NodeTagInt
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
// CLIDefinitionsPrefix is the prefix for cli definition keys.
|
||||
CLIDefinitionsPrefix = "io.k8s.cli."
|
||||
|
||||
// SetterDefinitionPrefix is the prefix for setter definition keys.
|
||||
SetterDefinitionPrefix = CLIDefinitionsPrefix + "setters."
|
||||
|
||||
// SubstitutionDefinitionPrefix is the prefix for substitution definition keys.
|
||||
SubstitutionDefinitionPrefix = CLIDefinitionsPrefix + "substitutions."
|
||||
|
||||
// DefinitionsPrefix is the prefix used to reference definitions in the OpenAPI
|
||||
DefinitionsPrefix = "#/definitions/"
|
||||
)
|
||||
|
||||
// shortHandRef is the shorthand reference to setters and substitutions
|
||||
var shortHandRef = "$openapi"
|
||||
|
||||
func SetShortHandRef(ref string) {
|
||||
shortHandRef = ref
|
||||
}
|
||||
|
||||
func ShortHandRef() string {
|
||||
return shortHandRef
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConfirmedDir is a clean, absolute, delinkified path
|
||||
// that was confirmed to point to an existing directory.
|
||||
type ConfirmedDir string
|
||||
|
||||
// NewTmpConfirmedDir returns a temporary dir, else error.
|
||||
// The directory is cleaned, no symlinks, etc. so it's
|
||||
// returned as a ConfirmedDir.
|
||||
func NewTmpConfirmedDir() (ConfirmedDir, error) {
|
||||
n, err := os.MkdirTemp("", "kustomize-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// In MacOs `os.MkdirTemp` creates a directory
|
||||
// with root in the `/var` folder, which is in turn
|
||||
// a symlinked path to `/private/var`.
|
||||
// Function `filepath.EvalSymlinks`is used to
|
||||
// resolve the real absolute path.
|
||||
deLinked, err := filepath.EvalSymlinks(n)
|
||||
return ConfirmedDir(deLinked), err
|
||||
}
|
||||
|
||||
// HasPrefix returns true if the directory argument
|
||||
// is a prefix of self (d) from the point of view of
|
||||
// a file system.
|
||||
//
|
||||
// I.e., it's true if the argument equals or contains
|
||||
// self (d) in a file path sense.
|
||||
//
|
||||
// HasPrefix emulates the semantics of strings.HasPrefix
|
||||
// such that the following are true:
|
||||
//
|
||||
// strings.HasPrefix("foobar", "foobar")
|
||||
// strings.HasPrefix("foobar", "foo")
|
||||
// strings.HasPrefix("foobar", "")
|
||||
//
|
||||
// d := fSys.ConfirmDir("/foo/bar")
|
||||
// d.HasPrefix("/foo/bar")
|
||||
// d.HasPrefix("/foo")
|
||||
// d.HasPrefix("/")
|
||||
//
|
||||
// Not contacting a file system here to check for
|
||||
// actual path existence.
|
||||
//
|
||||
// This is tested on linux, but will have trouble
|
||||
// on other operating systems.
|
||||
// TODO(monopole) Refactor when #golang/go/18358 closes.
|
||||
// See also:
|
||||
// https://github.com/golang/go/issues/18358
|
||||
// https://github.com/golang/dep/issues/296
|
||||
// https://github.com/golang/dep/blob/master/internal/fs/fs.go#L33
|
||||
// https://codereview.appspot.com/5712045
|
||||
func (d ConfirmedDir) HasPrefix(path ConfirmedDir) bool {
|
||||
if path.String() == string(filepath.Separator) || path == d {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(
|
||||
string(d),
|
||||
string(path)+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func (d ConfirmedDir) Join(path string) string {
|
||||
return filepath.Join(string(d), path)
|
||||
}
|
||||
|
||||
func (d ConfirmedDir) String() string {
|
||||
return string(d)
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package filesys provides a file system abstraction,
|
||||
// a subset of that provided by golang.org/pkg/os,
|
||||
// with an on-disk and in-memory representation.
|
||||
package filesys
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// File groups the basic os.File methods.
|
||||
type File interface {
|
||||
io.ReadWriteCloser
|
||||
Stat() (os.FileInfo, error)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ os.FileInfo = fileInfo{}
|
||||
|
||||
// fileInfo implements os.FileInfo for a fileInMemory instance.
|
||||
type fileInfo struct {
|
||||
node *fsNode
|
||||
}
|
||||
|
||||
// Name returns the name of the file
|
||||
func (fi fileInfo) Name() string { return fi.node.Name() }
|
||||
|
||||
// Size returns the size of the file
|
||||
func (fi fileInfo) Size() int64 { return fi.node.Size() }
|
||||
|
||||
// Mode returns the file mode
|
||||
func (fi fileInfo) Mode() os.FileMode { return 0777 }
|
||||
|
||||
// ModTime returns a bogus time
|
||||
func (fi fileInfo) ModTime() time.Time { return time.Time{} }
|
||||
|
||||
// IsDir returns true if it is a directory
|
||||
func (fi fileInfo) IsDir() bool { return fi.node.isNodeADir() }
|
||||
|
||||
// Sys should return underlying data source, but it now returns nil
|
||||
func (fi fileInfo) Sys() interface{} { return nil }
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ File = &fileOnDisk{}
|
||||
|
||||
// fileOnDisk implements File using the local filesystem.
|
||||
type fileOnDisk struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// Close closes a file.
|
||||
func (f *fileOnDisk) Close() error { return f.file.Close() }
|
||||
|
||||
// Read reads a file's content.
|
||||
func (f *fileOnDisk) Read(p []byte) (n int, err error) { return f.file.Read(p) }
|
||||
|
||||
// Write writes bytes to a file
|
||||
func (f *fileOnDisk) Write(p []byte) (n int, err error) { return f.file.Write(p) }
|
||||
|
||||
// Stat returns an interface which has all the information regarding the file.
|
||||
func (f *fileOnDisk) Stat() (os.FileInfo, error) { return f.file.Stat() }
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
Separator = string(filepath.Separator)
|
||||
SelfDir = "."
|
||||
ParentDir = ".."
|
||||
)
|
||||
|
||||
// FileSystem groups basic os filesystem methods.
|
||||
// It's supposed be functional subset of https://golang.org/pkg/os
|
||||
type FileSystem interface {
|
||||
|
||||
// Create a file.
|
||||
Create(path string) (File, error)
|
||||
|
||||
// MkDir makes a directory.
|
||||
Mkdir(path string) error
|
||||
|
||||
// MkDirAll makes a directory path, creating intervening directories.
|
||||
MkdirAll(path string) error
|
||||
|
||||
// RemoveAll removes path and any children it contains.
|
||||
RemoveAll(path string) error
|
||||
|
||||
// Open opens the named file for reading.
|
||||
Open(path string) (File, error)
|
||||
|
||||
// IsDir returns true if the path is a directory.
|
||||
IsDir(path string) bool
|
||||
|
||||
// ReadDir returns a list of files and directories within a directory.
|
||||
ReadDir(path string) ([]string, error)
|
||||
|
||||
// CleanedAbs converts the given path into a
|
||||
// directory and a file name, where the directory
|
||||
// is represented as a ConfirmedDir and all that implies.
|
||||
// If the entire path is a directory, the file component
|
||||
// is an empty string.
|
||||
CleanedAbs(path string) (ConfirmedDir, string, error)
|
||||
|
||||
// Exists is true if the path exists in the file system.
|
||||
Exists(path string) bool
|
||||
|
||||
// Glob returns the list of matching files,
|
||||
// emulating https://golang.org/pkg/path/filepath/#Glob
|
||||
Glob(pattern string) ([]string, error)
|
||||
|
||||
// ReadFile returns the contents of the file at the given path.
|
||||
ReadFile(path string) ([]byte, error)
|
||||
|
||||
// WriteFile writes the data to a file at the given path,
|
||||
// overwriting anything that's already there.
|
||||
WriteFile(path string, data []byte) error
|
||||
|
||||
// Walk walks the file system with the given WalkFunc.
|
||||
Walk(path string, walkFn filepath.WalkFunc) error
|
||||
}
|
||||
|
||||
// ConfirmDir returns an error if the user-specified path is not an existing directory on fSys.
|
||||
// Otherwise, ConfirmDir returns path, which can be relative, as a ConfirmedDir and all that implies.
|
||||
func ConfirmDir(fSys FileSystem, path string) (ConfirmedDir, error) {
|
||||
if path == "" {
|
||||
return "", errors.Errorf("directory path cannot be empty")
|
||||
}
|
||||
|
||||
d, f, err := fSys.CleanedAbs(path)
|
||||
if err != nil {
|
||||
return "", errors.WrapPrefixf(err, "not a valid directory")
|
||||
}
|
||||
if f != "" {
|
||||
return "", errors.WrapPrefixf(errors.Errorf("file is not directory"), "'%s'", path)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// FileSystemOrOnDisk satisfies the FileSystem interface by forwarding
|
||||
// all of its method calls to the given FileSystem whenever it's not nil.
|
||||
// If it's nil, the call is forwarded to the OS's underlying file system.
|
||||
type FileSystemOrOnDisk struct {
|
||||
FileSystem FileSystem
|
||||
}
|
||||
|
||||
// Set sets the given FileSystem as the target for all the FileSystem method calls.
|
||||
func (fs *FileSystemOrOnDisk) Set(f FileSystem) { fs.FileSystem = f }
|
||||
|
||||
func (fs FileSystemOrOnDisk) fs() FileSystem {
|
||||
if fs.FileSystem != nil {
|
||||
return fs.FileSystem
|
||||
}
|
||||
return MakeFsOnDisk()
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Create(path string) (File, error) {
|
||||
return fs.fs().Create(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Mkdir(path string) error {
|
||||
return fs.fs().Mkdir(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) MkdirAll(path string) error {
|
||||
return fs.fs().MkdirAll(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) RemoveAll(path string) error {
|
||||
return fs.fs().RemoveAll(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Open(path string) (File, error) {
|
||||
return fs.fs().Open(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) IsDir(path string) bool {
|
||||
return fs.fs().IsDir(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) ReadDir(path string) ([]string, error) {
|
||||
return fs.fs().ReadDir(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) CleanedAbs(path string) (ConfirmedDir, string, error) {
|
||||
return fs.fs().CleanedAbs(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Exists(path string) bool {
|
||||
return fs.fs().Exists(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Glob(pattern string) ([]string, error) {
|
||||
return fs.fs().Glob(pattern)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) ReadFile(path string) ([]byte, error) {
|
||||
return fs.fs().ReadFile(path)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) WriteFile(path string, data []byte) error {
|
||||
return fs.fs().WriteFile(path, data)
|
||||
}
|
||||
|
||||
func (fs FileSystemOrOnDisk) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||
return fs.fs().Walk(path, walkFn)
|
||||
}
|
||||
+647
@@ -0,0 +1,647 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
var _ File = &fsNode{}
|
||||
var _ FileSystem = &fsNode{}
|
||||
|
||||
// fsNode is either a file or a directory.
|
||||
type fsNode struct {
|
||||
// What node owns me?
|
||||
parent *fsNode
|
||||
|
||||
// Value to return as the Name() when the
|
||||
// parent is nil.
|
||||
nilParentName string
|
||||
|
||||
// A directory mapping names to nodes.
|
||||
// If dir is nil, then self node is a file.
|
||||
// If dir is non-nil, then self node is a directory,
|
||||
// albeit possibly an empty directory.
|
||||
dir map[string]*fsNode
|
||||
|
||||
// if this node is a file, this is the content.
|
||||
content []byte
|
||||
|
||||
// if offset is not nil the file is open and it tracks
|
||||
// the current file offset.
|
||||
offset *int
|
||||
}
|
||||
|
||||
// MakeEmptyDirInMemory returns an empty directory.
|
||||
// The paths of nodes in this object will never
|
||||
// report a leading Separator, meaning they
|
||||
// aren't "absolute" in the sense defined by
|
||||
// https://golang.org/pkg/path/filepath/#IsAbs.
|
||||
func MakeEmptyDirInMemory() *fsNode {
|
||||
return &fsNode{
|
||||
dir: make(map[string]*fsNode),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeFsInMemory returns an empty 'file system'.
|
||||
// The paths of nodes in this object will always
|
||||
// report a leading Separator, meaning they
|
||||
// are "absolute" in the sense defined by
|
||||
// https://golang.org/pkg/path/filepath/#IsAbs.
|
||||
// This is a relevant difference when using Walk,
|
||||
// Glob, Match, etc.
|
||||
func MakeFsInMemory() FileSystem {
|
||||
return &fsNode{
|
||||
nilParentName: Separator,
|
||||
dir: make(map[string]*fsNode),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name of the node.
|
||||
func (n *fsNode) Name() string {
|
||||
if n.parent == nil {
|
||||
// Unable to lookup name in parent.
|
||||
return n.nilParentName
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir")
|
||||
}
|
||||
for key, value := range n.parent.dir {
|
||||
if value == n {
|
||||
return key
|
||||
}
|
||||
}
|
||||
log.Fatal("unable to find fsNode name")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Path returns the full path to the node.
|
||||
func (n *fsNode) Path() string {
|
||||
if n.parent == nil {
|
||||
return n.nilParentName
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir, structural error")
|
||||
}
|
||||
return filepath.Join(n.parent.Path(), n.Name())
|
||||
}
|
||||
|
||||
// mySplit trims trailing separators from the directory
|
||||
// result of filepath.Split.
|
||||
func mySplit(s string) (string, string) {
|
||||
dName, fName := filepath.Split(s)
|
||||
return StripTrailingSeps(dName), fName
|
||||
}
|
||||
|
||||
func (n *fsNode) addFile(name string, c []byte) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, fileName := mySplit(name)
|
||||
if dName != "" {
|
||||
parent, err = parent.addDir(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !isLegalFileNameForCreation(fileName) {
|
||||
return nil, fmt.Errorf(
|
||||
"illegal name '%s' in file creation", fileName)
|
||||
}
|
||||
result, ok := parent.dir[fileName]
|
||||
if ok {
|
||||
// File already exists; overwrite it.
|
||||
if result.offset != nil {
|
||||
return nil, fmt.Errorf("cannot add already opened file '%s'", n.Path())
|
||||
}
|
||||
result.content = append(result.content[:0], c...)
|
||||
return result, nil
|
||||
}
|
||||
result = &fsNode{
|
||||
content: append([]byte(nil), c...),
|
||||
parent: parent,
|
||||
}
|
||||
parent.dir[fileName] = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Create implements FileSystem.
|
||||
// Create makes an empty file.
|
||||
func (n *fsNode) Create(path string) (result File, err error) {
|
||||
f, err := n.AddFile(path, nil)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
f.offset = new(int)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// WriteFile implements FileSystem.
|
||||
func (n *fsNode) WriteFile(path string, d []byte) error {
|
||||
_, err := n.AddFile(path, d)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddFile adds a file and any necessary containing
|
||||
// directories to the node.
|
||||
func (n *fsNode) AddFile(
|
||||
name string, c []byte) (result *fsNode, err error) {
|
||||
if n.dir == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a file to a non-directory '%s'", n.Name())
|
||||
}
|
||||
return n.addFile(cleanQueryPath(name), c)
|
||||
}
|
||||
|
||||
func (n *fsNode) addDir(path string) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, subDirName := mySplit(path)
|
||||
if dName != "" {
|
||||
parent, err = n.addDir(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
switch subDirName {
|
||||
case "", SelfDir:
|
||||
return n, nil
|
||||
case ParentDir:
|
||||
if n.parent == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a directory above '%s'", n.Path())
|
||||
}
|
||||
return n.parent, nil
|
||||
default:
|
||||
if !isLegalFileNameForCreation(subDirName) {
|
||||
return nil, fmt.Errorf(
|
||||
"illegal name '%s' in directory creation", subDirName)
|
||||
}
|
||||
result, ok := parent.dir[subDirName]
|
||||
if ok {
|
||||
if result.isNodeADir() {
|
||||
// it's already there.
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"cannot make dir '%s'; a file of that name already exists in '%s'",
|
||||
subDirName, parent.Name())
|
||||
}
|
||||
result = &fsNode{
|
||||
dir: make(map[string]*fsNode),
|
||||
parent: parent,
|
||||
}
|
||||
parent.dir[subDirName] = result
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Mkdir implements FileSystem.
|
||||
// Mkdir creates a directory.
|
||||
func (n *fsNode) Mkdir(path string) error {
|
||||
_, err := n.AddDir(path)
|
||||
return err
|
||||
}
|
||||
|
||||
// MkdirAll implements FileSystem.
|
||||
// MkdirAll creates a directory.
|
||||
func (n *fsNode) MkdirAll(path string) error {
|
||||
_, err := n.AddDir(path)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddDir adds a directory to the node, not complaining
|
||||
// if it is already there.
|
||||
func (n *fsNode) AddDir(path string) (result *fsNode, err error) {
|
||||
if n.dir == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"cannot add a directory to file node '%s'", n.Name())
|
||||
}
|
||||
return n.addDir(cleanQueryPath(path))
|
||||
}
|
||||
|
||||
// CleanedAbs implements FileSystem.
|
||||
func (n *fsNode) CleanedAbs(path string) (ConfirmedDir, string, error) {
|
||||
node, err := n.Find(path)
|
||||
if err != nil {
|
||||
return "", "", errors.WrapPrefixf(err, "unable to clean")
|
||||
}
|
||||
if node == nil {
|
||||
return "", "", notExistError(path)
|
||||
}
|
||||
if node.isNodeADir() {
|
||||
return ConfirmedDir(node.Path()), "", nil
|
||||
}
|
||||
return ConfirmedDir(node.parent.Path()), node.Name(), nil
|
||||
}
|
||||
|
||||
// Exists implements FileSystem.
|
||||
// Exists returns true if the path exists.
|
||||
func (n *fsNode) Exists(path string) bool {
|
||||
if !n.isNodeADir() {
|
||||
return n.Name() == path
|
||||
}
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return result != nil
|
||||
}
|
||||
|
||||
func cleanQueryPath(path string) string {
|
||||
// Always ignore leading separator?
|
||||
// Remember that filepath.Clean returns "." if
|
||||
// given an empty string argument.
|
||||
return filepath.Clean(StripLeadingSeps(path))
|
||||
}
|
||||
|
||||
// Find finds the given node, else nil if not found.
|
||||
// Return error on structural/argument errors.
|
||||
func (n *fsNode) Find(path string) (*fsNode, error) {
|
||||
if !n.isNodeADir() {
|
||||
return nil, fmt.Errorf("can only find inside a dir")
|
||||
}
|
||||
if path == "" {
|
||||
// Special case; check *before* cleaning and *before*
|
||||
// comparison to nilParentName.
|
||||
return nil, nil
|
||||
}
|
||||
if (n.parent == nil && path == n.nilParentName) || path == SelfDir {
|
||||
// Special case
|
||||
return n, nil
|
||||
}
|
||||
return n.findIt(cleanQueryPath(path))
|
||||
}
|
||||
|
||||
func (n *fsNode) findIt(path string) (result *fsNode, err error) {
|
||||
parent := n
|
||||
dName, item := mySplit(path)
|
||||
if dName != "" {
|
||||
parent, err = n.findIt(dName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent == nil {
|
||||
// all done, target doesn't exist.
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if !parent.isNodeADir() {
|
||||
return nil, fmt.Errorf("'%s' is not a directory", parent.Path())
|
||||
}
|
||||
return parent.dir[item], nil
|
||||
}
|
||||
|
||||
// RemoveAll implements FileSystem.
|
||||
// RemoveAll removes an item and everything it contains.
|
||||
func (n *fsNode) RemoveAll(path string) error {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
// If the path doesn't exist, no need to remove anything.
|
||||
return nil
|
||||
}
|
||||
return result.Remove()
|
||||
}
|
||||
|
||||
// Remove drop the node, and everything it contains, from its parent.
|
||||
func (n *fsNode) Remove() error {
|
||||
if n.parent == nil {
|
||||
return fmt.Errorf("cannot remove a root node")
|
||||
}
|
||||
if !n.parent.isNodeADir() {
|
||||
log.Fatal("parent not a dir")
|
||||
}
|
||||
for key, value := range n.parent.dir {
|
||||
if value == n {
|
||||
delete(n.parent.dir, key)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
log.Fatal("unable to find self in parent")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNodeADir returns true if the node is a directory.
|
||||
// Cannot collide with the poorly named "IsDir".
|
||||
func (n *fsNode) isNodeADir() bool {
|
||||
return n.dir != nil
|
||||
}
|
||||
|
||||
// IsDir implements FileSystem.
|
||||
// IsDir returns true if the argument resolves
|
||||
// to a directory rooted at the node.
|
||||
func (n *fsNode) IsDir(path string) bool {
|
||||
result, err := n.Find(path)
|
||||
if err != nil || result == nil {
|
||||
return false
|
||||
}
|
||||
return result.isNodeADir()
|
||||
}
|
||||
|
||||
// ReadDir implements FileSystem.
|
||||
func (n *fsNode) ReadDir(path string) ([]string, error) {
|
||||
if !n.Exists(path) {
|
||||
return nil, notExistError(path)
|
||||
}
|
||||
if !n.IsDir(path) {
|
||||
return nil, fmt.Errorf("%s is not a directory", path)
|
||||
}
|
||||
|
||||
dir, err := n.Find(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dir == nil {
|
||||
return nil, fmt.Errorf("could not find directory %s", path)
|
||||
}
|
||||
|
||||
keys := make([]string, len(dir.dir))
|
||||
i := 0
|
||||
for k := range dir.dir {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// Size returns the size of the node.
|
||||
func (n *fsNode) Size() int64 {
|
||||
if n.isNodeADir() {
|
||||
return int64(len(n.dir))
|
||||
}
|
||||
return int64(len(n.content))
|
||||
}
|
||||
|
||||
// Open implements FileSystem.
|
||||
// Open opens the node in read-write mode and sets the offset its start.
|
||||
// Writing right after opening the file will replace the original content
|
||||
// and move the offset forward, as with a file opened with O_RDWR | O_CREATE.
|
||||
//
|
||||
// As an example, let's consider a file with content "content":
|
||||
// - open: sets offset to start, content is "content"
|
||||
// - write "@": offset increases by one, the content is now "@ontent"
|
||||
// - read the rest: since offset is 1, the read operation returns "ontent"
|
||||
// - write "$": offset is at EOF, so "$" is appended and content is now "@ontent$"
|
||||
// - read the rest: returns 0 bytes and EOF
|
||||
// - close: the content is still "@ontent$"
|
||||
func (n *fsNode) Open(path string) (File, error) {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, notExistError(path)
|
||||
}
|
||||
if result.offset != nil {
|
||||
return nil, fmt.Errorf("cannot open previously opened file '%s'", path)
|
||||
}
|
||||
result.offset = new(int)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Close marks the node closed.
|
||||
func (n *fsNode) Close() error {
|
||||
if n.offset == nil {
|
||||
return fmt.Errorf("cannot close already closed file '%s'", n.Path())
|
||||
}
|
||||
n.offset = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadFile implements FileSystem.
|
||||
func (n *fsNode) ReadFile(path string) (c []byte, err error) {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, notExistError(path)
|
||||
}
|
||||
if result.isNodeADir() {
|
||||
return nil, fmt.Errorf("cannot read content from non-file '%s'", n.Path())
|
||||
}
|
||||
c = make([]byte, len(result.content))
|
||||
copy(c, result.content)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Read returns the content of the file node.
|
||||
func (n *fsNode) Read(d []byte) (c int, err error) {
|
||||
if n.isNodeADir() {
|
||||
return 0, fmt.Errorf(
|
||||
"cannot read content from non-file '%s'", n.Path())
|
||||
}
|
||||
if n.offset == nil {
|
||||
return 0, fmt.Errorf("cannot read from closed file '%s'", n.Path())
|
||||
}
|
||||
|
||||
rest := n.content[*n.offset:]
|
||||
if len(d) < len(rest) {
|
||||
rest = rest[:len(d)]
|
||||
} else {
|
||||
err = io.EOF
|
||||
}
|
||||
copy(d, rest)
|
||||
*n.offset += len(rest)
|
||||
return len(rest), err
|
||||
}
|
||||
|
||||
// Write saves the contents of the argument to the file node.
|
||||
func (n *fsNode) Write(p []byte) (c int, err error) {
|
||||
if n.isNodeADir() {
|
||||
return 0, fmt.Errorf(
|
||||
"cannot write content to non-file '%s'", n.Path())
|
||||
}
|
||||
if n.offset == nil {
|
||||
return 0, fmt.Errorf("cannot write to closed file '%s'", n.Path())
|
||||
}
|
||||
n.content = append(n.content[:*n.offset], p...)
|
||||
*n.offset = len(n.content)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ContentMatches returns true if v matches fake file's content.
|
||||
func (n *fsNode) ContentMatches(v []byte) bool {
|
||||
return bytes.Equal(v, n.content)
|
||||
}
|
||||
|
||||
// GetContent the content of a fake file.
|
||||
func (n *fsNode) GetContent() []byte {
|
||||
return n.content
|
||||
}
|
||||
|
||||
// Stat returns an instance of FileInfo.
|
||||
func (n *fsNode) Stat() (os.FileInfo, error) {
|
||||
return fileInfo{node: n}, nil
|
||||
}
|
||||
|
||||
// Walk implements FileSystem.
|
||||
func (n *fsNode) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||
result, err := n.Find(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
return notExistError(path)
|
||||
}
|
||||
return result.WalkMe(walkFn)
|
||||
}
|
||||
|
||||
// Walk runs the given walkFn on each node.
|
||||
func (n *fsNode) WalkMe(walkFn filepath.WalkFunc) error {
|
||||
fi, err := n.Stat()
|
||||
// always visit self first
|
||||
err = walkFn(n.Path(), fi, err)
|
||||
if !n.isNodeADir() {
|
||||
// it's a file, so nothing more to do
|
||||
return err
|
||||
}
|
||||
// process self as a directory
|
||||
if err == filepath.SkipDir {
|
||||
return nil
|
||||
}
|
||||
// Walk is supposed to visit in lexical order.
|
||||
for _, k := range n.sortedDirEntries() {
|
||||
if err := n.dir[k].WalkMe(walkFn); err != nil {
|
||||
if err == filepath.SkipDir {
|
||||
// stop processing this directory
|
||||
break
|
||||
}
|
||||
// bail out completely
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *fsNode) sortedDirEntries() []string {
|
||||
keys := make([]string, len(n.dir))
|
||||
i := 0
|
||||
for k := range n.dir {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// FileCount returns a count of files.
|
||||
// Directories, empty or otherwise, not counted.
|
||||
func (n *fsNode) FileCount() int {
|
||||
count := 0
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
func (n *fsNode) DebugPrint() {
|
||||
n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Printf("err '%v' at path %q\n", err, path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
if info.Size() == 0 {
|
||||
fmt.Println("empty dir: " + path)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" file: " + path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var legalFileNamePattern = regexp.MustCompile("^[a-zA-Z0-9-_.:]+$")
|
||||
|
||||
// This rules enforced here should be simpler and tighter
|
||||
// than what's allowed on a real OS.
|
||||
// Should be fine for testing or in-memory purposes.
|
||||
func isLegalFileNameForCreation(n string) bool {
|
||||
if n == "" || n == SelfDir || !legalFileNamePattern.MatchString(n) {
|
||||
return false
|
||||
}
|
||||
return !strings.Contains(n, ParentDir)
|
||||
}
|
||||
|
||||
// RegExpGlob returns a list of file paths matching the regexp.
|
||||
// Excludes directories.
|
||||
func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
var expression = regexp.MustCompile(pattern)
|
||||
err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if expression.MatchString(path) {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Glob implements FileSystem.
|
||||
// Glob returns the list of file paths matching
|
||||
// per filepath.Match semantics, i.e. unlike RegExpGlob,
|
||||
// Match("foo/a*") will not match sub-sub directories of foo.
|
||||
// This is how /bin/ls behaves.
|
||||
func (n *fsNode) Glob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
var allFiles []string
|
||||
err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
match, err := filepath.Match(pattern, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if match {
|
||||
allFiles = append(allFiles, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsHiddenFilePath(pattern) {
|
||||
result = allFiles
|
||||
} else {
|
||||
result = RemoveHiddenFiles(allFiles)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// notExistError indicates that a file or directory does not exist.
|
||||
// Unwrapping returns os.ErrNotExist so errors.Is(err, os.ErrNotExist) works correctly.
|
||||
type notExistError string
|
||||
|
||||
func (err notExistError) Error() string { return fmt.Sprintf("'%s' doesn't exist", string(err)) }
|
||||
func (err notExistError) Unwrap() error { return os.ErrNotExist }
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
var _ FileSystem = fsOnDisk{}
|
||||
|
||||
// fsOnDisk implements FileSystem using the local filesystem.
|
||||
type fsOnDisk struct{}
|
||||
|
||||
// MakeFsOnDisk makes an instance of fsOnDisk.
|
||||
func MakeFsOnDisk() FileSystem {
|
||||
return fsOnDisk{}
|
||||
}
|
||||
|
||||
// Create delegates to os.Create.
|
||||
func (fsOnDisk) Create(name string) (File, error) { return os.Create(name) }
|
||||
|
||||
// Mkdir delegates to os.Mkdir.
|
||||
func (fsOnDisk) Mkdir(name string) error {
|
||||
return os.Mkdir(name, 0777|os.ModeDir)
|
||||
}
|
||||
|
||||
// MkdirAll delegates to os.MkdirAll.
|
||||
func (fsOnDisk) MkdirAll(name string) error {
|
||||
return os.MkdirAll(name, 0777|os.ModeDir)
|
||||
}
|
||||
|
||||
// RemoveAll delegates to os.RemoveAll.
|
||||
func (fsOnDisk) RemoveAll(name string) error {
|
||||
return os.RemoveAll(name)
|
||||
}
|
||||
|
||||
// Open delegates to os.Open.
|
||||
func (fsOnDisk) Open(name string) (File, error) { return os.Open(name) }
|
||||
|
||||
// CleanedAbs converts the given path into a
|
||||
// directory and a file name, where the directory
|
||||
// is represented as a ConfirmedDir and all that implies.
|
||||
// If the entire path is a directory, the file component
|
||||
// is an empty string.
|
||||
func (x fsOnDisk) CleanedAbs(
|
||||
path string) (ConfirmedDir, string, error) {
|
||||
absRoot, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(
|
||||
"abs path error on '%s' : %v", path, err)
|
||||
}
|
||||
deLinked, err := filepath.EvalSymlinks(absRoot)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(
|
||||
"evalsymlink failure on '%s' : %w", path, err)
|
||||
}
|
||||
if x.IsDir(deLinked) {
|
||||
return ConfirmedDir(deLinked), "", nil
|
||||
}
|
||||
d := filepath.Dir(deLinked)
|
||||
if !x.IsDir(d) {
|
||||
// Programmer/assumption error.
|
||||
log.Fatalf("first part of '%s' not a directory", deLinked)
|
||||
}
|
||||
if d == deLinked {
|
||||
// Programmer/assumption error.
|
||||
log.Fatalf("d '%s' should be a subset of deLinked", d)
|
||||
}
|
||||
f := filepath.Base(deLinked)
|
||||
if filepath.Join(d, f) != deLinked {
|
||||
// Programmer/assumption error.
|
||||
log.Fatalf("these should be equal: '%s', '%s'",
|
||||
filepath.Join(d, f), deLinked)
|
||||
}
|
||||
return ConfirmedDir(d), f, nil
|
||||
}
|
||||
|
||||
// Exists returns true if os.Stat succeeds.
|
||||
func (fsOnDisk) Exists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Glob returns the list of matching files
|
||||
func (fsOnDisk) Glob(pattern string) ([]string, error) {
|
||||
var result []string
|
||||
allFilePaths, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsHiddenFilePath(pattern) {
|
||||
result = allFilePaths
|
||||
} else {
|
||||
result = RemoveHiddenFiles(allFilePaths)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// IsDir delegates to os.Stat and FileInfo.IsDir
|
||||
func (fsOnDisk) IsDir(name string) bool {
|
||||
info, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// ReadDir delegates to os.ReadDir
|
||||
func (fsOnDisk) ReadDir(name string) ([]string, error) {
|
||||
dirEntries, err := os.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]string, len(dirEntries))
|
||||
for i := range dirEntries {
|
||||
result[i] = dirEntries[i].Name()
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReadFile delegates to os.ReadFile.
|
||||
func (fsOnDisk) ReadFile(name string) ([]byte, error) {
|
||||
content, err := os.ReadFile(name)
|
||||
return content, errors.Wrap(err)
|
||||
}
|
||||
|
||||
// WriteFile delegates to os.WriteFile with read/write permissions.
|
||||
func (fsOnDisk) WriteFile(name string, c []byte) error {
|
||||
return errors.Wrap(os.WriteFile(name, c, 0666)) //nolint:gosec
|
||||
}
|
||||
|
||||
// Walk delegates to filepath.Walk.
|
||||
func (fsOnDisk) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||
return filepath.Walk(path, walkFn)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func getOSRoot() (string, error) {
|
||||
return string(filepath.Separator), nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func getOSRoot() (string, error) {
|
||||
sysDir, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.VolumeName(sysDir) + `\`, nil
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package filesys
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RootedPath returns a rooted path, e.g. "/foo/bar" as
|
||||
// opposed to "foo/bar".
|
||||
func RootedPath(elem ...string) string {
|
||||
return Separator + filepath.Join(elem...)
|
||||
}
|
||||
|
||||
// StripTrailingSeps trims trailing filepath separators from input.
|
||||
func StripTrailingSeps(s string) string {
|
||||
k := len(s)
|
||||
for k > 0 && s[k-1] == filepath.Separator {
|
||||
k--
|
||||
}
|
||||
return s[:k]
|
||||
}
|
||||
|
||||
// StripLeadingSeps trims leading filepath separators from input.
|
||||
func StripLeadingSeps(s string) string {
|
||||
k := 0
|
||||
for k < len(s) && s[k] == filepath.Separator {
|
||||
k++
|
||||
}
|
||||
return s[k:]
|
||||
}
|
||||
|
||||
// PathSplit converts a file path to a slice of string.
|
||||
// If the path is absolute (if the path has a leading slash),
|
||||
// then the first entry in the result is an empty string.
|
||||
// Desired: path == PathJoin(PathSplit(path))
|
||||
func PathSplit(incoming string) []string {
|
||||
if incoming == "" {
|
||||
return []string{}
|
||||
}
|
||||
dir, path := filepath.Split(incoming)
|
||||
if dir == string(os.PathSeparator) {
|
||||
if path == "" {
|
||||
return []string{""}
|
||||
}
|
||||
return []string{"", path}
|
||||
}
|
||||
dir = strings.TrimSuffix(dir, string(os.PathSeparator))
|
||||
if dir == "" {
|
||||
return []string{path}
|
||||
}
|
||||
return append(PathSplit(dir), path)
|
||||
}
|
||||
|
||||
// PathJoin converts a slice of string to a file path.
|
||||
// If the first entry is an empty string, then the returned
|
||||
// path is absolute (it has a leading slash).
|
||||
// Desired: path == PathJoin(PathSplit(path))
|
||||
func PathJoin(incoming []string) string {
|
||||
if len(incoming) == 0 {
|
||||
return ""
|
||||
}
|
||||
if incoming[0] == "" {
|
||||
return string(os.PathSeparator) + filepath.Join(incoming[1:]...)
|
||||
}
|
||||
return filepath.Join(incoming...)
|
||||
}
|
||||
|
||||
// InsertPathPart inserts 'part' at position 'pos' in the given filepath.
|
||||
// The first position is 0.
|
||||
//
|
||||
// E.g. if part == 'PEACH'
|
||||
//
|
||||
// OLD : NEW : POS
|
||||
// --------------------------------------------------------
|
||||
// {empty} : PEACH : irrelevant
|
||||
// / : /PEACH : irrelevant
|
||||
// pie : PEACH/pie : 0 (or negative)
|
||||
// /pie : /PEACH/pie : 0 (or negative)
|
||||
// raw : raw/PEACH : 1 (or larger)
|
||||
// /raw : /raw/PEACH : 1 (or larger)
|
||||
// a/nice/warm/pie : a/nice/warm/PEACH/pie : 3
|
||||
// /a/nice/warm/pie : /a/nice/warm/PEACH/pie : 3
|
||||
//
|
||||
// * An empty part results in no change.
|
||||
//
|
||||
// - Absolute paths get their leading '/' stripped, treated like
|
||||
// relative paths, and the leading '/' is re-added on output.
|
||||
// The meaning of pos is intentionally the same in either absolute or
|
||||
// relative paths; if it weren't, this function could convert absolute
|
||||
// paths to relative paths, which is not desirable.
|
||||
//
|
||||
// - For robustness (liberal input, conservative output) Pos values
|
||||
// that are too small (large) to index the split filepath result in a
|
||||
// prefix (postfix) rather than an error. Use extreme position values
|
||||
// to assure a prefix or postfix (e.g. 0 will always prefix, and
|
||||
// 9999 will presumably always postfix).
|
||||
func InsertPathPart(path string, pos int, part string) string {
|
||||
if part == "" {
|
||||
return path
|
||||
}
|
||||
parts := PathSplit(path)
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
} else if pos > len(parts) {
|
||||
pos = len(parts)
|
||||
}
|
||||
if len(parts) > 0 && parts[0] == "" && pos < len(parts) {
|
||||
// An empty string at 0 indicates an absolute path, and means
|
||||
// we must increment pos. This change means that a position
|
||||
// specification has the same meaning in relative and absolute paths.
|
||||
// E.g. in either the path 'a/b/c' or the path '/a/b/c',
|
||||
// 'a' is at 0, 'b' is at 1 and 'c' is at 2, and inserting at
|
||||
// zero means a new first field _without_ changing an absolute
|
||||
// path to a relative path.
|
||||
pos++
|
||||
}
|
||||
result := make([]string, len(parts)+1)
|
||||
copy(result, parts[0:pos])
|
||||
result[pos] = part
|
||||
return PathJoin(append(result, parts[pos:]...)) //nolint: makezero
|
||||
}
|
||||
|
||||
func IsHiddenFilePath(pattern string) bool {
|
||||
return strings.HasPrefix(filepath.Base(pattern), ".")
|
||||
}
|
||||
|
||||
// Removes paths containing hidden files/folders from a list of paths
|
||||
func RemoveHiddenFiles(paths []string) []string {
|
||||
if len(paths) == 0 {
|
||||
return paths
|
||||
}
|
||||
var result []string
|
||||
for _, path := range paths {
|
||||
if !IsHiddenFilePath(path) {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
runtimeexec "sigs.k8s.io/kustomize/kyaml/fn/runtime/exec"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter filters Resources using a container image.
|
||||
// The container must start a process that reads the list of
|
||||
// input Resources from stdin, reads the Configuration from the env
|
||||
// API_CONFIG, and writes the filtered Resources to stdout.
|
||||
// If there is a error or validation failure, the process must exit
|
||||
// non-zero.
|
||||
// The full set of environment variables from the parent process
|
||||
// are passed to the container.
|
||||
//
|
||||
// Function Scoping:
|
||||
// Filter applies the function only to Resources to which it is scoped.
|
||||
//
|
||||
// Resources are scoped to a function if any of the following are true:
|
||||
// - the Resource were read from the same directory as the function config
|
||||
// - the Resource were read from a subdirectory of the function config directory
|
||||
// - the function config is in a directory named "functions" and
|
||||
// they were read from a subdirectory of "functions" parent
|
||||
// - the function config doesn't have a path annotation (considered globally scoped)
|
||||
// - the Filter has GlobalScope == true
|
||||
//
|
||||
// In Scope Examples:
|
||||
//
|
||||
// Example 1: deployment.yaml and service.yaml in function.yaml scope
|
||||
// same directory as the function config directory
|
||||
// .
|
||||
// ├── function.yaml
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 2: apps/deployment.yaml and apps/service.yaml in function.yaml scope
|
||||
// subdirectory of the function config directory
|
||||
// .
|
||||
// ├── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 3: apps/deployment.yaml and apps/service.yaml in functions/function.yaml scope
|
||||
// function config is in a directory named "functions"
|
||||
// .
|
||||
// ├── functions
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Out of Scope Examples:
|
||||
//
|
||||
// Example 1: apps/deployment.yaml and apps/service.yaml NOT in stuff/function.yaml scope
|
||||
// .
|
||||
// ├── stuff
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 2: apps/deployment.yaml and apps/service.yaml NOT in stuff/functions/function.yaml scope
|
||||
// .
|
||||
// ├── stuff
|
||||
// │ └── functions
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Default Paths:
|
||||
// Resources emitted by functions will have default path applied as annotations
|
||||
// if none is present.
|
||||
// The default path will be the function-dir/ (or parent directory in the case of "functions")
|
||||
// + function-file-name/ + namespace/ + kind_name.yaml
|
||||
//
|
||||
// Example 1: Given a function in fn.yaml that produces a Deployment name foo and a Service named bar
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── fn.yaml
|
||||
// └── fn
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
//
|
||||
// Example 2: Given a function in functions/fn.yaml that produces a Deployment name foo and a Service named bar
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── functions
|
||||
// │ └── fn.yaml
|
||||
// └── fn
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
//
|
||||
// Example 3: Given a function in fn.yaml that produces a Deployment name foo, namespace baz and a Service named bar namespace baz
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── fn.yaml
|
||||
// └── fn
|
||||
// └── baz
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
type Filter struct {
|
||||
runtimeutil.ContainerSpec `json:",inline" yaml:",inline"`
|
||||
|
||||
Exec runtimeexec.Filter
|
||||
|
||||
UIDGID string
|
||||
}
|
||||
|
||||
func (c Filter) String() string {
|
||||
if c.Exec.DeferFailure {
|
||||
return fmt.Sprintf("%s deferFailure: %v", c.Image, c.Exec.DeferFailure)
|
||||
}
|
||||
return c.Image
|
||||
}
|
||||
func (c Filter) GetExit() error {
|
||||
return c.Exec.GetExit()
|
||||
}
|
||||
|
||||
func (c *Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
if err := c.setupExec(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Exec.Filter(nodes)
|
||||
}
|
||||
|
||||
func (c *Filter) setupExec() error {
|
||||
// don't init 2x
|
||||
if c.Exec.Path != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Exec.WorkingDir == "" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
c.Exec.WorkingDir = wd
|
||||
}
|
||||
|
||||
path, args := c.getCommand()
|
||||
c.Exec.Path = path
|
||||
c.Exec.Args = args
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCommand returns the command + args to run to spawn the container
|
||||
func (c *Filter) getCommand() (string, []string) {
|
||||
network := runtimeutil.NetworkNameNone
|
||||
if c.ContainerSpec.Network {
|
||||
network = runtimeutil.NetworkNameHost
|
||||
}
|
||||
// run the container using docker. this is simpler than using the docker
|
||||
// libraries, and ensures things like auth work the same as if the container
|
||||
// was run from the cli.
|
||||
args := []string{"run",
|
||||
"--rm", // delete the container afterward
|
||||
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", // attach stdin, stdout, stderr
|
||||
"--network", string(network),
|
||||
|
||||
// added security options
|
||||
"--user", c.UIDGID,
|
||||
"--security-opt=no-new-privileges", // don't allow the user to escalate privileges
|
||||
// note: don't make fs readonly because things like heredoc rely on writing tmp files
|
||||
}
|
||||
|
||||
for _, storageMount := range c.StorageMounts {
|
||||
// convert declarative relative paths to absolute (otherwise docker will throw an error)
|
||||
if !filepath.IsAbs(storageMount.Src) {
|
||||
storageMount.Src = filepath.Join(c.Exec.WorkingDir, storageMount.Src)
|
||||
}
|
||||
args = append(args, "--mount", storageMount.String())
|
||||
}
|
||||
|
||||
args = append(args, runtimeutil.NewContainerEnvFromStringSlice(c.Env).GetDockerFlags()...)
|
||||
a := append(args, c.Image) //nolint:gocritic
|
||||
return "docker", a
|
||||
}
|
||||
|
||||
// NewContainer returns a new container filter
|
||||
func NewContainer(spec runtimeutil.ContainerSpec, uidgid string) Filter {
|
||||
f := Filter{ContainerSpec: spec, UIDGID: uidgid}
|
||||
|
||||
return f
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package exec contains the exec function implementation.
|
||||
package exec
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
// Path is the path to the executable to run
|
||||
Path string `yaml:"path,omitempty"`
|
||||
|
||||
// Args are the arguments to the executable
|
||||
Args []string `yaml:"args,omitempty"`
|
||||
|
||||
// Env is exposed to the environment
|
||||
Env []string `yaml:"env,omitempty"`
|
||||
|
||||
// WorkingDir is the working directory that the executable
|
||||
// should run in
|
||||
WorkingDir string
|
||||
|
||||
runtimeutil.FunctionFilter
|
||||
}
|
||||
|
||||
func (c *Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
c.FunctionFilter.Run = c.Run
|
||||
return c.FunctionFilter.Filter(nodes)
|
||||
}
|
||||
|
||||
func (c *Filter) Run(reader io.Reader, writer io.Writer) error {
|
||||
cmd := exec.Command(c.Path, c.Args...)
|
||||
cmd.Env = append(os.Environ(), c.Env...)
|
||||
cmd.Stdin = reader
|
||||
cmd.Stdout = writer
|
||||
cmd.Stderr = os.Stderr
|
||||
if c.WorkingDir == "" {
|
||||
return errors.Errorf("no working directory set for exec function")
|
||||
}
|
||||
if !filepath.IsAbs(c.WorkingDir) {
|
||||
return errors.Errorf(
|
||||
"relative working directory %s not allowed", c.WorkingDir)
|
||||
}
|
||||
if c.WorkingDir == "/" {
|
||||
return errors.Errorf(
|
||||
"root working directory '/' not allowed")
|
||||
}
|
||||
cmd.Dir = c.WorkingDir
|
||||
return cmd.Run()
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package runtimeutil contains libraries for implementing function runtimes.
|
||||
package runtimeutil
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package runtimeutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
k8syaml "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
FunctionAnnotationKey = "config.kubernetes.io/function"
|
||||
oldFunctionAnnotationKey = "config.k8s.io/function"
|
||||
)
|
||||
|
||||
var functionAnnotationKeys = []string{FunctionAnnotationKey, oldFunctionAnnotationKey}
|
||||
|
||||
// ContainerNetworkName is a type for network name used in container
|
||||
type ContainerNetworkName string
|
||||
|
||||
const (
|
||||
NetworkNameNone ContainerNetworkName = "none"
|
||||
NetworkNameHost ContainerNetworkName = "host"
|
||||
)
|
||||
const defaultEnvValue string = "true"
|
||||
|
||||
// ContainerEnv defines the environment present in a container.
|
||||
type ContainerEnv struct {
|
||||
// EnvVars is a key-value map that will be set as env in container
|
||||
EnvVars map[string]string
|
||||
|
||||
// VarsToExport are only env key. Value will be the value in the host system
|
||||
VarsToExport []string
|
||||
}
|
||||
|
||||
// GetDockerFlags returns docker run style env flags
|
||||
func (ce *ContainerEnv) GetDockerFlags() []string {
|
||||
envs := ce.EnvVars
|
||||
if envs == nil {
|
||||
envs = make(map[string]string)
|
||||
}
|
||||
|
||||
flags := []string{}
|
||||
// return in order to keep consistent among different runs
|
||||
keys := []string{}
|
||||
for k := range envs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
flags = append(flags, "-e", key+"="+envs[key])
|
||||
}
|
||||
|
||||
for _, key := range ce.VarsToExport {
|
||||
flags = append(flags, "-e", key)
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
// AddKeyValue adds a key-value pair into the envs
|
||||
func (ce *ContainerEnv) AddKeyValue(key, value string) {
|
||||
if ce.EnvVars == nil {
|
||||
ce.EnvVars = make(map[string]string)
|
||||
}
|
||||
ce.EnvVars[key] = value
|
||||
}
|
||||
|
||||
// HasExportedKey returns true if the key is a exported key
|
||||
func (ce *ContainerEnv) HasExportedKey(key string) bool {
|
||||
for _, k := range ce.VarsToExport {
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddKey adds a key into the envs
|
||||
func (ce *ContainerEnv) AddKey(key string) {
|
||||
if !ce.HasExportedKey(key) {
|
||||
ce.VarsToExport = append(ce.VarsToExport, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Raw returns a slice of string which represents the envs.
|
||||
// Example: [foo=bar, baz]
|
||||
func (ce *ContainerEnv) Raw() []string {
|
||||
var ret []string
|
||||
for k, v := range ce.EnvVars {
|
||||
ret = append(ret, k+"="+v)
|
||||
}
|
||||
|
||||
ret = append(ret, ce.VarsToExport...)
|
||||
return ret
|
||||
}
|
||||
|
||||
// NewContainerEnv returns a pointer to a new ContainerEnv
|
||||
func NewContainerEnv() *ContainerEnv {
|
||||
var ce ContainerEnv
|
||||
ce.EnvVars = make(map[string]string)
|
||||
// default envs
|
||||
ce.EnvVars["LOG_TO_STDERR"] = defaultEnvValue
|
||||
ce.EnvVars["STRUCTURED_RESULTS"] = defaultEnvValue
|
||||
return &ce
|
||||
}
|
||||
|
||||
// NewContainerEnvFromStringSlice returns a new ContainerEnv pointer with parsing
|
||||
// input envStr. envStr example: ["foo=bar", "baz"]
|
||||
func NewContainerEnvFromStringSlice(envStr []string) *ContainerEnv {
|
||||
ce := NewContainerEnv()
|
||||
for _, e := range envStr {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
ce.AddKey(e)
|
||||
} else {
|
||||
ce.AddKeyValue(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
// FunctionSpec defines a spec for running a function
|
||||
type FunctionSpec struct {
|
||||
DeferFailure bool `json:"deferFailure,omitempty" yaml:"deferFailure,omitempty"`
|
||||
|
||||
// Container is the spec for running a function as a container
|
||||
Container ContainerSpec `json:"container,omitempty" yaml:"container,omitempty"`
|
||||
|
||||
// ExecSpec is the spec for running a function as an executable
|
||||
Exec ExecSpec `json:"exec,omitempty" yaml:"exec,omitempty"`
|
||||
}
|
||||
|
||||
type ExecSpec struct {
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||
|
||||
// Args is a slice of args that will be passed as arguments to script
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||
|
||||
// Env is a slice of env string that will be exposed to container
|
||||
Env []string `json:"envs,omitempty" yaml:"envs,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerSpec defines a spec for running a function as a container
|
||||
type ContainerSpec struct {
|
||||
// Image is the container image to run
|
||||
Image string `json:"image,omitempty" yaml:"image,omitempty"`
|
||||
|
||||
// Network defines network specific configuration
|
||||
Network bool `json:"network,omitempty" yaml:"network,omitempty"`
|
||||
|
||||
// Mounts are the storage or directories to mount into the container
|
||||
StorageMounts []StorageMount `json:"mounts,omitempty" yaml:"mounts,omitempty"`
|
||||
|
||||
// Env is a slice of env string that will be exposed to container
|
||||
Env []string `json:"envs,omitempty" yaml:"envs,omitempty"`
|
||||
}
|
||||
|
||||
// StorageMount represents a container's mounted storage option(s)
|
||||
type StorageMount struct {
|
||||
// Type of mount e.g. bind mount, local volume, etc.
|
||||
MountType string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||
|
||||
// Source for the storage to be mounted.
|
||||
// For named volumes, this is the name of the volume.
|
||||
// For anonymous volumes, this field is omitted (empty string).
|
||||
// For bind mounts, this is the path to the file or directory on the host.
|
||||
Src string `json:"src,omitempty" yaml:"src,omitempty"`
|
||||
|
||||
// The path where the file or directory is mounted in the container.
|
||||
DstPath string `json:"dst,omitempty" yaml:"dst,omitempty"`
|
||||
|
||||
// Mount in ReadWrite mode if it's explicitly configured
|
||||
// See https://docs.docker.com/storage/bind-mounts/#use-a-read-only-bind-mount
|
||||
ReadWriteMode bool `json:"rw,omitempty" yaml:"rw,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StorageMount) String() string {
|
||||
mode := ""
|
||||
if !s.ReadWriteMode {
|
||||
mode = ",readonly"
|
||||
}
|
||||
return fmt.Sprintf("type=%s,source=%s,target=%s%s", s.MountType, s.Src, s.DstPath, mode)
|
||||
}
|
||||
|
||||
// GetFunctionSpec returns the FunctionSpec for a resource. Returns
|
||||
// nil if the resource does not have a FunctionSpec.
|
||||
//
|
||||
// The FunctionSpec is read from the resource metadata.annotation
|
||||
// "config.kubernetes.io/function"
|
||||
func GetFunctionSpec(n *yaml.RNode) (*FunctionSpec, error) {
|
||||
meta, err := n.GetMeta()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ResourceMeta: %w", err)
|
||||
}
|
||||
|
||||
fn, err := getFunctionSpecFromAnnotation(n, meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fn != nil {
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// legacy function specification for backwards compatibility
|
||||
container := meta.Annotations["config.kubernetes.io/container"]
|
||||
if container != "" {
|
||||
return &FunctionSpec{Container: ContainerSpec{Image: container}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getFunctionSpecFromAnnotation parses the config function from an annotation
|
||||
// if it is found
|
||||
func getFunctionSpecFromAnnotation(n *yaml.RNode, meta yaml.ResourceMeta) (*FunctionSpec, error) {
|
||||
var fs FunctionSpec
|
||||
for _, s := range functionAnnotationKeys {
|
||||
fn := meta.Annotations[s]
|
||||
if fn != "" {
|
||||
if err := k8syaml.UnmarshalStrict([]byte(fn), &fs); err != nil {
|
||||
return nil, fmt.Errorf("%s unmarshal error: %w", s, err)
|
||||
}
|
||||
return &fs, nil
|
||||
}
|
||||
}
|
||||
n, err := n.Pipe(yaml.Lookup("metadata", "configFn"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to look up metadata.configFn: %w", err)
|
||||
}
|
||||
if yaml.IsMissingOrNull(n) {
|
||||
return nil, nil
|
||||
}
|
||||
s, err := n.String()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "configFn parse error: %v\n", err)
|
||||
return nil, fmt.Errorf("configFn parse error: %w", err)
|
||||
}
|
||||
if err := k8syaml.UnmarshalStrict([]byte(s), &fs); err != nil {
|
||||
return nil, fmt.Errorf("%s unmarshal error: %w", "configFn", err)
|
||||
}
|
||||
return &fs, nil
|
||||
}
|
||||
|
||||
func StringToStorageMount(s string) StorageMount {
|
||||
m := make(map[string]string)
|
||||
options := strings.Split(s, ",")
|
||||
for _, option := range options {
|
||||
keyVal := strings.SplitN(option, "=", 2)
|
||||
if len(keyVal) == 2 {
|
||||
m[keyVal[0]] = keyVal[1]
|
||||
}
|
||||
}
|
||||
var sm StorageMount
|
||||
for key, value := range m {
|
||||
switch {
|
||||
case key == "type":
|
||||
sm.MountType = value
|
||||
case key == "src" || key == "source":
|
||||
sm.Src = value
|
||||
case key == "dst" || key == "target":
|
||||
sm.DstPath = value
|
||||
case key == "rw" && value == "true":
|
||||
sm.ReadWriteMode = true
|
||||
}
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
// IsReconcilerFilter filters Resources based on whether or not they are Reconciler Resource.
|
||||
// Resources with an apiVersion starting with '*.gcr.io', 'gcr.io' or 'docker.io' are considered
|
||||
// Reconciler Resources.
|
||||
type IsReconcilerFilter struct {
|
||||
// ExcludeReconcilers if set to true, then Reconcilers will be excluded -- e.g.
|
||||
// Resources with a reconcile container through the apiVersion (gcr.io prefix) or
|
||||
// through the annotations
|
||||
ExcludeReconcilers bool `yaml:"excludeReconcilers,omitempty"`
|
||||
|
||||
// IncludeNonReconcilers if set to true, the NonReconciler will be included.
|
||||
IncludeNonReconcilers bool `yaml:"includeNonReconcilers,omitempty"`
|
||||
}
|
||||
|
||||
// Filter implements kio.Filter
|
||||
func (c *IsReconcilerFilter) Filter(inputs []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
var out []*yaml.RNode
|
||||
for i := range inputs {
|
||||
functionSpec, err := GetFunctionSpec(inputs[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isFnResource := functionSpec != nil
|
||||
if isFnResource && !c.ExcludeReconcilers {
|
||||
out = append(out, inputs[i])
|
||||
}
|
||||
if !isFnResource && c.IncludeNonReconcilers {
|
||||
out = append(out, inputs[i])
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package runtimeutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/comments"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/order"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// FunctionFilter wraps another filter to be invoked in the context of a function.
|
||||
// FunctionFilter manages scoping the function, deferring failures, and saving results
|
||||
// to files.
|
||||
type FunctionFilter struct {
|
||||
// Run implements the function.
|
||||
Run func(reader io.Reader, writer io.Writer) error
|
||||
|
||||
// FunctionConfig is passed to the function through ResourceList.functionConfig.
|
||||
FunctionConfig *yaml.RNode `yaml:"functionConfig,omitempty"`
|
||||
|
||||
// GlobalScope explicitly scopes the function to all input resources rather than only those
|
||||
// resources scoped to it by path.
|
||||
GlobalScope bool
|
||||
|
||||
// ResultsFile is the file to write function ResourceList.results to.
|
||||
// If unset, results will not be written.
|
||||
ResultsFile string
|
||||
|
||||
// DeferFailure will cause the Filter to return a nil error even if Run returns an error.
|
||||
// The Run error will be available through GetExit().
|
||||
DeferFailure bool
|
||||
|
||||
// results saves the results emitted from Run
|
||||
Results *yaml.RNode
|
||||
|
||||
// exit saves the error returned from Run
|
||||
exit error
|
||||
|
||||
ids map[string]*yaml.RNode
|
||||
}
|
||||
|
||||
// GetExit returns the error from Run
|
||||
func (c FunctionFilter) GetExit() error {
|
||||
return c.exit
|
||||
}
|
||||
|
||||
// functionsDirectoryName is keyword directory name for functions scoped 1 directory higher
|
||||
const functionsDirectoryName = "functions"
|
||||
|
||||
// getFunctionScope returns the path of the directory containing the function config,
|
||||
// or its parent directory if the base directory is named "functions"
|
||||
func (c *FunctionFilter) getFunctionScope() (string, error) {
|
||||
m, err := c.FunctionConfig.GetMeta()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err)
|
||||
}
|
||||
var p string
|
||||
var found bool
|
||||
p, found = m.Annotations[kioutil.PathAnnotation]
|
||||
if !found {
|
||||
p, found = m.Annotations[kioutil.LegacyPathAnnotation]
|
||||
if !found {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
functionDir := path.Clean(path.Dir(p))
|
||||
|
||||
if path.Base(functionDir) == functionsDirectoryName {
|
||||
// the scope of functions in a directory called "functions" is 1 level higher
|
||||
// this is similar to how the golang "internal" directory scoping works
|
||||
functionDir = path.Dir(functionDir)
|
||||
}
|
||||
return functionDir, nil
|
||||
}
|
||||
|
||||
// scope partitions the input nodes into 2 slices. The first slice contains only Resources
|
||||
// which are scoped under dir, and the second slice contains the Resources which are not.
|
||||
func (c *FunctionFilter) scope(dir string, nodes []*yaml.RNode) ([]*yaml.RNode, []*yaml.RNode, error) {
|
||||
// scope container filtered Resources to Resources under that directory
|
||||
var input, saved []*yaml.RNode
|
||||
if c.GlobalScope {
|
||||
return nodes, nil, nil
|
||||
}
|
||||
|
||||
// global function
|
||||
if dir == "" || dir == "." {
|
||||
return nodes, nil, nil
|
||||
}
|
||||
|
||||
// identify Resources read from directories under the function configuration
|
||||
for i := range nodes {
|
||||
m, err := nodes[i].GetMeta()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var p string
|
||||
var found bool
|
||||
p, found = m.Annotations[kioutil.PathAnnotation]
|
||||
if !found {
|
||||
p, found = m.Annotations[kioutil.LegacyPathAnnotation]
|
||||
if !found {
|
||||
// this Resource isn't scoped under the function -- don't know where it came from
|
||||
// consider it out of scope
|
||||
saved = append(saved, nodes[i])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
resourceDir := path.Clean(path.Dir(p))
|
||||
if path.Base(resourceDir) == functionsDirectoryName {
|
||||
// Functions in the `functions` directory are scoped to
|
||||
// themselves, and should see themselves as input
|
||||
resourceDir = path.Dir(resourceDir)
|
||||
}
|
||||
if !strings.HasPrefix(resourceDir, dir) {
|
||||
// this Resource doesn't fall under the function scope if it
|
||||
// isn't in a subdirectory of where the function lives
|
||||
saved = append(saved, nodes[i])
|
||||
continue
|
||||
}
|
||||
|
||||
// this input is scoped under the function
|
||||
input = append(input, nodes[i])
|
||||
}
|
||||
|
||||
return input, saved, nil
|
||||
}
|
||||
|
||||
func (c *FunctionFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
in := &bytes.Buffer{}
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
// only process Resources scoped to this function, save the others
|
||||
functionDir, err := c.getFunctionScope()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input, saved, err := c.scope(functionDir, nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set ids on each input so it is possible to copy comments from inputs back to outputs
|
||||
if err := c.setIds(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// write the input
|
||||
err = kio.ByteWriter{
|
||||
WrappingAPIVersion: kio.ResourceListAPIVersion,
|
||||
WrappingKind: kio.ResourceListKind,
|
||||
Writer: in,
|
||||
KeepReaderAnnotations: true,
|
||||
FunctionConfig: c.FunctionConfig}.Write(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// capture the command stdout for the return value
|
||||
r := &kio.ByteReader{Reader: out}
|
||||
|
||||
// don't exit immediately if the function fails -- write out the validation
|
||||
c.exit = c.Run(in, out)
|
||||
|
||||
output, err := r.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// copy the comments and sync the order of fields from the inputs to the outputs
|
||||
if err := c.copyCommentsAndSyncOrder(output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.doResults(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.exit != nil && !c.DeferFailure {
|
||||
return append(output, saved...), c.exit
|
||||
}
|
||||
|
||||
// annotate any generated Resources with a path and index if they don't already have one
|
||||
if err := kioutil.DefaultPathAnnotation(functionDir, output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// emit both the Resources output from the function, and the out-of-scope Resources
|
||||
// which were not provided to the function
|
||||
return append(output, saved...), nil
|
||||
}
|
||||
|
||||
func (c *FunctionFilter) setIds(nodes []*yaml.RNode) error {
|
||||
// set the id on each node to map inputs to outputs
|
||||
var id int
|
||||
c.ids = map[string]*yaml.RNode{}
|
||||
for i := range nodes {
|
||||
id++
|
||||
idStr := fmt.Sprintf("%v", id)
|
||||
err := nodes[i].PipeE(yaml.SetAnnotation(kioutil.IdAnnotation, idStr))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
err = nodes[i].PipeE(yaml.SetAnnotation(kioutil.LegacyIdAnnotation, idStr))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
c.ids[idStr] = nodes[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FunctionFilter) copyCommentsAndSyncOrder(nodes []*yaml.RNode) error {
|
||||
for i := range nodes {
|
||||
node := nodes[i]
|
||||
anID, err := node.Pipe(yaml.GetAnnotation(kioutil.IdAnnotation))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if anID == nil {
|
||||
anID, err = node.Pipe(yaml.GetAnnotation(kioutil.LegacyIdAnnotation))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if anID == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var in *yaml.RNode
|
||||
var found bool
|
||||
if in, found = c.ids[anID.YNode().Value]; !found {
|
||||
continue
|
||||
}
|
||||
if err := comments.CopyComments(in, node); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if err := order.SyncOrder(in, node); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if err := node.PipeE(yaml.ClearAnnotation(kioutil.IdAnnotation)); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if err := node.PipeE(yaml.ClearAnnotation(kioutil.LegacyIdAnnotation)); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FunctionFilter) doResults(r *kio.ByteReader) error {
|
||||
// Write the results to a file if configured to do so
|
||||
if c.ResultsFile != "" && r.Results != nil {
|
||||
results, err := r.Results.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(c.ResultsFile, []byte(results), 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if r.Results != nil {
|
||||
c.Results = r.Results
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package runtimeutil
|
||||
|
||||
type DeferFailureFunction interface {
|
||||
GetExit() error
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceListKind = "ResourceList"
|
||||
ResourceListAPIVersion = "config.kubernetes.io/v1"
|
||||
)
|
||||
|
||||
// ByteReadWriter reads from an input and writes to an output.
|
||||
type ByteReadWriter struct {
|
||||
// Reader is where ResourceNodes are decoded from.
|
||||
Reader io.Reader
|
||||
|
||||
// Writer is where ResourceNodes are encoded.
|
||||
Writer io.Writer
|
||||
|
||||
// OmitReaderAnnotations will configures Read to skip setting the config.kubernetes.io/index
|
||||
// annotation on Resources as they are Read.
|
||||
OmitReaderAnnotations bool
|
||||
|
||||
// KeepReaderAnnotations if set will keep the Reader specific annotations when writing
|
||||
// the Resources, otherwise they will be cleared.
|
||||
KeepReaderAnnotations bool
|
||||
|
||||
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
|
||||
PreserveSeqIndent bool
|
||||
|
||||
// Style is a style that is set on the Resource Node Document.
|
||||
Style yaml.Style
|
||||
|
||||
// WrapBareSeqNode wraps the bare sequence node document with map node,
|
||||
// kyaml uses reader annotations to track resources, it is not possible to
|
||||
// add them to bare sequence nodes, this option enables wrapping such bare
|
||||
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
|
||||
// note that this wrapping is different and not related to ResourceList wrapping
|
||||
WrapBareSeqNode bool
|
||||
|
||||
FunctionConfig *yaml.RNode
|
||||
|
||||
Results *yaml.RNode
|
||||
|
||||
NoWrap bool
|
||||
WrappingAPIVersion string
|
||||
WrappingKind string
|
||||
}
|
||||
|
||||
func (rw *ByteReadWriter) Read() ([]*yaml.RNode, error) {
|
||||
b := &ByteReader{
|
||||
Reader: rw.Reader,
|
||||
OmitReaderAnnotations: rw.OmitReaderAnnotations,
|
||||
PreserveSeqIndent: rw.PreserveSeqIndent,
|
||||
WrapBareSeqNode: rw.WrapBareSeqNode,
|
||||
}
|
||||
val, err := b.Read()
|
||||
rw.Results = b.Results
|
||||
|
||||
if rw.FunctionConfig == nil {
|
||||
rw.FunctionConfig = b.FunctionConfig
|
||||
}
|
||||
if !rw.NoWrap && rw.WrappingKind == "" {
|
||||
rw.WrappingAPIVersion = b.WrappingAPIVersion
|
||||
rw.WrappingKind = b.WrappingKind
|
||||
}
|
||||
return val, errors.Wrap(err)
|
||||
}
|
||||
|
||||
func (rw *ByteReadWriter) Write(nodes []*yaml.RNode) error {
|
||||
w := ByteWriter{
|
||||
Writer: rw.Writer,
|
||||
KeepReaderAnnotations: rw.KeepReaderAnnotations,
|
||||
Style: rw.Style,
|
||||
FunctionConfig: rw.FunctionConfig,
|
||||
Results: rw.Results,
|
||||
}
|
||||
if !rw.NoWrap {
|
||||
w.WrappingAPIVersion = rw.WrappingAPIVersion
|
||||
w.WrappingKind = rw.WrappingKind
|
||||
}
|
||||
return w.Write(nodes)
|
||||
}
|
||||
|
||||
// ParseAll reads all of the inputs into resources
|
||||
func ParseAll(inputs ...string) ([]*yaml.RNode, error) {
|
||||
return (&ByteReader{
|
||||
Reader: bytes.NewBufferString(strings.Join(inputs, "\n---\n")),
|
||||
}).Read()
|
||||
}
|
||||
|
||||
// FromBytes reads from a byte slice.
|
||||
func FromBytes(bs []byte) ([]*yaml.RNode, error) {
|
||||
return (&ByteReader{
|
||||
OmitReaderAnnotations: true,
|
||||
AnchorsAweigh: true,
|
||||
Reader: bytes.NewBuffer(bs),
|
||||
}).Read()
|
||||
}
|
||||
|
||||
// StringAll writes all of the resources to a string
|
||||
func StringAll(resources []*yaml.RNode) (string, error) {
|
||||
var b bytes.Buffer
|
||||
err := (&ByteWriter{Writer: &b}).Write(resources)
|
||||
return b.String(), err
|
||||
}
|
||||
|
||||
// ByteReader decodes ResourceNodes from bytes.
|
||||
// By default, Read will set the config.kubernetes.io/index annotation on each RNode as it
|
||||
// is read so they can be written back in the same order.
|
||||
type ByteReader struct {
|
||||
// Reader is where ResourceNodes are decoded from.
|
||||
Reader io.Reader
|
||||
|
||||
// OmitReaderAnnotations will configures Read to skip setting the config.kubernetes.io/index
|
||||
// and internal.config.kubernetes.io/seqindent annotations on Resources as they are Read.
|
||||
OmitReaderAnnotations bool
|
||||
|
||||
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
|
||||
PreserveSeqIndent bool
|
||||
|
||||
// SetAnnotations is a map of caller specified annotations to set on resources as they are read
|
||||
// These are independent of the annotations controlled by OmitReaderAnnotations
|
||||
SetAnnotations map[string]string
|
||||
|
||||
FunctionConfig *yaml.RNode
|
||||
|
||||
Results *yaml.RNode
|
||||
|
||||
// DisableUnwrapping prevents Resources in Lists and ResourceLists from being unwrapped
|
||||
DisableUnwrapping bool
|
||||
|
||||
// WrappingAPIVersion is set by Read(), and is the apiVersion of the object that
|
||||
// the read objects were originally wrapped in.
|
||||
WrappingAPIVersion string
|
||||
|
||||
// WrappingKind is set by Read(), and is the kind of the object that
|
||||
// the read objects were originally wrapped in.
|
||||
WrappingKind string
|
||||
|
||||
// WrapBareSeqNode wraps the bare sequence node document with map node,
|
||||
// kyaml uses reader annotations to track resources, it is not possible to
|
||||
// add them to bare sequence nodes, this option enables wrapping such bare
|
||||
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
|
||||
// note that this wrapping is different and not related to ResourceList wrapping
|
||||
WrapBareSeqNode bool
|
||||
|
||||
// AnchorsAweigh set to true attempts to replace all YAML anchor aliases
|
||||
// with their definitions (anchor values) immediately after the read.
|
||||
AnchorsAweigh bool
|
||||
}
|
||||
|
||||
var _ Reader = &ByteReader{}
|
||||
|
||||
// splitDocuments returns a slice of all documents contained in a YAML string. Multiple documents can be divided by the
|
||||
// YAML document separator (---). It allows for white space and comments to be after the separator on the same line,
|
||||
// but will return an error if anything else is on the line.
|
||||
func splitDocuments(s string) ([]string, error) {
|
||||
docs := make([]string, 0)
|
||||
if len(s) > 0 {
|
||||
// The YAML document separator is any line that starts with ---
|
||||
yamlSeparatorRegexp := regexp.MustCompile(`\n---.*\n`)
|
||||
|
||||
// Find all separators, check them for invalid content, and append each document to docs
|
||||
separatorLocations := yamlSeparatorRegexp.FindAllStringIndex(s, -1)
|
||||
prev := 0
|
||||
for i := range separatorLocations {
|
||||
loc := separatorLocations[i]
|
||||
separator := s[loc[0]:loc[1]]
|
||||
|
||||
// If the next non-whitespace character on the line following the separator is not a comment, return an error
|
||||
trimmedContentAfterSeparator := strings.TrimSpace(separator[4:])
|
||||
if len(trimmedContentAfterSeparator) > 0 && trimmedContentAfterSeparator[0] != '#' {
|
||||
return nil, errors.Errorf("invalid document separator: %s", strings.TrimSpace(separator))
|
||||
}
|
||||
|
||||
docs = append(docs, s[prev:loc[0]])
|
||||
prev = loc[1]
|
||||
}
|
||||
docs = append(docs, s[prev:])
|
||||
}
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func (r *ByteReader) Read() ([]*yaml.RNode, error) {
|
||||
if r.PreserveSeqIndent && r.OmitReaderAnnotations {
|
||||
return nil, errors.Errorf(`"PreserveSeqIndent" option adds a reader annotation, please set "OmitReaderAnnotations" to false`)
|
||||
}
|
||||
|
||||
output := ResourceNodeSlice{}
|
||||
|
||||
// by manually splitting resources -- otherwise the decoder will get the Resource
|
||||
// boundaries wrong for header comments.
|
||||
input := &bytes.Buffer{}
|
||||
_, err := io.Copy(input, r.Reader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
|
||||
// Replace the ending \r\n (line ending used in windows) with \n and then split it into multiple YAML documents
|
||||
// if it contains document separators (---)
|
||||
values, err := splitDocuments(strings.ReplaceAll(input.String(), "\r\n", "\n"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
|
||||
index := 0
|
||||
for i := range values {
|
||||
// the Split used above will eat the tail '\n' from each resource. This may affect the
|
||||
// literal string value since '\n' is meaningful in it.
|
||||
if i != len(values)-1 {
|
||||
values[i] += "\n"
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewBufferString(values[i]))
|
||||
node, err := r.decode(values[i], index, decoder)
|
||||
if err == io.EOF {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if yaml.IsMissingOrNull(node) {
|
||||
// empty value
|
||||
continue
|
||||
}
|
||||
|
||||
// ok if no metadata -- assume not an InputList
|
||||
meta, err := node.GetMeta()
|
||||
if err != yaml.ErrMissingMetadata && err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "[%d]", i)
|
||||
}
|
||||
|
||||
// the elements are wrapped in an InputList, unwrap them
|
||||
// don't check apiVersion, we haven't standardized on the domain
|
||||
if !r.DisableUnwrapping &&
|
||||
len(values) == 1 && // Only unwrap if there is only 1 value
|
||||
(meta.Kind == ResourceListKind || meta.Kind == "List") &&
|
||||
(node.Field("items") != nil || node.Field("functionConfig") != nil) {
|
||||
r.WrappingKind = meta.Kind
|
||||
r.WrappingAPIVersion = meta.APIVersion
|
||||
|
||||
// unwrap the list
|
||||
if fc := node.Field("functionConfig"); fc != nil {
|
||||
r.FunctionConfig = fc.Value
|
||||
}
|
||||
if res := node.Field("results"); res != nil {
|
||||
r.Results = res.Value
|
||||
}
|
||||
|
||||
items := node.Field("items")
|
||||
if items != nil {
|
||||
for i := range items.Value.Content() {
|
||||
// add items
|
||||
output = append(output, yaml.NewRNode(items.Value.Content()[i]))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// add the node to the list
|
||||
output = append(output, node)
|
||||
|
||||
// increment the index annotation value
|
||||
index++
|
||||
}
|
||||
if r.AnchorsAweigh {
|
||||
for _, n := range output {
|
||||
if err = n.DeAnchor(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (r *ByteReader) decode(originalYAML string, index int, decoder *yaml.Decoder) (*yaml.RNode, error) {
|
||||
node := &yaml.Node{}
|
||||
err := decoder.Decode(node)
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "MalformedYAMLError")
|
||||
}
|
||||
|
||||
if yaml.IsYNodeEmptyDoc(node) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// set annotations on the read Resources
|
||||
// sort the annotations by key so the output Resources is consistent (otherwise the
|
||||
// annotations will be in a random order)
|
||||
n := yaml.NewRNode(node)
|
||||
// check if it is a bare sequence node and wrap it with a yaml.BareSeqNodeWrappingKey
|
||||
if r.WrapBareSeqNode && node.Kind == yaml.DocumentNode && len(node.Content) > 0 &&
|
||||
node.Content[0] != nil && node.Content[0].Kind == yaml.SequenceNode {
|
||||
wrappedNode := yaml.NewRNode(&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
})
|
||||
wrappedNode.PipeE(yaml.SetField(yaml.BareSeqNodeWrappingKey, n))
|
||||
n = wrappedNode
|
||||
}
|
||||
|
||||
if r.SetAnnotations == nil {
|
||||
r.SetAnnotations = map[string]string{}
|
||||
}
|
||||
if !r.OmitReaderAnnotations {
|
||||
err := kioutil.CopyLegacyAnnotations(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.SetAnnotations[kioutil.IndexAnnotation] = fmt.Sprintf("%d", index)
|
||||
r.SetAnnotations[kioutil.LegacyIndexAnnotation] = fmt.Sprintf("%d", index)
|
||||
|
||||
if r.PreserveSeqIndent {
|
||||
// derive and add the seqindent annotation
|
||||
seqIndentStyle := yaml.DeriveSeqIndentStyle(originalYAML)
|
||||
if seqIndentStyle != "" {
|
||||
r.SetAnnotations[kioutil.SeqIndentAnnotation] = seqIndentStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
var keys []string
|
||||
for k := range r.SetAnnotations {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
_, err = n.Pipe(yaml.SetAnnotation(k, r.SetAnnotations[k]))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// ByteWriter writes ResourceNodes to bytes. Generally YAML encoding will be used but in the special
|
||||
// case of writing a single, bare yaml.RNode that has a kioutil.PathAnnotation indicating that the
|
||||
// target is a JSON file JSON encoding is used. See shouldJSONEncodeSingleBareNode below for more
|
||||
// information.
|
||||
type ByteWriter struct {
|
||||
// Writer is where ResourceNodes are encoded.
|
||||
Writer io.Writer
|
||||
|
||||
// KeepReaderAnnotations if set will keep the Reader specific annotations when writing
|
||||
// the Resources, otherwise they will be cleared.
|
||||
KeepReaderAnnotations bool
|
||||
|
||||
// ClearAnnotations is a list of annotations to clear when writing the Resources.
|
||||
ClearAnnotations []string
|
||||
|
||||
// Style is a style that is set on the Resource Node Document.
|
||||
Style yaml.Style
|
||||
|
||||
// FunctionConfig is the function config for an ResourceList. If non-nil
|
||||
// wrap the results in an ResourceList.
|
||||
FunctionConfig *yaml.RNode
|
||||
|
||||
Results *yaml.RNode
|
||||
|
||||
// WrappingKind if set will cause ByteWriter to wrap the Resources in
|
||||
// an 'items' field in this kind. e.g. if WrappingKind is 'List',
|
||||
// ByteWriter will wrap the Resources in a List .items field.
|
||||
WrappingKind string
|
||||
|
||||
// WrappingAPIVersion is the apiVersion for WrappingKind
|
||||
WrappingAPIVersion string
|
||||
|
||||
// Sort if set, will cause ByteWriter to sort the nodes before writing them.
|
||||
Sort bool
|
||||
}
|
||||
|
||||
var _ Writer = ByteWriter{}
|
||||
|
||||
func (w ByteWriter) Write(inputNodes []*yaml.RNode) error {
|
||||
// Copy the nodes to prevent writer from mutating the original nodes.
|
||||
nodes := copyRNodes(inputNodes)
|
||||
if w.Sort {
|
||||
if err := kioutil.SortNodes(nodes); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Even though we use the this value further down we must check this before removing annotations
|
||||
jsonEncodeSingleBareNode := w.shouldJSONEncodeSingleBareNode(nodes)
|
||||
|
||||
// store seqindent annotation value for each node in order to set the encoder indentation
|
||||
var seqIndentsForNodes []string
|
||||
for i := range nodes {
|
||||
seqIndentsForNodes = append(seqIndentsForNodes, nodes[i].GetAnnotations()[kioutil.SeqIndentAnnotation])
|
||||
}
|
||||
|
||||
for i := range nodes {
|
||||
// clean resources by removing annotations set by the Reader
|
||||
if !w.KeepReaderAnnotations {
|
||||
_, err := nodes[i].Pipe(yaml.ClearAnnotation(kioutil.IndexAnnotation))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
_, err = nodes[i].Pipe(yaml.ClearAnnotation(kioutil.LegacyIndexAnnotation))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
_, err = nodes[i].Pipe(yaml.ClearAnnotation(kioutil.SeqIndentAnnotation))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
for _, a := range w.ClearAnnotations {
|
||||
_, err := nodes[i].Pipe(yaml.ClearAnnotation(a))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := yaml.ClearEmptyAnnotations(nodes[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.Style != 0 {
|
||||
nodes[i].YNode().Style = w.Style
|
||||
}
|
||||
}
|
||||
|
||||
if jsonEncodeSingleBareNode {
|
||||
encoder := json.NewEncoder(w.Writer)
|
||||
encoder.SetIndent("", " ")
|
||||
return errors.Wrap(encoder.Encode(nodes[0]))
|
||||
}
|
||||
|
||||
encoder := yaml.NewEncoder(w.Writer)
|
||||
defer encoder.Close()
|
||||
// don't wrap the elements
|
||||
if w.WrappingKind == "" {
|
||||
for i := range nodes {
|
||||
if seqIndentsForNodes[i] == string(yaml.WideSequenceStyle) {
|
||||
encoder.DefaultSeqIndent()
|
||||
} else {
|
||||
encoder.CompactSeqIndent()
|
||||
}
|
||||
if err := encoder.Encode(upWrapBareSequenceNode(nodes[i].Document())); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// wrap the elements in a list
|
||||
items := &yaml.Node{Kind: yaml.SequenceNode}
|
||||
list := &yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Style: w.Style,
|
||||
Content: []*yaml.Node{
|
||||
{Kind: yaml.ScalarNode, Value: "apiVersion"},
|
||||
{Kind: yaml.ScalarNode, Value: w.WrappingAPIVersion},
|
||||
{Kind: yaml.ScalarNode, Value: "kind"},
|
||||
{Kind: yaml.ScalarNode, Value: w.WrappingKind},
|
||||
{Kind: yaml.ScalarNode, Value: "items"}, items,
|
||||
}}
|
||||
if w.FunctionConfig != nil {
|
||||
list.Content = append(list.Content,
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Value: "functionConfig"},
|
||||
w.FunctionConfig.YNode())
|
||||
}
|
||||
if w.Results != nil {
|
||||
list.Content = append(list.Content,
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Value: "results"},
|
||||
w.Results.YNode())
|
||||
}
|
||||
doc := &yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{list}}
|
||||
for i := range nodes {
|
||||
items.Content = append(items.Content, nodes[i].YNode())
|
||||
}
|
||||
return encoder.Encode(doc)
|
||||
}
|
||||
|
||||
func copyRNodes(in []*yaml.RNode) []*yaml.RNode {
|
||||
out := make([]*yaml.RNode, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i].Copy()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// shouldJSONEncodeSingleBareNode determines if nodes contain a single node that should not be
|
||||
// wrapped and has a JSON file extension, which in turn means that the node should be JSON encoded.
|
||||
// Note 1: this must be checked before any annotations to avoid losing information about the target
|
||||
// filename extension.
|
||||
// Note 2: JSON encoding should only be used for single, unwrapped nodes because multiple unwrapped
|
||||
// nodes cannot be represented in JSON (no multi doc support). Furthermore, the typical use
|
||||
// cases for wrapping nodes would likely not include later writing the whole wrapper to a
|
||||
// .json file, i.e. there is no point risking any edge case information loss e.g. comments
|
||||
// disappearing, that could come from JSON encoding the whole wrapper just to ensure that
|
||||
// one (or all nodes) can be read as JSON.
|
||||
func (w ByteWriter) shouldJSONEncodeSingleBareNode(nodes []*yaml.RNode) bool {
|
||||
if w.WrappingKind == "" && len(nodes) == 1 {
|
||||
if path, _, _ := kioutil.GetFileAnnotations(nodes[0]); path != "" {
|
||||
filename := filepath.Base(path)
|
||||
for _, glob := range JSONMatch {
|
||||
if match, _ := filepath.Match(glob, filename); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// upWrapBareSequenceNode unwraps the bare sequence nodes wrapped by yaml.BareSeqNodeWrappingKey
|
||||
func upWrapBareSequenceNode(node *yaml.Node) *yaml.Node {
|
||||
rNode := yaml.NewRNode(node)
|
||||
seqNode, err := rNode.Pipe(yaml.Lookup(yaml.BareSeqNodeWrappingKey))
|
||||
if err == nil && !seqNode.IsNilOrEmpty() {
|
||||
return seqNode.YNode()
|
||||
}
|
||||
return node
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kio contains libraries for reading and writing collections of Resources.
|
||||
//
|
||||
// Reading Resources
|
||||
//
|
||||
// Resources are Read using a kio.Reader function. Examples:
|
||||
// [kio.LocalPackageReader{}, kio.ByteReader{}]
|
||||
//
|
||||
// Resources read using a LocalPackageReader will have annotations applied so they can be
|
||||
// written back to the files they were read from.
|
||||
//
|
||||
// Modifying Resources
|
||||
//
|
||||
// Resources are modified using a kio.Filter. The kio.Filter accepts a collection of
|
||||
// Resources as input, and returns a new collection as output.
|
||||
// It is recommended to use the yaml package for manipulating individual Resources in
|
||||
// the collection.
|
||||
//
|
||||
// Writing Resources
|
||||
//
|
||||
// Resources are Read using a kio.Reader function. Examples:
|
||||
// [kio.LocalPackageWriter{}, kio.ByteWriter{}]
|
||||
//
|
||||
// ReadWriters
|
||||
//
|
||||
// It is preferred to use a ReadWriter when reading and writing from / to the same source.
|
||||
//
|
||||
// Building Pipelines
|
||||
//
|
||||
// The preferred way to transforms a collection of Resources is to use kio.Pipeline to Read,
|
||||
// Modify and Write the collection of Resources. Pipeline will automatically sequentially
|
||||
// invoke the Read, Modify, Write steps, returning and error immediately on any failure.
|
||||
package kio
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gitignore "github.com/monochromegane/go-gitignore"
|
||||
"sigs.k8s.io/kustomize/kyaml/ext"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
// ignoreFilesMatcher handles `.krmignore` files, which allows for ignoring
|
||||
// files or folders in a package. The format of this file is a subset of the
|
||||
// gitignore format, with recursive patterns (like a/**/c) not supported. If a
|
||||
// file or folder matches any of the patterns in the .krmignore file for the
|
||||
// package, it will be excluded.
|
||||
//
|
||||
// It works as follows:
|
||||
//
|
||||
// * It will look for .krmignore file in the top folder and on the top level
|
||||
// of any subpackages. Subpackages are defined by the presence of a Krmfile
|
||||
// in the folder.
|
||||
// * `.krmignore` files only cover files and folders for the package in which
|
||||
// it is defined. So ignore patterns defined in a parent package does not
|
||||
// affect which files are ignored from a subpackage.
|
||||
// * An ignore pattern can not ignore a subpackage. So even if the parent
|
||||
// package contains a pattern that ignores the directory foo, if foo is a
|
||||
// subpackage, it will still be included if the IncludeSubpackages property
|
||||
// is set to true
|
||||
type ignoreFilesMatcher struct {
|
||||
matchers []matcher
|
||||
fs filesys.FileSystemOrOnDisk
|
||||
}
|
||||
|
||||
// readIgnoreFile checks whether there is a .krmignore file in the path, and
|
||||
// if it is, reads it in and turns it into a matcher. If we can't find a file,
|
||||
// we just add a matcher that match nothing.
|
||||
func (i *ignoreFilesMatcher) readIgnoreFile(path string) error {
|
||||
i.verifyPath(path)
|
||||
f, err := i.fs.Open(filepath.Join(path, ext.IgnoreFileName()))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
i.matchers = append(i.matchers, matcher{
|
||||
matcher: gitignore.DummyIgnoreMatcher(false),
|
||||
basePath: path,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
i.matchers = append(i.matchers, matcher{
|
||||
matcher: gitignore.NewGitIgnoreFromReader(path, f),
|
||||
basePath: path,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyPath checks whether the top matcher on the stack
|
||||
// is correct for the provided filepath. Matchers are removed once
|
||||
// we encounter a filepath that is not a subpath of the basepath for
|
||||
// the matcher.
|
||||
func (i *ignoreFilesMatcher) verifyPath(path string) {
|
||||
for j := len(i.matchers) - 1; j >= 0; j-- {
|
||||
matcher := i.matchers[j]
|
||||
if strings.HasPrefix(path, matcher.basePath) || path == matcher.basePath {
|
||||
i.matchers = i.matchers[:j+1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// matchFile checks whether the file given by the provided path matches
|
||||
// any of the patterns in the .krmignore file for the package.
|
||||
func (i *ignoreFilesMatcher) matchFile(path string) bool {
|
||||
if len(i.matchers) == 0 {
|
||||
return false
|
||||
}
|
||||
i.verifyPath(filepath.Dir(path))
|
||||
return i.matchers[len(i.matchers)-1].matcher.Match(path, false)
|
||||
}
|
||||
|
||||
// matchDir checks whether the directory given by the provided path matches
|
||||
// any of the patterns in the .krmignore file for the package.
|
||||
func (i *ignoreFilesMatcher) matchDir(path string) bool {
|
||||
if len(i.matchers) == 0 {
|
||||
return false
|
||||
}
|
||||
i.verifyPath(path)
|
||||
return i.matchers[len(i.matchers)-1].matcher.Match(path, true)
|
||||
}
|
||||
|
||||
// matcher wraps the gitignore matcher and the path to the folder
|
||||
// where the file was found.
|
||||
type matcher struct {
|
||||
matcher gitignore.IgnoreMatcher
|
||||
|
||||
basePath string
|
||||
}
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package kio contains low-level libraries for reading, modifying and writing
|
||||
// Resource Configuration and packages.
|
||||
package kio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Reader reads ResourceNodes. Analogous to io.Reader.
|
||||
type Reader interface {
|
||||
Read() ([]*yaml.RNode, error)
|
||||
}
|
||||
|
||||
// ResourceNodeSlice is a collection of ResourceNodes.
|
||||
// While ResourceNodeSlice has no inherent constraints on ordering or uniqueness, specific
|
||||
// Readers, Filters or Writers may have constraints.
|
||||
type ResourceNodeSlice []*yaml.RNode
|
||||
|
||||
var _ Reader = ResourceNodeSlice{}
|
||||
|
||||
func (o ResourceNodeSlice) Read() ([]*yaml.RNode, error) {
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Writer writes ResourceNodes. Analogous to io.Writer.
|
||||
type Writer interface {
|
||||
Write([]*yaml.RNode) error
|
||||
}
|
||||
|
||||
// WriterFunc implements a Writer as a function.
|
||||
type WriterFunc func([]*yaml.RNode) error
|
||||
|
||||
func (fn WriterFunc) Write(o []*yaml.RNode) error {
|
||||
return fn(o)
|
||||
}
|
||||
|
||||
// ReaderWriter implements both Reader and Writer interfaces
|
||||
type ReaderWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
|
||||
// Filter modifies a collection of Resource Configuration by returning the modified slice.
|
||||
// When possible, Filters should be serializable to yaml so that they can be described
|
||||
// as either data or code.
|
||||
//
|
||||
// Analogous to http://www.linfo.org/filters.html
|
||||
type Filter interface {
|
||||
Filter([]*yaml.RNode) ([]*yaml.RNode, error)
|
||||
}
|
||||
|
||||
// TrackableFilter is an extension of Filter which is also capable of tracking
|
||||
// which fields were mutated by the filter.
|
||||
type TrackableFilter interface {
|
||||
Filter
|
||||
WithMutationTracker(func(key, value, tag string, node *yaml.RNode))
|
||||
}
|
||||
|
||||
// FilterFunc implements a Filter as a function.
|
||||
type FilterFunc func([]*yaml.RNode) ([]*yaml.RNode, error)
|
||||
|
||||
func (fn FilterFunc) Filter(o []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return fn(o)
|
||||
}
|
||||
|
||||
// Pipeline reads Resource Configuration from a set of Inputs, applies some
|
||||
// transformation filters, and writes the results to a set of Outputs.
|
||||
//
|
||||
// Analogous to http://www.linfo.org/pipes.html
|
||||
type Pipeline struct {
|
||||
// Inputs provide sources for Resource Configuration to be read.
|
||||
Inputs []Reader `yaml:"inputs,omitempty"`
|
||||
|
||||
// Filters are transformations applied to the Resource Configuration.
|
||||
// They are applied in the order they are specified.
|
||||
// Analogous to http://www.linfo.org/filters.html
|
||||
Filters []Filter `yaml:"filters,omitempty"`
|
||||
|
||||
// Outputs are where the transformed Resource Configuration is written.
|
||||
Outputs []Writer `yaml:"outputs,omitempty"`
|
||||
|
||||
// ContinueOnEmptyResult configures what happens when a filter in the pipeline
|
||||
// returns an empty result.
|
||||
// If it is false (default), subsequent filters will be skipped and the result
|
||||
// will be returned immediately. This is useful as an optimization when you
|
||||
// know that subsequent filters will not alter the empty result.
|
||||
// If it is true, the empty result will be provided as input to the next
|
||||
// filter in the list. This is useful when subsequent functions in the
|
||||
// pipeline may generate new resources.
|
||||
ContinueOnEmptyResult bool `yaml:"continueOnEmptyResult,omitempty"`
|
||||
}
|
||||
|
||||
// Execute executes each step in the sequence, returning immediately after encountering
|
||||
// any error as part of the Pipeline.
|
||||
func (p Pipeline) Execute() error {
|
||||
return p.ExecuteWithCallback(nil)
|
||||
}
|
||||
|
||||
// PipelineExecuteCallbackFunc defines a callback function that will be called each time a step in the pipeline succeeds.
|
||||
type PipelineExecuteCallbackFunc = func(op Filter)
|
||||
|
||||
// ExecuteWithCallback executes each step in the sequence, returning immediately after encountering
|
||||
// any error as part of the Pipeline. The callback will be called each time a step succeeds.
|
||||
func (p Pipeline) ExecuteWithCallback(callback PipelineExecuteCallbackFunc) error {
|
||||
var result []*yaml.RNode
|
||||
|
||||
// read from the inputs
|
||||
for _, i := range p.Inputs {
|
||||
nodes, err := i.Read()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
result = append(result, nodes...)
|
||||
}
|
||||
|
||||
// apply operations
|
||||
for i := range p.Filters {
|
||||
// Not all RNodes passed through kio.Pipeline have metadata nor should
|
||||
// they all be required to.
|
||||
nodeAnnos, err := PreprocessResourcesForInternalAnnotationMigration(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op := p.Filters[i]
|
||||
if callback != nil {
|
||||
callback(op)
|
||||
}
|
||||
result, err = op.Filter(result)
|
||||
// TODO (issue 2872): This len(result) == 0 should be removed and empty result list should be
|
||||
// handled by outputs. However currently some writer like LocalPackageReadWriter
|
||||
// will clear the output directory and which will cause unpredictable results
|
||||
if len(result) == 0 && !p.ContinueOnEmptyResult || err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// If either the internal annotations for path, index, and id OR the legacy
|
||||
// annotations for path, index, and id are changed, we have to update the other.
|
||||
err = ReconcileInternalAnnotations(result, nodeAnnos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// write to the outputs
|
||||
for _, o := range p.Outputs {
|
||||
if err := o.Write(result); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterAll runs the yaml.Filter against all inputs
|
||||
func FilterAll(filter yaml.Filter) Filter {
|
||||
return FilterFunc(func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
for i := range nodes {
|
||||
_, err := filter.Filter(nodes[i])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
})
|
||||
}
|
||||
|
||||
// PreprocessResourcesForInternalAnnotationMigration returns a mapping from id to all
|
||||
// internal annotations, so that we can use it to reconcile the annotations
|
||||
// later. This is necessary because currently both internal-prefixed annotations
|
||||
// and legacy annotations are currently supported, and a change to one must be
|
||||
// reflected in the other if needed.
|
||||
func PreprocessResourcesForInternalAnnotationMigration(result []*yaml.RNode) (map[string]map[string]string, error) {
|
||||
idToAnnosMap := make(map[string]map[string]string)
|
||||
for i := range result {
|
||||
idStr := strconv.Itoa(i)
|
||||
err := result[i].PipeE(yaml.SetAnnotation(kioutil.InternalAnnotationsMigrationResourceIDAnnotation, idStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idToAnnosMap[idStr] = kioutil.GetInternalAnnotations(result[i])
|
||||
if err = kioutil.CopyLegacyAnnotations(result[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, _ := result[i].GetMeta()
|
||||
if err = checkMismatchedAnnos(meta.Annotations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return idToAnnosMap, nil
|
||||
}
|
||||
|
||||
func checkMismatchedAnnos(annotations map[string]string) error {
|
||||
path := annotations[kioutil.PathAnnotation]
|
||||
index := annotations[kioutil.IndexAnnotation]
|
||||
id := annotations[kioutil.IdAnnotation]
|
||||
|
||||
legacyPath := annotations[kioutil.LegacyPathAnnotation]
|
||||
legacyIndex := annotations[kioutil.LegacyIndexAnnotation]
|
||||
legacyId := annotations[kioutil.LegacyIdAnnotation]
|
||||
|
||||
// if prior to running the functions, the legacy and internal annotations differ,
|
||||
// throw an error as we cannot infer the user's intent.
|
||||
if path != "" && legacyPath != "" && path != legacyPath {
|
||||
return fmt.Errorf("resource input to function has mismatched legacy and internal path annotations")
|
||||
}
|
||||
if index != "" && legacyIndex != "" && index != legacyIndex {
|
||||
return fmt.Errorf("resource input to function has mismatched legacy and internal index annotations")
|
||||
}
|
||||
if id != "" && legacyId != "" && id != legacyId {
|
||||
return fmt.Errorf("resource input to function has mismatched legacy and internal id annotations")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type nodeAnnotations struct {
|
||||
path string
|
||||
index string
|
||||
id string
|
||||
}
|
||||
|
||||
// ReconcileInternalAnnotations reconciles the annotation format for path, index and id annotations.
|
||||
// It will ensure the output annotation format matches the format in the input. e.g. if the input
|
||||
// format uses the legacy format and the output will be converted to the legacy format if it's not.
|
||||
func ReconcileInternalAnnotations(result []*yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
|
||||
useInternal, useLegacy, err := determineAnnotationsFormat(nodeAnnosMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
// if only one annotation is set, set the other.
|
||||
err = missingInternalOrLegacyAnnotations(result[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we must check to see if the function changed either the new internal annotations
|
||||
// or the old legacy annotations. If one is changed, the change must be reflected
|
||||
// in the other.
|
||||
err = checkAnnotationsAltered(result[i], nodeAnnosMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We invoke determineAnnotationsFormat to find out if the original annotations
|
||||
// use the internal or (and) the legacy format. We format the resources to
|
||||
// make them consistent with original format.
|
||||
err = formatInternalAnnotations(result[i], useInternal, useLegacy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// if the annotations are still somehow out of sync, throw an error
|
||||
meta, _ := result[i].GetMeta()
|
||||
err = checkMismatchedAnnos(meta.Annotations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = result[i].Pipe(yaml.ClearAnnotation(kioutil.InternalAnnotationsMigrationResourceIDAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineAnnotationsFormat determines if the resources are using one of the internal and legacy annotation format or both of them.
|
||||
func determineAnnotationsFormat(nodeAnnosMap map[string]map[string]string) (bool, bool, error) {
|
||||
var useInternal, useLegacy bool
|
||||
var err error
|
||||
|
||||
if len(nodeAnnosMap) == 0 {
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
var internal, legacy *bool
|
||||
for _, annos := range nodeAnnosMap {
|
||||
_, foundPath := annos[kioutil.PathAnnotation]
|
||||
_, foundIndex := annos[kioutil.IndexAnnotation]
|
||||
_, foundId := annos[kioutil.IdAnnotation]
|
||||
_, foundLegacyPath := annos[kioutil.LegacyPathAnnotation]
|
||||
_, foundLegacyIndex := annos[kioutil.LegacyIndexAnnotation]
|
||||
_, foundLegacyId := annos[kioutil.LegacyIdAnnotation]
|
||||
|
||||
if !(foundPath || foundIndex || foundId || foundLegacyPath || foundLegacyIndex || foundLegacyId) {
|
||||
continue
|
||||
}
|
||||
|
||||
foundOneOf := foundPath || foundIndex || foundId
|
||||
if internal == nil {
|
||||
f := foundOneOf
|
||||
internal = &f
|
||||
}
|
||||
if (foundOneOf && !*internal) || (!foundOneOf && *internal) {
|
||||
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
|
||||
return useInternal, useLegacy, err
|
||||
}
|
||||
|
||||
foundOneOf = foundLegacyPath || foundLegacyIndex || foundLegacyId
|
||||
if legacy == nil {
|
||||
f := foundOneOf
|
||||
legacy = &f
|
||||
}
|
||||
if (foundOneOf && !*legacy) || (!foundOneOf && *legacy) {
|
||||
err = fmt.Errorf("the annotation formatting in the input resources is not consistent")
|
||||
return useInternal, useLegacy, err
|
||||
}
|
||||
}
|
||||
if internal != nil {
|
||||
useInternal = *internal
|
||||
}
|
||||
if legacy != nil {
|
||||
useLegacy = *legacy
|
||||
}
|
||||
return useInternal, useLegacy, err
|
||||
}
|
||||
|
||||
func missingInternalOrLegacyAnnotations(rn *yaml.RNode) error {
|
||||
if err := missingInternalOrLegacyAnnotation(rn, kioutil.PathAnnotation, kioutil.LegacyPathAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := missingInternalOrLegacyAnnotation(rn, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := missingInternalOrLegacyAnnotation(rn, kioutil.IdAnnotation, kioutil.LegacyIdAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func missingInternalOrLegacyAnnotation(rn *yaml.RNode, newKey string, legacyKey string) error {
|
||||
meta, _ := rn.GetMeta()
|
||||
annotations := meta.Annotations
|
||||
value := annotations[newKey]
|
||||
legacyValue := annotations[legacyKey]
|
||||
|
||||
if value == "" && legacyValue == "" {
|
||||
// do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
// new key is not set, copy from legacy key
|
||||
if err := rn.PipeE(yaml.SetAnnotation(newKey, legacyValue)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if legacyValue == "" {
|
||||
// legacy key is not set, copy from new key
|
||||
if err := rn.PipeE(yaml.SetAnnotation(legacyKey, value)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAnnotationsAltered(rn *yaml.RNode, nodeAnnosMap map[string]map[string]string) error {
|
||||
meta, _ := rn.GetMeta()
|
||||
annotations := meta.Annotations
|
||||
// get the resource's current path, index, and ids from the new annotations
|
||||
internal := nodeAnnotations{
|
||||
path: annotations[kioutil.PathAnnotation],
|
||||
index: annotations[kioutil.IndexAnnotation],
|
||||
id: annotations[kioutil.IdAnnotation],
|
||||
}
|
||||
|
||||
// get the resource's current path, index, and ids from the legacy annotations
|
||||
legacy := nodeAnnotations{
|
||||
path: annotations[kioutil.LegacyPathAnnotation],
|
||||
index: annotations[kioutil.LegacyIndexAnnotation],
|
||||
id: annotations[kioutil.LegacyIdAnnotation],
|
||||
}
|
||||
|
||||
rid := annotations[kioutil.InternalAnnotationsMigrationResourceIDAnnotation]
|
||||
originalAnnotations, found := nodeAnnosMap[rid]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
originalPath, found := originalAnnotations[kioutil.PathAnnotation]
|
||||
if !found {
|
||||
originalPath = originalAnnotations[kioutil.LegacyPathAnnotation]
|
||||
}
|
||||
if originalPath != "" {
|
||||
switch {
|
||||
case originalPath != internal.path && originalPath != legacy.path && internal.path != legacy.path:
|
||||
return fmt.Errorf("resource input to function has mismatched legacy and internal path annotations")
|
||||
case originalPath != internal.path:
|
||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, internal.path)); err != nil {
|
||||
return err
|
||||
}
|
||||
case originalPath != legacy.path:
|
||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.PathAnnotation, legacy.path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalIndex, found := originalAnnotations[kioutil.IndexAnnotation]
|
||||
if !found {
|
||||
originalIndex = originalAnnotations[kioutil.LegacyIndexAnnotation]
|
||||
}
|
||||
if originalIndex != "" {
|
||||
switch {
|
||||
case originalIndex != internal.index && originalIndex != legacy.index && internal.index != legacy.index:
|
||||
return fmt.Errorf("resource input to function has mismatched legacy and internal index annotations")
|
||||
case originalIndex != internal.index:
|
||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.LegacyIndexAnnotation, internal.index)); err != nil {
|
||||
return err
|
||||
}
|
||||
case originalIndex != legacy.index:
|
||||
if _, err := rn.Pipe(yaml.SetAnnotation(kioutil.IndexAnnotation, legacy.index)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatInternalAnnotations(rn *yaml.RNode, useInternal, useLegacy bool) error {
|
||||
if !useInternal {
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.IdAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.PathAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.IndexAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !useLegacy {
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyIdAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyPathAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rn.PipeE(yaml.ClearAnnotation(kioutil.LegacyIndexAnnotation)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kioutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type AnnotationKey = string
|
||||
|
||||
const (
|
||||
// internalPrefix is the prefix given to internal annotations that are used
|
||||
// internally by the orchestrator
|
||||
internalPrefix string = "internal.config.kubernetes.io/"
|
||||
|
||||
// IndexAnnotation records the index of a specific resource in a file or input stream.
|
||||
IndexAnnotation AnnotationKey = internalPrefix + "index"
|
||||
|
||||
// PathAnnotation records the path to the file the Resource was read from
|
||||
PathAnnotation AnnotationKey = internalPrefix + "path"
|
||||
|
||||
// SeqIndentAnnotation records the sequence nodes indentation of the input resource
|
||||
SeqIndentAnnotation AnnotationKey = internalPrefix + "seqindent"
|
||||
|
||||
// IdAnnotation records the id of the resource to map inputs to outputs
|
||||
IdAnnotation AnnotationKey = internalPrefix + "id"
|
||||
|
||||
// Deprecated: Use IndexAnnotation instead.
|
||||
LegacyIndexAnnotation AnnotationKey = "config.kubernetes.io/index"
|
||||
|
||||
// Deprecated: use PathAnnotation instead.
|
||||
LegacyPathAnnotation AnnotationKey = "config.kubernetes.io/path"
|
||||
|
||||
// Deprecated: use IdAnnotation instead.
|
||||
LegacyIdAnnotation = "config.k8s.io/id"
|
||||
|
||||
// InternalAnnotationsMigrationResourceIDAnnotation is used to uniquely identify
|
||||
// resources during round trip to and from a function execution. We will use it
|
||||
// to track the internal annotations and reconcile them if needed.
|
||||
InternalAnnotationsMigrationResourceIDAnnotation = internalPrefix + "annotations-migration-resource-id"
|
||||
)
|
||||
|
||||
func GetFileAnnotations(rn *yaml.RNode) (string, string, error) {
|
||||
rm, _ := rn.GetMeta()
|
||||
annotations := rm.Annotations
|
||||
path, found := annotations[PathAnnotation]
|
||||
if !found {
|
||||
path = annotations[LegacyPathAnnotation]
|
||||
}
|
||||
index, found := annotations[IndexAnnotation]
|
||||
if !found {
|
||||
index = annotations[LegacyIndexAnnotation]
|
||||
}
|
||||
return path, index, nil
|
||||
}
|
||||
|
||||
func GetIdAnnotation(rn *yaml.RNode) string {
|
||||
rm, _ := rn.GetMeta()
|
||||
annotations := rm.Annotations
|
||||
id, found := annotations[IdAnnotation]
|
||||
if !found {
|
||||
id = annotations[LegacyIdAnnotation]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func CopyLegacyAnnotations(rn *yaml.RNode) error {
|
||||
meta, err := rn.GetMeta()
|
||||
if err != nil {
|
||||
if err == yaml.ErrMissingMetadata {
|
||||
// resource has no metadata, this should be a no-op
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := copyAnnotations(meta, rn, LegacyPathAnnotation, PathAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyAnnotations(meta, rn, LegacyIndexAnnotation, IndexAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyAnnotations(meta, rn, LegacyIdAnnotation, IdAnnotation); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyAnnotations(meta yaml.ResourceMeta, rn *yaml.RNode, legacyKey string, newKey string) error {
|
||||
newValue := meta.Annotations[newKey]
|
||||
legacyValue := meta.Annotations[legacyKey]
|
||||
if newValue != "" {
|
||||
if legacyValue == "" {
|
||||
if err := rn.PipeE(yaml.SetAnnotation(legacyKey, newValue)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if legacyValue != "" {
|
||||
if err := rn.PipeE(yaml.SetAnnotation(newKey, legacyValue)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrorIfMissingAnnotation validates the provided annotations are present on the given resources
|
||||
func ErrorIfMissingAnnotation(nodes []*yaml.RNode, keys ...AnnotationKey) error {
|
||||
for _, key := range keys {
|
||||
for _, node := range nodes {
|
||||
val, err := node.Pipe(yaml.GetAnnotation(key))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if val == nil {
|
||||
return errors.Errorf("missing annotation %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePathAnnotationValue creates a default path annotation value for a Resource.
|
||||
// The path prefix will be dir.
|
||||
func CreatePathAnnotationValue(dir string, m yaml.ResourceMeta) string {
|
||||
filename := fmt.Sprintf("%s_%s.yaml", strings.ToLower(m.Kind), m.Name)
|
||||
return path.Join(dir, m.Namespace, filename)
|
||||
}
|
||||
|
||||
// DefaultPathAndIndexAnnotation sets a default path or index value on any nodes missing the
|
||||
// annotation
|
||||
func DefaultPathAndIndexAnnotation(dir string, nodes []*yaml.RNode) error {
|
||||
counts := map[string]int{}
|
||||
|
||||
// check each node for the path annotation
|
||||
for i := range nodes {
|
||||
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := nodes[i].GetMeta()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// calculate the max index in each file in case we are appending
|
||||
if p, found := m.Annotations[PathAnnotation]; found {
|
||||
// record the max indexes into each file
|
||||
if i, found := m.Annotations[IndexAnnotation]; found {
|
||||
index, _ := strconv.Atoi(i)
|
||||
if index > counts[p] {
|
||||
counts[p] = index
|
||||
}
|
||||
}
|
||||
|
||||
// has the path annotation already -- do nothing
|
||||
continue
|
||||
}
|
||||
|
||||
// set a path annotation on the Resource
|
||||
path := CreatePathAnnotationValue(dir, m)
|
||||
if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set the index annotations
|
||||
for i := range nodes {
|
||||
m, err := nodes[i].GetMeta()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, found := m.Annotations[IndexAnnotation]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
p := m.Annotations[PathAnnotation]
|
||||
|
||||
// set an index annotation on the Resource
|
||||
c := counts[p]
|
||||
counts[p] = c + 1
|
||||
if err := nodes[i].PipeE(
|
||||
yaml.SetAnnotation(IndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := nodes[i].PipeE(
|
||||
yaml.SetAnnotation(LegacyIndexAnnotation, fmt.Sprintf("%d", c))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultPathAnnotation sets a default path annotation on any Reources
|
||||
// missing it.
|
||||
func DefaultPathAnnotation(dir string, nodes []*yaml.RNode) error {
|
||||
// check each node for the path annotation
|
||||
for i := range nodes {
|
||||
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := nodes[i].GetMeta()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, found := m.Annotations[PathAnnotation]; found {
|
||||
// has the path annotation already -- do nothing
|
||||
continue
|
||||
}
|
||||
|
||||
// set a path annotation on the Resource
|
||||
path := CreatePathAnnotationValue(dir, m)
|
||||
if err := nodes[i].PipeE(yaml.SetAnnotation(PathAnnotation, path)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := nodes[i].PipeE(yaml.SetAnnotation(LegacyPathAnnotation, path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Map invokes fn for each element in nodes.
|
||||
func Map(nodes []*yaml.RNode, fn func(*yaml.RNode) (*yaml.RNode, error)) ([]*yaml.RNode, error) {
|
||||
var returnNodes []*yaml.RNode
|
||||
for i := range nodes {
|
||||
n, err := fn(nodes[i])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if n != nil {
|
||||
returnNodes = append(returnNodes, n)
|
||||
}
|
||||
}
|
||||
return returnNodes, nil
|
||||
}
|
||||
|
||||
func MapMeta(nodes []*yaml.RNode, fn func(*yaml.RNode, yaml.ResourceMeta) (*yaml.RNode, error)) (
|
||||
[]*yaml.RNode, error) {
|
||||
var returnNodes []*yaml.RNode
|
||||
for i := range nodes {
|
||||
meta, err := nodes[i].GetMeta()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
n, err := fn(nodes[i], meta)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if n != nil {
|
||||
returnNodes = append(returnNodes, n)
|
||||
}
|
||||
}
|
||||
return returnNodes, nil
|
||||
}
|
||||
|
||||
// SortNodes sorts nodes in place:
|
||||
// - by PathAnnotation annotation
|
||||
// - by IndexAnnotation annotation
|
||||
func SortNodes(nodes []*yaml.RNode) error {
|
||||
var err error
|
||||
// use stable sort to keep ordering of equal elements
|
||||
sort.SliceStable(nodes, func(i, j int) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := CopyLegacyAnnotations(nodes[i]); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := CopyLegacyAnnotations(nodes[j]); err != nil {
|
||||
return false
|
||||
}
|
||||
var iMeta, jMeta yaml.ResourceMeta
|
||||
if iMeta, _ = nodes[i].GetMeta(); err != nil {
|
||||
return false
|
||||
}
|
||||
if jMeta, _ = nodes[j].GetMeta(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
iValue := iMeta.Annotations[PathAnnotation]
|
||||
jValue := jMeta.Annotations[PathAnnotation]
|
||||
if iValue != jValue {
|
||||
return iValue < jValue
|
||||
}
|
||||
|
||||
iValue = iMeta.Annotations[IndexAnnotation]
|
||||
jValue = jMeta.Annotations[IndexAnnotation]
|
||||
|
||||
// put resource config without an index first
|
||||
if iValue == jValue {
|
||||
return false
|
||||
}
|
||||
if iValue == "" {
|
||||
return true
|
||||
}
|
||||
if jValue == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// sort by index
|
||||
var iIndex, jIndex int
|
||||
iIndex, err = strconv.Atoi(iValue)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", iValue, err)
|
||||
return false
|
||||
}
|
||||
jIndex, err = strconv.Atoi(jValue)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to parse config.kubernetes.io/index %s :%v", jValue, err)
|
||||
return false
|
||||
}
|
||||
if iIndex != jIndex {
|
||||
return iIndex < jIndex
|
||||
}
|
||||
|
||||
// elements are equal
|
||||
return false
|
||||
})
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// CopyInternalAnnotations copies the annotations that begin with the prefix
|
||||
// `internal.config.kubernetes.io` from the source RNode to the destination RNode.
|
||||
// It takes a parameter exclusions, which is a list of annotation keys to ignore.
|
||||
func CopyInternalAnnotations(src *yaml.RNode, dst *yaml.RNode, exclusions ...AnnotationKey) error {
|
||||
srcAnnotations := GetInternalAnnotations(src)
|
||||
for k, v := range srcAnnotations {
|
||||
if stringSliceContains(exclusions, k) {
|
||||
continue
|
||||
}
|
||||
if err := dst.PipeE(yaml.SetAnnotation(k, v)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfirmInternalAnnotationUnchanged compares the annotations of the RNodes that begin with the prefix
|
||||
// `internal.config.kubernetes.io`, throwing an error if they differ. It takes a parameter exclusions,
|
||||
// which is a list of annotation keys to ignore.
|
||||
func ConfirmInternalAnnotationUnchanged(r1 *yaml.RNode, r2 *yaml.RNode, exclusions ...AnnotationKey) error {
|
||||
r1Annotations := GetInternalAnnotations(r1)
|
||||
r2Annotations := GetInternalAnnotations(r2)
|
||||
|
||||
// this is a map to prevent duplicates
|
||||
diffAnnos := make(map[string]bool)
|
||||
|
||||
for k, v1 := range r1Annotations {
|
||||
if stringSliceContains(exclusions, k) {
|
||||
continue
|
||||
}
|
||||
if v2, ok := r2Annotations[k]; !ok || v1 != v2 {
|
||||
diffAnnos[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v2 := range r2Annotations {
|
||||
if stringSliceContains(exclusions, k) {
|
||||
continue
|
||||
}
|
||||
if v1, ok := r1Annotations[k]; !ok || v2 != v1 {
|
||||
diffAnnos[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(diffAnnos) > 0 {
|
||||
keys := make([]string, 0, len(diffAnnos))
|
||||
for k := range diffAnnos {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
errorString := "internal annotations differ: "
|
||||
for _, key := range keys {
|
||||
errorString = errorString + key + ", "
|
||||
}
|
||||
return errors.Errorf("%s", errorString[0:len(errorString)-2])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInternalAnnotations returns a map of all the annotations of the provided
|
||||
// RNode that satisfies one of the following: 1) begin with the prefix
|
||||
// `internal.config.kubernetes.io` 2) is one of `config.kubernetes.io/path`,
|
||||
// `config.kubernetes.io/index` and `config.k8s.io/id`.
|
||||
func GetInternalAnnotations(rn *yaml.RNode) map[string]string {
|
||||
meta, _ := rn.GetMeta()
|
||||
annotations := meta.Annotations
|
||||
result := make(map[string]string)
|
||||
for k, v := range annotations {
|
||||
if strings.HasPrefix(k, internalPrefix) || k == LegacyPathAnnotation || k == LegacyIndexAnnotation || k == LegacyIdAnnotation {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// stringSliceContains returns true if the slice has the string.
|
||||
func stringSliceContains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// requiredResourcePackageAnnotations are annotations that are required to write resources back to
|
||||
// files.
|
||||
var requiredResourcePackageAnnotations = []string{kioutil.IndexAnnotation, kioutil.PathAnnotation}
|
||||
|
||||
// PackageBuffer implements Reader and Writer, storing Resources in a local field.
|
||||
type PackageBuffer struct {
|
||||
Nodes []*yaml.RNode
|
||||
}
|
||||
|
||||
func (r *PackageBuffer) Read() ([]*yaml.RNode, error) {
|
||||
return r.Nodes, nil
|
||||
}
|
||||
|
||||
func (r *PackageBuffer) Write(nodes []*yaml.RNode) error {
|
||||
r.Nodes = nodes
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalPackageReadWriter reads and writes Resources from / to a local directory.
|
||||
// When writing, LocalPackageReadWriter will delete files if all of the Resources from
|
||||
// that file have been removed from the output.
|
||||
type LocalPackageReadWriter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
KeepReaderAnnotations bool `yaml:"keepReaderAnnotations,omitempty"`
|
||||
|
||||
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
|
||||
PreserveSeqIndent bool
|
||||
|
||||
// PackagePath is the path to the package directory.
|
||||
PackagePath string `yaml:"path,omitempty"`
|
||||
|
||||
// PackageFileName is the name of file containing package metadata.
|
||||
// It will be used to identify package.
|
||||
PackageFileName string `yaml:"packageFileName,omitempty"`
|
||||
|
||||
// MatchFilesGlob configures Read to only read Resources from files matching any of the
|
||||
// provided patterns.
|
||||
// Defaults to ["*.yaml", "*.yml"] if empty. To match all files specify ["*"].
|
||||
MatchFilesGlob []string `yaml:"matchFilesGlob,omitempty"`
|
||||
|
||||
// IncludeSubpackages will configure Read to read Resources from subpackages.
|
||||
// Subpackages are identified by presence of PackageFileName.
|
||||
IncludeSubpackages bool `yaml:"includeSubpackages,omitempty"`
|
||||
|
||||
// ErrorIfNonResources will configure Read to throw an error if yaml missing missing
|
||||
// apiVersion or kind is read.
|
||||
ErrorIfNonResources bool `yaml:"errorIfNonResources,omitempty"`
|
||||
|
||||
// OmitReaderAnnotations will cause the reader to skip annotating Resources with the file
|
||||
// path and mode.
|
||||
OmitReaderAnnotations bool `yaml:"omitReaderAnnotations,omitempty"`
|
||||
|
||||
// SetAnnotations are annotations to set on the Resources as they are read.
|
||||
SetAnnotations map[string]string `yaml:"setAnnotations,omitempty"`
|
||||
|
||||
// NoDeleteFiles if set to true, LocalPackageReadWriter won't delete any files
|
||||
NoDeleteFiles bool `yaml:"noDeleteFiles,omitempty"`
|
||||
|
||||
files sets.String
|
||||
|
||||
// FileSkipFunc is a function which returns true if reader should ignore
|
||||
// the file
|
||||
FileSkipFunc LocalPackageSkipFileFunc
|
||||
|
||||
// FileSystem can be used to mock the disk file system.
|
||||
FileSystem filesys.FileSystemOrOnDisk
|
||||
|
||||
// WrapBareSeqNode wraps the bare sequence node document with map node,
|
||||
// kyaml uses reader annotations to track resources, it is not possible to
|
||||
// add them to bare sequence nodes, this option enables wrapping such bare
|
||||
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
|
||||
// note that this wrapping is different and not related to ResourceList wrapping
|
||||
WrapBareSeqNode bool
|
||||
}
|
||||
|
||||
func (r *LocalPackageReadWriter) Read() ([]*yaml.RNode, error) {
|
||||
nodes, err := LocalPackageReader{
|
||||
PackagePath: r.PackagePath,
|
||||
MatchFilesGlob: r.MatchFilesGlob,
|
||||
IncludeSubpackages: r.IncludeSubpackages,
|
||||
ErrorIfNonResources: r.ErrorIfNonResources,
|
||||
SetAnnotations: r.SetAnnotations,
|
||||
PackageFileName: r.PackageFileName,
|
||||
FileSkipFunc: r.FileSkipFunc,
|
||||
PreserveSeqIndent: r.PreserveSeqIndent,
|
||||
FileSystem: r.FileSystem,
|
||||
WrapBareSeqNode: r.WrapBareSeqNode,
|
||||
}.Read()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
// keep track of all the files
|
||||
if !r.NoDeleteFiles {
|
||||
r.files, err = r.getFiles(nodes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (r *LocalPackageReadWriter) Write(nodes []*yaml.RNode) error {
|
||||
newFiles, err := r.getFiles(nodes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
var clear []string
|
||||
for k := range r.SetAnnotations {
|
||||
clear = append(clear, k)
|
||||
}
|
||||
err = LocalPackageWriter{
|
||||
PackagePath: r.PackagePath,
|
||||
ClearAnnotations: clear,
|
||||
KeepReaderAnnotations: r.KeepReaderAnnotations,
|
||||
FileSystem: r.FileSystem,
|
||||
}.Write(nodes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
deleteFiles := r.files.Difference(newFiles)
|
||||
for f := range deleteFiles {
|
||||
if err = r.FileSystem.RemoveAll(filepath.Join(r.PackagePath, f)); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LocalPackageReadWriter) getFiles(nodes []*yaml.RNode) (sets.String, error) {
|
||||
val := sets.String{}
|
||||
for _, n := range nodes {
|
||||
path, _, err := kioutil.GetFileAnnotations(n)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
val.Insert(path)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// LocalPackageSkipFileFunc is a function which returns true if the file
|
||||
// in the package should be ignored by reader.
|
||||
// relPath is an OS specific relative path
|
||||
type LocalPackageSkipFileFunc func(relPath string) bool
|
||||
|
||||
// LocalPackageReader reads ResourceNodes from a local package.
|
||||
type LocalPackageReader struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// PackagePath is the path to the package directory.
|
||||
PackagePath string `yaml:"path,omitempty"`
|
||||
|
||||
// PackageFileName is the name of file containing package metadata.
|
||||
// It will be used to identify package.
|
||||
PackageFileName string `yaml:"packageFileName,omitempty"`
|
||||
|
||||
// MatchFilesGlob configures Read to only read Resources from files matching any of the
|
||||
// provided patterns.
|
||||
// Defaults to ["*.yaml", "*.yml"] if empty. To match all files specify ["*"].
|
||||
MatchFilesGlob []string `yaml:"matchFilesGlob,omitempty"`
|
||||
|
||||
// IncludeSubpackages will configure Read to read Resources from subpackages.
|
||||
// Subpackages are identified by presence of PackageFileName.
|
||||
IncludeSubpackages bool `yaml:"includeSubpackages,omitempty"`
|
||||
|
||||
// ErrorIfNonResources will configure Read to throw an error if yaml missing missing
|
||||
// apiVersion or kind is read.
|
||||
ErrorIfNonResources bool `yaml:"errorIfNonResources,omitempty"`
|
||||
|
||||
// OmitReaderAnnotations will cause the reader to skip annotating Resources with the file
|
||||
// path and mode.
|
||||
OmitReaderAnnotations bool `yaml:"omitReaderAnnotations,omitempty"`
|
||||
|
||||
// SetAnnotations are annotations to set on the Resources as they are read.
|
||||
SetAnnotations map[string]string `yaml:"setAnnotations,omitempty"`
|
||||
|
||||
// FileSkipFunc is a function which returns true if reader should ignore
|
||||
// the file
|
||||
FileSkipFunc LocalPackageSkipFileFunc
|
||||
|
||||
// PreserveSeqIndent if true adds kioutil.SeqIndentAnnotation to each resource
|
||||
PreserveSeqIndent bool
|
||||
|
||||
// FileSystem can be used to mock the disk file system.
|
||||
FileSystem filesys.FileSystemOrOnDisk
|
||||
|
||||
// WrapBareSeqNode wraps the bare sequence node document with map node,
|
||||
// kyaml uses reader annotations to track resources, it is not possible to
|
||||
// add them to bare sequence nodes, this option enables wrapping such bare
|
||||
// sequence nodes into map node with key yaml.BareSeqNodeWrappingKey
|
||||
// note that this wrapping is different and not related to ResourceList wrapping
|
||||
WrapBareSeqNode bool
|
||||
}
|
||||
|
||||
var _ Reader = LocalPackageReader{}
|
||||
|
||||
var DefaultMatch = []string{"*.yaml", "*.yml"}
|
||||
var JSONMatch = []string{"*.json"}
|
||||
var MatchAll = append(DefaultMatch, JSONMatch...)
|
||||
|
||||
// Read reads the Resources.
|
||||
func (r LocalPackageReader) Read() ([]*yaml.RNode, error) {
|
||||
if r.PackagePath == "" {
|
||||
return nil, fmt.Errorf("must specify package path")
|
||||
}
|
||||
|
||||
// use slash for path
|
||||
r.PackagePath = filepath.ToSlash(r.PackagePath)
|
||||
if len(r.MatchFilesGlob) == 0 {
|
||||
r.MatchFilesGlob = DefaultMatch
|
||||
}
|
||||
|
||||
var operand ResourceNodeSlice
|
||||
var pathRelativeTo string
|
||||
var err error
|
||||
ignoreFilesMatcher := &ignoreFilesMatcher{
|
||||
fs: r.FileSystem,
|
||||
}
|
||||
dir, file, err := r.FileSystem.CleanedAbs(r.PackagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
r.PackagePath = filepath.Join(string(dir), file)
|
||||
err = r.FileSystem.Walk(r.PackagePath, func(
|
||||
path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// is this the user specified path?
|
||||
if path == r.PackagePath {
|
||||
if info.IsDir() {
|
||||
// skip the root package directory, but check for a
|
||||
// .krmignore file first.
|
||||
pathRelativeTo = r.PackagePath
|
||||
return ignoreFilesMatcher.readIgnoreFile(path)
|
||||
}
|
||||
|
||||
// user specified path is a file rather than a directory.
|
||||
// make its path relative to its parent so it can be written to another file.
|
||||
pathRelativeTo = filepath.Dir(r.PackagePath)
|
||||
}
|
||||
|
||||
// check if we should skip the directory or file
|
||||
if info.IsDir() {
|
||||
return r.shouldSkipDir(path, ignoreFilesMatcher)
|
||||
}
|
||||
|
||||
// get the relative path to file within the package so we can write the files back out
|
||||
// to another location.
|
||||
relPath, err := filepath.Rel(pathRelativeTo, path)
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "%s", pathRelativeTo)
|
||||
}
|
||||
if match, err := r.shouldSkipFile(path, relPath, ignoreFilesMatcher); err != nil {
|
||||
return err
|
||||
} else if match {
|
||||
// skip this file
|
||||
return nil
|
||||
}
|
||||
|
||||
r.initReaderAnnotations(relPath, info)
|
||||
nodes, err := r.readFile(path, info)
|
||||
if err != nil {
|
||||
return errors.WrapPrefixf(err, "%s", path)
|
||||
}
|
||||
operand = append(operand, nodes...)
|
||||
return nil
|
||||
})
|
||||
return operand, err
|
||||
}
|
||||
|
||||
// readFile reads the ResourceNodes from a file
|
||||
func (r *LocalPackageReader) readFile(path string, _ os.FileInfo) ([]*yaml.RNode, error) {
|
||||
f, err := r.FileSystem.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rr := &ByteReader{
|
||||
DisableUnwrapping: true,
|
||||
Reader: f,
|
||||
OmitReaderAnnotations: r.OmitReaderAnnotations,
|
||||
SetAnnotations: r.SetAnnotations,
|
||||
PreserveSeqIndent: r.PreserveSeqIndent,
|
||||
WrapBareSeqNode: r.WrapBareSeqNode,
|
||||
}
|
||||
return rr.Read()
|
||||
}
|
||||
|
||||
// shouldSkipFile returns true if the file should be skipped
|
||||
func (r *LocalPackageReader) shouldSkipFile(path, relPath string, matcher *ignoreFilesMatcher) (bool, error) {
|
||||
// check if the file is covered by a .krmignore file.
|
||||
if matcher.matchFile(path) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if r.FileSkipFunc != nil && r.FileSkipFunc(relPath) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// check if the files are in scope
|
||||
for _, g := range r.MatchFilesGlob {
|
||||
if match, err := filepath.Match(g, filepath.Base(path)); err != nil {
|
||||
return true, errors.Wrap(err)
|
||||
} else if match {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// initReaderAnnotations adds the LocalPackageReader Annotations to r.SetAnnotations
|
||||
func (r *LocalPackageReader) initReaderAnnotations(path string, _ os.FileInfo) {
|
||||
if r.SetAnnotations == nil {
|
||||
r.SetAnnotations = map[string]string{}
|
||||
}
|
||||
if !r.OmitReaderAnnotations {
|
||||
r.SetAnnotations[kioutil.PathAnnotation] = path
|
||||
r.SetAnnotations[kioutil.LegacyPathAnnotation] = path
|
||||
}
|
||||
}
|
||||
|
||||
// shouldSkipDir returns a filepath.SkipDir if the directory should be skipped
|
||||
func (r *LocalPackageReader) shouldSkipDir(path string, matcher *ignoreFilesMatcher) error {
|
||||
if matcher.matchDir(path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if r.PackageFileName == "" {
|
||||
return nil
|
||||
}
|
||||
// check if this is a subpackage
|
||||
if !r.FileSystem.Exists(filepath.Join(path, r.PackageFileName)) {
|
||||
return nil
|
||||
}
|
||||
if !r.IncludeSubpackages {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return matcher.readIgnoreFile(path)
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// LocalPackageWriter writes ResourceNodes to a filesystem
|
||||
type LocalPackageWriter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// PackagePath is the path to the package directory.
|
||||
PackagePath string `yaml:"path,omitempty"`
|
||||
|
||||
// KeepReaderAnnotations if set will retain the annotations set by LocalPackageReader
|
||||
KeepReaderAnnotations bool `yaml:"keepReaderAnnotations,omitempty"`
|
||||
|
||||
// ClearAnnotations will clear annotations before writing the resources
|
||||
ClearAnnotations []string `yaml:"clearAnnotations,omitempty"`
|
||||
|
||||
// FileSystem can be used to mock the disk file system.
|
||||
FileSystem filesys.FileSystemOrOnDisk
|
||||
}
|
||||
|
||||
var _ Writer = LocalPackageWriter{}
|
||||
|
||||
func (r LocalPackageWriter) Write(nodes []*yaml.RNode) error {
|
||||
// set the path and index annotations if they are missing
|
||||
if err := kioutil.DefaultPathAndIndexAnnotation("", nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.FileSystem.Exists(r.PackagePath) {
|
||||
return errors.WrapPrefixf(os.ErrNotExist, "could not write to %q", r.PackagePath)
|
||||
}
|
||||
if !r.FileSystem.IsDir(r.PackagePath) {
|
||||
// if the user specified input isn't a directory, the package is the directory of the
|
||||
// target
|
||||
r.PackagePath = filepath.Dir(r.PackagePath)
|
||||
}
|
||||
|
||||
// setup indexes for writing Resources back to files
|
||||
if err := r.errorIfMissingRequiredAnnotation(nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
outputFiles, err := r.indexByFilePath(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k := range outputFiles {
|
||||
if err = kioutil.SortNodes(outputFiles[k]); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if !r.KeepReaderAnnotations {
|
||||
r.ClearAnnotations = append(r.ClearAnnotations, kioutil.PathAnnotation)
|
||||
r.ClearAnnotations = append(r.ClearAnnotations, kioutil.LegacyPathAnnotation)
|
||||
}
|
||||
|
||||
// validate outputs before writing any
|
||||
for path := range outputFiles {
|
||||
outputPath := filepath.Join(r.PackagePath, path)
|
||||
if r.FileSystem.IsDir(outputPath) {
|
||||
return fmt.Errorf("config.kubernetes.io/path cannot be a directory: %s", path)
|
||||
}
|
||||
|
||||
err = r.FileSystem.MkdirAll(filepath.Dir(outputPath))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// write files
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for path := range outputFiles {
|
||||
outputPath := filepath.Join(r.PackagePath, path)
|
||||
err = r.FileSystem.MkdirAll(filepath.Dir(filepath.Join(r.PackagePath, path)))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
w := ByteWriter{
|
||||
Writer: buf,
|
||||
KeepReaderAnnotations: r.KeepReaderAnnotations,
|
||||
ClearAnnotations: r.ClearAnnotations,
|
||||
}
|
||||
if err = w.Write(outputFiles[path]); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
if err := r.FileSystem.WriteFile(outputPath, buf.Bytes()); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r LocalPackageWriter) errorIfMissingRequiredAnnotation(nodes []*yaml.RNode) error {
|
||||
for i := range nodes {
|
||||
for _, s := range requiredResourcePackageAnnotations {
|
||||
key, err := nodes[i].Pipe(yaml.GetAnnotation(s))
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if key == nil || key.YNode() == nil || key.YNode().Value == "" {
|
||||
return errors.Errorf(
|
||||
"resources must be annotated with %s to be written to files", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r LocalPackageWriter) indexByFilePath(nodes []*yaml.RNode) (map[string][]*yaml.RNode, error) {
|
||||
outputFiles := map[string][]*yaml.RNode{}
|
||||
for i := range nodes {
|
||||
// parse the file write path
|
||||
node := nodes[i]
|
||||
value, err := node.Pipe(yaml.GetAnnotation(kioutil.PathAnnotation))
|
||||
if err != nil {
|
||||
// this should never happen if errorIfMissingRequiredAnnotation was run
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
path := value.YNode().Value
|
||||
outputFiles[path] = append(outputFiles[path], node)
|
||||
|
||||
if filepath.IsAbs(path) {
|
||||
return nil, errors.Errorf("package paths may not be absolute paths")
|
||||
}
|
||||
if strings.Contains(filepath.Clean(path), "..") {
|
||||
return nil, fmt.Errorf("resource must be written under package %s: %s",
|
||||
r.PackagePath, filepath.Clean(path))
|
||||
}
|
||||
}
|
||||
return outputFiles, nil
|
||||
}
|
||||
+519
@@ -0,0 +1,519 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package kio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/xlab/treeprint"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type TreeStructure string
|
||||
|
||||
const (
|
||||
// TreeStructurePackage configures TreeWriter to generate the tree structure off of the
|
||||
// Resources packages.
|
||||
TreeStructurePackage TreeStructure = "directory"
|
||||
|
||||
// TreeStructureOwners configures TreeWriter to generate the tree structure off of the
|
||||
// Resource owners.
|
||||
TreeStructureGraph TreeStructure = "owners"
|
||||
)
|
||||
|
||||
var GraphStructures = []string{string(TreeStructureGraph), string(TreeStructurePackage)}
|
||||
|
||||
// TreeWriter prints the package structured as a tree.
|
||||
// TODO(pwittrock): test this package better. it is lower-risk since it is only
|
||||
// used for printing rather than updating or editing.
|
||||
type TreeWriter struct {
|
||||
Writer io.Writer
|
||||
Root string
|
||||
Fields []TreeWriterField
|
||||
Structure TreeStructure
|
||||
OpenAPIFileName string
|
||||
}
|
||||
|
||||
// TreeWriterField configures a Resource field to be included in the tree
|
||||
type TreeWriterField struct {
|
||||
yaml.PathMatcher
|
||||
Name string
|
||||
SubName string
|
||||
}
|
||||
|
||||
func (p TreeWriter) packageStructure(nodes []*yaml.RNode) error {
|
||||
for i := range nodes {
|
||||
if err := kioutil.CopyLegacyAnnotations(nodes[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
indexByPackage := p.index(nodes)
|
||||
|
||||
// create the new tree
|
||||
tree := treeprint.New()
|
||||
tree.SetValue(p.Root)
|
||||
|
||||
// add each package to the tree
|
||||
treeIndex := map[string]treeprint.Tree{}
|
||||
keys := p.sort(indexByPackage)
|
||||
for _, pkg := range keys {
|
||||
// create a branch for this package -- search for the parent package and create
|
||||
// the branch under it -- requires that the keys are sorted
|
||||
branch := tree
|
||||
for parent, subTree := range treeIndex {
|
||||
if strings.HasPrefix(pkg, parent) {
|
||||
// found a package whose path is a prefix to our own, use this
|
||||
// package if a closer one isn't found
|
||||
branch = subTree
|
||||
// don't break, continue searching for more closely related ancestors
|
||||
}
|
||||
}
|
||||
|
||||
// create a new branch for the package
|
||||
createOk := pkg != "." // special edge case logic for tree on current working dir
|
||||
if createOk {
|
||||
branch = branch.AddBranch(branchName(p.Root, pkg, p.OpenAPIFileName))
|
||||
}
|
||||
|
||||
// cache the branch for this package
|
||||
treeIndex[pkg] = branch
|
||||
|
||||
// print each resource in the package
|
||||
for i := range indexByPackage[pkg] {
|
||||
var err error
|
||||
if _, err = p.doResource(indexByPackage[pkg][i], "", branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err := io.WriteString(p.Writer, tree.String())
|
||||
return err
|
||||
}
|
||||
|
||||
// branchName takes the root directory and relative path to the directory
|
||||
// and returns the branch name
|
||||
func branchName(root, dirRelPath, openAPIFileName string) string {
|
||||
name := filepath.Base(dirRelPath)
|
||||
_, err := os.Stat(filepath.Join(root, dirRelPath, openAPIFileName))
|
||||
if !os.IsNotExist(err) {
|
||||
// add Pkg: prefix indicating that it is a separate package as it has
|
||||
// openAPIFile
|
||||
return fmt.Sprintf("Pkg: %s", name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Write writes the ascii tree to p.Writer
|
||||
func (p TreeWriter) Write(nodes []*yaml.RNode) error {
|
||||
switch p.Structure {
|
||||
case TreeStructurePackage:
|
||||
return p.packageStructure(nodes)
|
||||
case TreeStructureGraph:
|
||||
return p.graphStructure(nodes)
|
||||
}
|
||||
|
||||
// If any resource has an owner reference, default to the graph structure. Otherwise, use package structure.
|
||||
for _, node := range nodes {
|
||||
if owners, _ := node.Pipe(yaml.Lookup("metadata", "ownerReferences")); owners != nil {
|
||||
return p.graphStructure(nodes)
|
||||
}
|
||||
}
|
||||
return p.packageStructure(nodes)
|
||||
}
|
||||
|
||||
// node wraps a tree node, and any children nodes
|
||||
type node struct {
|
||||
p TreeWriter
|
||||
*yaml.RNode
|
||||
children []*node
|
||||
}
|
||||
|
||||
func (a node) Len() int { return len(a.children) }
|
||||
func (a node) Swap(i, j int) { a.children[i], a.children[j] = a.children[j], a.children[i] }
|
||||
func (a node) Less(i, j int) bool {
|
||||
return compareNodes(a.children[i].RNode, a.children[j].RNode)
|
||||
}
|
||||
|
||||
// Tree adds this node to the root
|
||||
func (a node) Tree(root treeprint.Tree) error {
|
||||
sort.Sort(a)
|
||||
branch := root
|
||||
var err error
|
||||
|
||||
// generate a node for the Resource
|
||||
if a.RNode != nil {
|
||||
branch, err = a.p.doResource(a.RNode, "Resource", root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// attach children to the branch
|
||||
for _, n := range a.children {
|
||||
if err := n.Tree(branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// graphStructure writes the tree using owners for structure
|
||||
func (p TreeWriter) graphStructure(nodes []*yaml.RNode) error {
|
||||
resourceToOwner := map[string]*node{}
|
||||
root := &node{}
|
||||
// index each of the nodes by their owner
|
||||
for _, n := range nodes {
|
||||
ownerVal, err := ownerToString(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var owner *node
|
||||
if ownerVal == "" {
|
||||
// no owner -- attach to the root
|
||||
owner = root
|
||||
} else {
|
||||
// owner found -- attach to the owner
|
||||
var found bool
|
||||
owner, found = resourceToOwner[ownerVal]
|
||||
if !found {
|
||||
// initialize the owner if not found
|
||||
resourceToOwner[ownerVal] = &node{p: p}
|
||||
owner = resourceToOwner[ownerVal]
|
||||
}
|
||||
}
|
||||
|
||||
nodeVal, err := nodeToString(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val, found := resourceToOwner[nodeVal]
|
||||
if !found {
|
||||
// initialize the node if not found -- may have already been initialized if it
|
||||
// is the owner of another node
|
||||
resourceToOwner[nodeVal] = &node{p: p}
|
||||
val = resourceToOwner[nodeVal]
|
||||
}
|
||||
val.RNode = n
|
||||
owner.children = append(owner.children, val)
|
||||
}
|
||||
|
||||
for k, v := range resourceToOwner {
|
||||
if v.RNode == nil {
|
||||
return fmt.Errorf(
|
||||
"owner '%s' not found in input, but found as an owner of input objects", k)
|
||||
}
|
||||
}
|
||||
|
||||
// print the tree
|
||||
tree := treeprint.New()
|
||||
if err := root.Tree(tree); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := io.WriteString(p.Writer, tree.String())
|
||||
return err
|
||||
}
|
||||
|
||||
// nodeToString generates a string to identify the node -- matches ownerToString format
|
||||
func nodeToString(node *yaml.RNode) (string, error) {
|
||||
meta, err := node.GetMeta()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name), nil
|
||||
}
|
||||
|
||||
// ownerToString generate a string to identify the owner -- matches nodeToString format
|
||||
func ownerToString(node *yaml.RNode) (string, error) {
|
||||
meta, err := node.GetMeta()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
namespace := meta.Namespace
|
||||
|
||||
owners, err := node.Pipe(yaml.Lookup("metadata", "ownerReferences"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if owners == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
elements, err := owners.Elements()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(elements) == 0 {
|
||||
return "", err
|
||||
}
|
||||
owner := elements[0]
|
||||
var kind, name string
|
||||
|
||||
if value := owner.Field("kind"); !value.IsNilOrEmpty() {
|
||||
kind = value.Value.YNode().Value
|
||||
}
|
||||
if value := owner.Field("name"); !value.IsNilOrEmpty() {
|
||||
name = value.Value.YNode().Value
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s/%s", kind, namespace, name), nil
|
||||
}
|
||||
|
||||
// index indexes the Resources by their package
|
||||
func (p TreeWriter) index(nodes []*yaml.RNode) map[string][]*yaml.RNode {
|
||||
// index the ResourceNodes by package
|
||||
indexByPackage := map[string][]*yaml.RNode{}
|
||||
for i := range nodes {
|
||||
meta, err := nodes[i].GetMeta()
|
||||
if err != nil || meta.Kind == "" {
|
||||
// not a resource
|
||||
continue
|
||||
}
|
||||
pkg := filepath.Dir(meta.Annotations[kioutil.PathAnnotation])
|
||||
indexByPackage[pkg] = append(indexByPackage[pkg], nodes[i])
|
||||
}
|
||||
return indexByPackage
|
||||
}
|
||||
|
||||
func compareNodes(i, j *yaml.RNode) bool {
|
||||
metai, _ := i.GetMeta()
|
||||
metaj, _ := j.GetMeta()
|
||||
pi := metai.Annotations[kioutil.PathAnnotation]
|
||||
pj := metaj.Annotations[kioutil.PathAnnotation]
|
||||
|
||||
// compare file names
|
||||
if filepath.Base(pi) != filepath.Base(pj) {
|
||||
return filepath.Base(pi) < filepath.Base(pj)
|
||||
}
|
||||
|
||||
// compare namespace
|
||||
if metai.Namespace != metaj.Namespace {
|
||||
return metai.Namespace < metaj.Namespace
|
||||
}
|
||||
|
||||
// compare name
|
||||
if metai.Name != metaj.Name {
|
||||
return metai.Name < metaj.Name
|
||||
}
|
||||
|
||||
// compare kind
|
||||
if metai.Kind != metaj.Kind {
|
||||
return metai.Kind < metaj.Kind
|
||||
}
|
||||
|
||||
// compare apiVersion
|
||||
if metai.APIVersion != metaj.APIVersion {
|
||||
return metai.APIVersion < metaj.APIVersion
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// sort sorts the Resources in the index in display order and returns the ordered
|
||||
// keys for the index
|
||||
//
|
||||
// Packages are sorted by package name
|
||||
// Resources within a package are sorted by: [filename, namespace, name, kind, apiVersion]
|
||||
func (p TreeWriter) sort(indexByPackage map[string][]*yaml.RNode) []string {
|
||||
var keys []string
|
||||
for k := range indexByPackage {
|
||||
pkgNodes := indexByPackage[k]
|
||||
sort.Slice(pkgNodes, func(i, j int) bool { return compareNodes(pkgNodes[i], pkgNodes[j]) })
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
// return the package names sorted lexicographically
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (p TreeWriter) doResource(leaf *yaml.RNode, metaString string, branch treeprint.Tree) (treeprint.Tree, error) {
|
||||
meta, _ := leaf.GetMeta()
|
||||
if metaString == "" {
|
||||
path := meta.Annotations[kioutil.PathAnnotation]
|
||||
path = filepath.Base(path)
|
||||
metaString = path
|
||||
}
|
||||
|
||||
value := fmt.Sprintf("%s %s", meta.Kind, meta.Name)
|
||||
if len(meta.Namespace) > 0 {
|
||||
value = fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name)
|
||||
}
|
||||
|
||||
fields, err := p.getFields(leaf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := branch.AddMetaBranch(metaString, value)
|
||||
for i := range fields {
|
||||
field := fields[i]
|
||||
|
||||
// do leaf node
|
||||
if len(field.matchingElementsAndFields) == 0 {
|
||||
n.AddNode(fmt.Sprintf("%s: %s", field.name, field.value))
|
||||
continue
|
||||
}
|
||||
|
||||
// do nested nodes
|
||||
b := n.AddBranch(field.name)
|
||||
for j := range field.matchingElementsAndFields {
|
||||
elem := field.matchingElementsAndFields[j]
|
||||
b := b.AddBranch(elem.name)
|
||||
for k := range elem.matchingElementsAndFields {
|
||||
field := elem.matchingElementsAndFields[k]
|
||||
b.AddNode(fmt.Sprintf("%s: %s", field.name, field.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// getFields looks up p.Fields from leaf and structures them into treeFields.
|
||||
// TODO(pwittrock): simplify this function
|
||||
func (p TreeWriter) getFields(leaf *yaml.RNode) (treeFields, error) {
|
||||
fieldsByName := map[string]*treeField{}
|
||||
|
||||
// index nested and non-nested fields
|
||||
for i := range p.Fields {
|
||||
f := p.Fields[i]
|
||||
seq, err := leaf.Pipe(&f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seq == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldsByName[f.Name] == nil {
|
||||
fieldsByName[f.Name] = &treeField{name: f.Name}
|
||||
}
|
||||
|
||||
// non-nested field -- add directly to the treeFields list
|
||||
if f.SubName == "" {
|
||||
// non-nested field -- only 1 element
|
||||
val, err := yaml.String(seq.Content()[0], yaml.Trim, yaml.Flow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fieldsByName[f.Name].value = val
|
||||
continue
|
||||
}
|
||||
|
||||
// nested-field -- create a parent elem, and index by the 'match' value
|
||||
if fieldsByName[f.Name].subFieldByMatch == nil {
|
||||
fieldsByName[f.Name].subFieldByMatch = map[string]treeFields{}
|
||||
}
|
||||
index := fieldsByName[f.Name].subFieldByMatch
|
||||
for j := range seq.Content() {
|
||||
elem := seq.Content()[j]
|
||||
matches := f.Matches[elem]
|
||||
str, err := yaml.String(elem, yaml.Trim, yaml.Flow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// map the field by the name of the element
|
||||
// index the subfields by the matching element so we can put all the fields for the
|
||||
// same element under the same branch
|
||||
matchKey := strings.Join(matches, "/")
|
||||
index[matchKey] = append(index[matchKey], &treeField{name: f.SubName, value: str})
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over collection of all queried fields in the Resource
|
||||
for _, field := range fieldsByName {
|
||||
// iterate over collection of elements under the field -- indexed by element name
|
||||
for match, subFields := range field.subFieldByMatch {
|
||||
// create a new element for this collection of fields
|
||||
// note: we will convert name to an index later, but keep the match for sorting
|
||||
elem := &treeField{name: match}
|
||||
field.matchingElementsAndFields = append(field.matchingElementsAndFields, elem)
|
||||
|
||||
// iterate over collection of queried fields for the element
|
||||
for i := range subFields {
|
||||
// add to the list of fields for this element
|
||||
elem.matchingElementsAndFields = append(elem.matchingElementsAndFields, subFields[i])
|
||||
}
|
||||
}
|
||||
// clear this cached data
|
||||
field.subFieldByMatch = nil
|
||||
}
|
||||
|
||||
// put the fields in a list so they are ordered
|
||||
fieldList := treeFields{}
|
||||
for _, v := range fieldsByName {
|
||||
fieldList = append(fieldList, v)
|
||||
}
|
||||
|
||||
// sort the fields
|
||||
sort.Sort(fieldList)
|
||||
for i := range fieldList {
|
||||
field := fieldList[i]
|
||||
// sort the elements under this field
|
||||
sort.Sort(field.matchingElementsAndFields)
|
||||
|
||||
for i := range field.matchingElementsAndFields {
|
||||
element := field.matchingElementsAndFields[i]
|
||||
// sort the elements under a list field by their name
|
||||
sort.Sort(element.matchingElementsAndFields)
|
||||
// set the name of the element to its index
|
||||
element.name = fmt.Sprintf("%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return fieldList, nil
|
||||
}
|
||||
|
||||
// treeField wraps a field node
|
||||
type treeField struct {
|
||||
// name is the name of the node
|
||||
name string
|
||||
|
||||
// value is the value of the node -- may be empty
|
||||
value string
|
||||
|
||||
// matchingElementsAndFields is a slice of fields that go under this as a branch
|
||||
matchingElementsAndFields treeFields
|
||||
|
||||
// subFieldByMatch caches matchingElementsAndFields indexed by the name of the matching elem
|
||||
subFieldByMatch map[string]treeFields
|
||||
}
|
||||
|
||||
// treeFields wraps a slice of treeField so they can be sorted
|
||||
type treeFields []*treeField
|
||||
|
||||
func (nodes treeFields) Len() int { return len(nodes) }
|
||||
|
||||
func (nodes treeFields) Less(i, j int) bool {
|
||||
iIndex, iFound := yaml.FieldOrder[nodes[i].name]
|
||||
jIndex, jFound := yaml.FieldOrder[nodes[j].name]
|
||||
if iFound && jFound {
|
||||
return iIndex < jIndex
|
||||
}
|
||||
if iFound {
|
||||
return true
|
||||
}
|
||||
if jFound {
|
||||
return false
|
||||
}
|
||||
|
||||
if nodes[i].name != nodes[j].name {
|
||||
return nodes[i].name < nodes[j].name
|
||||
}
|
||||
if nodes[i].value != nodes[j].value {
|
||||
return nodes[i].value < nodes[j].value
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (nodes treeFields) Swap(i, j int) { nodes[i], nodes[j] = nodes[j], nodes[i] }
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# Sampling New OpenAPI Data
|
||||
|
||||
[OpenAPI schema]: ./kubernetesapi/
|
||||
[Kustomization schema]: ./kustomizationapi/
|
||||
[kind]: https://hub.docker.com/r/kindest/node/tags
|
||||
|
||||
This document describes how to fetch OpenAPI data from a
|
||||
live kubernetes API server.
|
||||
The scripts used will create a clean [kind] instance for this purpose.
|
||||
|
||||
## Replacing the default openapi schema version
|
||||
|
||||
### Delete all currently built-in schema
|
||||
|
||||
This will remove both the Kustomization and Kubernetes schemas:
|
||||
|
||||
```
|
||||
make nuke
|
||||
```
|
||||
|
||||
### Choose the new version to use
|
||||
|
||||
The compiled-in schema version should maximize API availability with respect to all actively supported Kubernetes versions. For example, while 1.20, 1.21 and 1.22 are the actively supported versions, 1.21 is the best choice. This is because 1.21 introduces at least one new API and does not remove any, while 1.22 removes a large set of long-deprecated APIs that are still supported in 1.20/1.21.
|
||||
|
||||
### Generating additional schema
|
||||
|
||||
If you'd like to change the default schema version, then in the Makefile in this directory, update the `API_VERSION` to your desired version.
|
||||
|
||||
You may need to update the version of Kind these scripts use by changing `KIND_VERSION` in the Makefile in this directory. You can find compatibility information in the [kind release notes](https://github.com/kubernetes-sigs/kind/releases).
|
||||
|
||||
In this directory, fetch the openapi schema, generate the
|
||||
corresponding swagger.go for the kubernetes api, and update `kubernetesapi/openapiinfo.go`:
|
||||
|
||||
```
|
||||
make all
|
||||
```
|
||||
|
||||
If you want to run the steps individually instead of using `make all`, you can run
|
||||
the following commands:
|
||||
|
||||
```
|
||||
make kustomizationapi/swagger.go
|
||||
make kubernetesapi/swagger.go
|
||||
make kubernetesapi/openapiinfo.go
|
||||
```
|
||||
|
||||
You can optionally delete the old `swagger.pb` and `swagger.go` files if we no longer need to support that kubernetes version of
|
||||
openapi data. Make sure you rerun `make kubernetesapi/openapiinfo.go` after deleting any old schemas.
|
||||
|
||||
|
||||
#### Precomputations
|
||||
|
||||
To avoid expensive schema lookups, some functions have precomputed results based on the schema. Unit tests
|
||||
ensure these are kept in sync with the schema; if these tests fail you will need to follow the suggested diff
|
||||
to update the precomputed results.
|
||||
|
||||
### Run all tests
|
||||
|
||||
At the top of the repository, run the tests.
|
||||
|
||||
```
|
||||
make prow-presubmit-check >& /tmp/k.txt; echo $?
|
||||
```
|
||||
|
||||
The exit code should be zero; if not, examine `/tmp/k.txt`.
|
||||
|
||||
## Partial regeneration
|
||||
|
||||
You can also regenerate the kubernetes api schemas specifically with:
|
||||
|
||||
```
|
||||
rm kubernetesapi/swagger.go
|
||||
make kubernetesapi/swagger.go
|
||||
```
|
||||
|
||||
To fetch the schema without generating the swagger.go, you can
|
||||
run:
|
||||
|
||||
```
|
||||
rm kubernetesapi/swagger.pb
|
||||
make kubernetesapi/swagger.pb
|
||||
```
|
||||
|
||||
Note that generating the swagger.go will re-fetch the schema.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2020 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by ./scripts/makeOpenApiInfoDotGo.sh; DO NOT EDIT.
|
||||
|
||||
package kubernetesapi
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi/v1_21_2"
|
||||
)
|
||||
|
||||
const Info = "{title:Kubernetes,version:v1.21.2}"
|
||||
|
||||
var OpenAPIMustAsset = map[string]func(string) []byte{
|
||||
"v1.21.2": v1_21_2.MustAsset,
|
||||
}
|
||||
|
||||
const DefaultOpenAPI = "v1.21.2"
|
||||
+249
File diff suppressed because one or more lines are too long
+44195
File diff suppressed because it is too large
Load Diff
+248
@@ -0,0 +1,248 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated for package kustomizationapi by go-bindata DO NOT EDIT. (@generated)
|
||||
// sources:
|
||||
// kustomizationapi/swagger.json
|
||||
package kustomizationapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gz)
|
||||
clErr := gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||
}
|
||||
if clErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type asset struct {
|
||||
bytes []byte
|
||||
info os.FileInfo
|
||||
}
|
||||
|
||||
type bindataFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
// Name return file name
|
||||
func (fi bindataFileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
// Size return file size
|
||||
func (fi bindataFileInfo) Size() int64 {
|
||||
return fi.size
|
||||
}
|
||||
|
||||
// Mode return file mode
|
||||
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
|
||||
// ModTime return file modify time
|
||||
func (fi bindataFileInfo) ModTime() time.Time {
|
||||
return fi.modTime
|
||||
}
|
||||
|
||||
// IsDir return file whether a directory
|
||||
func (fi bindataFileInfo) IsDir() bool {
|
||||
return fi.mode&os.ModeDir != 0
|
||||
}
|
||||
|
||||
// Sys return file is sys mode
|
||||
func (fi bindataFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _kustomizationapiSwaggerJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x56\xc1\x6e\xdb\x30\x0c\xbd\xe7\x2b\x04\x6d\xc7\xd8\x45\x6e\x43\x6e\xc3\x0e\x3b\x14\x05\x0a\x74\xb7\xa1\x07\xc6\xa1\x5d\xce\x8e\xa4\x51\xb4\xb1\x6c\xc8\xbf\x0f\xd6\x62\xd7\x4a\xec\x75\x0b\x1a\xac\x4b\x0f\x06\x0c\x99\x7c\x4f\xe4\x7b\x24\xfc\x63\xa6\x94\x5e\x63\x4e\x86\x84\xac\xf1\x7a\xa9\xda\x23\xa5\x34\xd9\xb4\x7c\xe7\x53\x70\x94\x82\x73\x3e\x6d\x16\xe9\x07\x6b\x72\x2a\x6e\xc0\xbd\xe7\xe2\x31\x52\x29\xed\xd8\x3a\x64\x21\x1c\x9e\x2a\xa5\x3f\xa2\x41\x06\xb1\x7c\x90\x10\x3e\xbe\x65\xcc\xf5\x52\xe9\x37\x57\x03\xfe\xab\x11\xda\x18\xa5\x87\xd8\xed\xdf\x76\xf3\xee\x1a\xb0\x5e\x07\x14\xa8\x6e\x87\x17\xca\xa1\xf2\xd8\x07\xc9\xd6\x61\x4b\x6b\x57\x5f\x30\x13\xdd\x9f\x7f\x4b\xca\x7a\x85\x6c\x50\xd0\x27\x05\xdb\xda\x25\x0d\xb2\x27\x6b\x92\x92\xcc\x5a\x2f\xd5\xe7\x9e\x3a\xaa\x23\xc4\xb6\x88\x65\xed\xc5\x6e\xe8\x3b\xa6\x59\x68\x54\x28\x84\x6c\x4f\x11\xa2\xf7\x58\x3a\xee\x65\x14\xb2\xa7\x6d\xa3\x9a\xc5\x0a\x05\x16\xc7\x45\xdf\xcf\x06\xa5\x8f\x69\x75\x87\x19\xa3\xbc\x0c\xa1\x1e\xab\xeb\xba\x1f\xe1\x77\x8a\x78\x61\x32\xc5\xa5\x08\x3c\x10\xe0\xf9\xd5\x9d\xd2\x6b\x52\x60\x03\x1b\xf4\x0e\xb2\x3f\x6f\xfe\x3c\x4e\x3e\x25\x6f\x85\x0f\xd0\x90\xe5\x53\x72\xaf\x9b\x5b\x20\xbe\xb3\x35\x67\x78\xba\x23\x63\x94\x0b\x71\x56\x2c\xfe\xf3\x9b\xeb\x7a\x7f\x19\x90\x5f\x50\xbd\xb9\x18\xbf\xd6\xc4\x18\x17\xa4\x3f\x6d\x1d\xde\xa0\x40\xc7\x74\x3f\x7f\xca\x8c\x59\xb7\xfb\xfa\x4a\x0e\x05\x26\xc1\xcd\xa1\xea\x7f\xa3\x7b\xbc\x5d\x07\x20\xbb\xf9\x98\x11\x81\x19\xb6\x71\x27\x23\x4d\x1d\x48\xf6\x90\x6c\x90\x0b\x4c\x4a\xdc\xb6\x29\x61\x26\x9e\xca\xf0\xc2\x20\x58\x84\x84\x90\x3d\xee\x75\x1f\x56\xc5\xd9\x9a\x31\xd8\x44\x2f\xb2\x13\xff\xf5\x30\xc6\xc3\x72\x86\x61\x9c\xd8\x83\x93\xc3\x55\x91\x20\x43\x75\xb4\x33\x27\x5c\x34\xb5\x8b\x7f\x6f\x90\x51\x1b\xe7\x54\x1d\xaf\xea\xf3\xd3\xa2\x69\xfe\x0d\xeb\xeb\xf8\x8f\x89\x0d\x78\xaa\xc1\x67\xed\xb3\xfb\x19\x00\x00\xff\xff\x2f\x39\x79\xd0\x6e\x0c\x00\x00")
|
||||
|
||||
func kustomizationapiSwaggerJsonBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
_kustomizationapiSwaggerJson,
|
||||
"kustomizationapi/swagger.json",
|
||||
)
|
||||
}
|
||||
|
||||
func kustomizationapiSwaggerJson() (*asset, error) {
|
||||
bytes, err := kustomizationapiSwaggerJsonBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "kustomizationapi/swagger.json", size: 3182, mode: os.FileMode(420), modTime: time.Unix(1615228558, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Asset loads and returns the asset for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func Asset(name string) ([]byte, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.bytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
|
||||
// MustAsset is like Asset but panics when Asset would return an error.
|
||||
// It simplifies safe initialization of global variables.
|
||||
func MustAsset(name string) []byte {
|
||||
a, err := Asset(name)
|
||||
if err != nil {
|
||||
panic("asset: Asset(" + name + "): " + err.Error())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// AssetInfo loads and returns the asset info for the given name.
|
||||
// It returns an error if the asset could not be found or
|
||||
// could not be loaded.
|
||||
func AssetInfo(name string) (os.FileInfo, error) {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
if f, ok := _bindata[cannonicalName]; ok {
|
||||
a, err := f()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||
}
|
||||
return a.info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||
}
|
||||
|
||||
// AssetNames returns the names of the assets.
|
||||
func AssetNames() []string {
|
||||
names := make([]string, 0, len(_bindata))
|
||||
for name := range _bindata {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"kustomizationapi/swagger.json": kustomizationapiSwaggerJson,
|
||||
}
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||
// AssetDir("") will return []string{"data"}.
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
node := _bintree
|
||||
if len(name) != 0 {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
pathList := strings.Split(cannonicalName, "/")
|
||||
for _, p := range pathList {
|
||||
node = node.Children[p]
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.Func != nil {
|
||||
return nil, fmt.Errorf("Asset %s not found", name)
|
||||
}
|
||||
rv := make([]string, 0, len(node.Children))
|
||||
for childName := range node.Children {
|
||||
rv = append(rv, childName)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
type bintree struct {
|
||||
Func func() (*asset, error)
|
||||
Children map[string]*bintree
|
||||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"kustomizationapi": &bintree{nil, map[string]*bintree{
|
||||
"swagger.json": &bintree{kustomizationapiSwaggerJson, map[string]*bintree{}},
|
||||
}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory
|
||||
func RestoreAsset(dir, name string) error {
|
||||
data, err := Asset(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := AssetInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreAssets restores an asset under the given directory recursively
|
||||
func RestoreAssets(dir, name string) error {
|
||||
children, err := AssetDir(name)
|
||||
// File
|
||||
if err != nil {
|
||||
return RestoreAsset(dir, name)
|
||||
}
|
||||
// Dir
|
||||
for _, child := range children {
|
||||
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _filePath(dir, name string) string {
|
||||
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"definitions": {
|
||||
"io.k8s.api.apps.v1.ConfigMapArgs": {
|
||||
"properties": {
|
||||
"GeneratorArgs": {
|
||||
"$ref": "#/definitions/io.k8s.api.apps.v1.GeneratorArgs"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "kustomize.config.k8s.io",
|
||||
"kind": "ConfigMapArgs",
|
||||
"version": "v1beta1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"io.k8s.api.apps.v1.SecretArgs": {
|
||||
"properties": {
|
||||
"GeneratorArgs": {
|
||||
"$ref": "#/definitions/io.k8s.api.apps.v1.GeneratorArgs"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "kustomize.config.k8s.io",
|
||||
"kind": "SecretArgs",
|
||||
"version": "v1beta1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"io.k8s.api.apps.v1.GeneratorArgs": {
|
||||
"properties": {
|
||||
"namespace": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"behavior": {
|
||||
"type": "string"
|
||||
},
|
||||
"KvPairSources": {
|
||||
"$ref": "#/definitions/io.k8s.api.apps.v1.KvPairSources"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "kustomize.config.k8s.io",
|
||||
"kind": "GeneratorArgs",
|
||||
"version": "v1beta1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"io.k8s.api.apps.v1.Kustomization": {
|
||||
"required": [
|
||||
"TypeMeta"
|
||||
],
|
||||
"properties": {
|
||||
"configMapGenerator": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/io.k8s.api.apps.v1.ConfigMapArgs"
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
},
|
||||
"secretGenerator": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/io.k8s.api.apps.v1.SecretArgs"
|
||||
},
|
||||
"type": "array",
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "kustomize.config.k8s.io",
|
||||
"kind": "Kustomization",
|
||||
"version": "v1beta1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"io.k8s.api.apps.v1.KvPairSources": {
|
||||
"properties": {
|
||||
"literals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"envs": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"env": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "kustomize.config.k8s.io",
|
||||
"kind": "KvPairSources",
|
||||
"version": "v1beta1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+823
@@ -0,0 +1,823 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi/kustomizationapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
k8syaml "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
// schemaLock is the lock for schema related globals.
|
||||
//
|
||||
// NOTE: This lock helps with preventing panics that might occur due to the data
|
||||
// race that concurrent access on this variable might cause but it doesn't
|
||||
// fully fix the issue described in https://github.com/kubernetes-sigs/kustomize/issues/4824.
|
||||
// For instance concurrently running goroutines where each of them calls SetSchema()
|
||||
// and/or GetSchemaVersion might end up received nil errors (success) whereas the
|
||||
// seconds one would overwrite the global variable that has been written by the
|
||||
// first one.
|
||||
schemaLock sync.RWMutex //nolint:gochecknoglobals
|
||||
|
||||
// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use.
|
||||
kubernetesOpenAPIVersion string //nolint:gochecknoglobals
|
||||
|
||||
// globalSchema contains global state information about the openapi
|
||||
globalSchema openapiData //nolint:gochecknoglobals
|
||||
|
||||
// customSchemaFile stores the custom OpenApi schema if it is provided
|
||||
customSchema []byte //nolint:gochecknoglobals
|
||||
)
|
||||
|
||||
// schemaParseStatus is used in cases when a schema should be parsed, but the
|
||||
// parsing may be delayed to a later time.
|
||||
type schemaParseStatus uint32
|
||||
|
||||
const (
|
||||
schemaNotParsed schemaParseStatus = iota
|
||||
schemaParseDelayed
|
||||
schemaParsed
|
||||
)
|
||||
|
||||
// openapiData contains the parsed openapi state. this is in a struct rather than
|
||||
// a list of vars so that it can be reset from tests.
|
||||
type openapiData struct {
|
||||
// schema holds the OpenAPI schema data
|
||||
schema spec.Schema
|
||||
|
||||
// schemaForResourceType is a map of Resource types to their schemas
|
||||
schemaByResourceType map[yaml.TypeMeta]*spec.Schema
|
||||
|
||||
// namespaceabilityByResourceType stores whether a given Resource type
|
||||
// is namespaceable or not
|
||||
namespaceabilityByResourceType map[yaml.TypeMeta]bool
|
||||
|
||||
// noUseBuiltInSchema stores whether we want to prevent using the built-in
|
||||
// Kubernetes schema as part of the global schema
|
||||
noUseBuiltInSchema bool
|
||||
|
||||
// schemaInit stores whether or not we've parsed the schema already,
|
||||
// so that we only reparse the when necessary (to speed up performance)
|
||||
schemaInit bool
|
||||
|
||||
// defaultBuiltInSchemaParseStatus stores the parse status of the default
|
||||
// built-in schema.
|
||||
defaultBuiltInSchemaParseStatus schemaParseStatus
|
||||
}
|
||||
|
||||
type format string
|
||||
|
||||
const (
|
||||
JsonOrYaml format = "jsonOrYaml"
|
||||
Proto format = "proto"
|
||||
)
|
||||
|
||||
// precomputedIsNamespaceScoped precomputes IsNamespaceScoped for known types. This avoids Schema creation,
|
||||
// which is expensive
|
||||
// The test output from TestIsNamespaceScopedPrecompute shows the expected map in go syntax,and can be copy and pasted
|
||||
// from the failure if it changes.
|
||||
var precomputedIsNamespaceScoped = map[yaml.TypeMeta]bool{
|
||||
{APIVersion: "admissionregistration.k8s.io/v1", Kind: "MutatingWebhookConfiguration"}: false,
|
||||
{APIVersion: "admissionregistration.k8s.io/v1", Kind: "ValidatingWebhookConfiguration"}: false,
|
||||
{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "MutatingWebhookConfiguration"}: false,
|
||||
{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "ValidatingWebhookConfiguration"}: false,
|
||||
{APIVersion: "apiextensions.k8s.io/v1", Kind: "CustomResourceDefinition"}: false,
|
||||
{APIVersion: "apiextensions.k8s.io/v1beta1", Kind: "CustomResourceDefinition"}: false,
|
||||
{APIVersion: "apiregistration.k8s.io/v1", Kind: "APIService"}: false,
|
||||
{APIVersion: "apiregistration.k8s.io/v1beta1", Kind: "APIService"}: false,
|
||||
{APIVersion: "apps/v1", Kind: "ControllerRevision"}: true,
|
||||
{APIVersion: "apps/v1", Kind: "DaemonSet"}: true,
|
||||
{APIVersion: "apps/v1", Kind: "Deployment"}: true,
|
||||
{APIVersion: "apps/v1", Kind: "ReplicaSet"}: true,
|
||||
{APIVersion: "apps/v1", Kind: "StatefulSet"}: true,
|
||||
{APIVersion: "autoscaling/v1", Kind: "HorizontalPodAutoscaler"}: true,
|
||||
{APIVersion: "autoscaling/v1", Kind: "Scale"}: true,
|
||||
{APIVersion: "autoscaling/v2beta1", Kind: "HorizontalPodAutoscaler"}: true,
|
||||
{APIVersion: "autoscaling/v2beta2", Kind: "HorizontalPodAutoscaler"}: true,
|
||||
{APIVersion: "batch/v1", Kind: "CronJob"}: true,
|
||||
{APIVersion: "batch/v1", Kind: "Job"}: true,
|
||||
{APIVersion: "batch/v1beta1", Kind: "CronJob"}: true,
|
||||
{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}: false,
|
||||
{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}: false,
|
||||
{APIVersion: "coordination.k8s.io/v1", Kind: "Lease"}: true,
|
||||
{APIVersion: "coordination.k8s.io/v1beta1", Kind: "Lease"}: true,
|
||||
{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"}: true,
|
||||
{APIVersion: "discovery.k8s.io/v1beta1", Kind: "EndpointSlice"}: true,
|
||||
{APIVersion: "events.k8s.io/v1", Kind: "Event"}: true,
|
||||
{APIVersion: "events.k8s.io/v1beta1", Kind: "Event"}: true,
|
||||
{APIVersion: "extensions/v1beta1", Kind: "Ingress"}: true,
|
||||
{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "FlowSchema"}: false,
|
||||
{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "PriorityLevelConfiguration"}: false,
|
||||
{APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}: true,
|
||||
{APIVersion: "networking.k8s.io/v1", Kind: "IngressClass"}: false,
|
||||
{APIVersion: "networking.k8s.io/v1", Kind: "NetworkPolicy"}: true,
|
||||
{APIVersion: "networking.k8s.io/v1beta1", Kind: "Ingress"}: true,
|
||||
{APIVersion: "networking.k8s.io/v1beta1", Kind: "IngressClass"}: false,
|
||||
{APIVersion: "node.k8s.io/v1", Kind: "RuntimeClass"}: false,
|
||||
{APIVersion: "node.k8s.io/v1beta1", Kind: "RuntimeClass"}: false,
|
||||
{APIVersion: "policy/v1", Kind: "PodDisruptionBudget"}: true,
|
||||
{APIVersion: "policy/v1beta1", Kind: "PodDisruptionBudget"}: true,
|
||||
{APIVersion: "policy/v1beta1", Kind: "PodSecurityPolicy"}: false, // remove after openapi upgrades to v1.25.
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}: false,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRoleBinding"}: false,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}: true,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}: true,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRole"}: false,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRoleBinding"}: false,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "Role"}: true,
|
||||
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "RoleBinding"}: true,
|
||||
{APIVersion: "scheduling.k8s.io/v1", Kind: "PriorityClass"}: false,
|
||||
{APIVersion: "scheduling.k8s.io/v1beta1", Kind: "PriorityClass"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1", Kind: "CSIDriver"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1", Kind: "CSINode"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1", Kind: "StorageClass"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1", Kind: "VolumeAttachment"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIDriver"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSINode"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIStorageCapacity"}: true,
|
||||
{APIVersion: "storage.k8s.io/v1beta1", Kind: "StorageClass"}: false,
|
||||
{APIVersion: "storage.k8s.io/v1beta1", Kind: "VolumeAttachment"}: false,
|
||||
{APIVersion: "v1", Kind: "ComponentStatus"}: false,
|
||||
{APIVersion: "v1", Kind: "ConfigMap"}: true,
|
||||
{APIVersion: "v1", Kind: "Endpoints"}: true,
|
||||
{APIVersion: "v1", Kind: "Event"}: true,
|
||||
{APIVersion: "v1", Kind: "LimitRange"}: true,
|
||||
{APIVersion: "v1", Kind: "Namespace"}: false,
|
||||
{APIVersion: "v1", Kind: "Node"}: false,
|
||||
{APIVersion: "v1", Kind: "NodeProxyOptions"}: false,
|
||||
{APIVersion: "v1", Kind: "PersistentVolume"}: false,
|
||||
{APIVersion: "v1", Kind: "PersistentVolumeClaim"}: true,
|
||||
{APIVersion: "v1", Kind: "Pod"}: true,
|
||||
{APIVersion: "v1", Kind: "PodAttachOptions"}: true,
|
||||
{APIVersion: "v1", Kind: "PodExecOptions"}: true,
|
||||
{APIVersion: "v1", Kind: "PodPortForwardOptions"}: true,
|
||||
{APIVersion: "v1", Kind: "PodProxyOptions"}: true,
|
||||
{APIVersion: "v1", Kind: "PodTemplate"}: true,
|
||||
{APIVersion: "v1", Kind: "ReplicationController"}: true,
|
||||
{APIVersion: "v1", Kind: "ResourceQuota"}: true,
|
||||
{APIVersion: "v1", Kind: "Secret"}: true,
|
||||
{APIVersion: "v1", Kind: "Service"}: true,
|
||||
{APIVersion: "v1", Kind: "ServiceAccount"}: true,
|
||||
{APIVersion: "v1", Kind: "ServiceProxyOptions"}: true,
|
||||
}
|
||||
|
||||
// ResourceSchema wraps the OpenAPI Schema.
|
||||
type ResourceSchema struct {
|
||||
// Schema is the OpenAPI schema for a Resource or field
|
||||
Schema *spec.Schema
|
||||
}
|
||||
|
||||
// IsMissingOrNull returns true if the ResourceSchema is missing or null
|
||||
func (rs *ResourceSchema) IsMissingOrNull() bool {
|
||||
if rs == nil || rs.Schema == nil {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(*rs.Schema, spec.Schema{})
|
||||
}
|
||||
|
||||
// SchemaForResourceType returns the Schema for the given Resource
|
||||
// TODO(pwittrock): create a version of this function that will return a schema
|
||||
// which can be used for duck-typed Resources -- e.g. contains common fields such
|
||||
// as metadata, replicas and spec.template.spec
|
||||
func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema {
|
||||
initSchema()
|
||||
rs, found := globalSchema.schemaByResourceType[t]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return &ResourceSchema{Schema: rs}
|
||||
}
|
||||
|
||||
// SupplementaryOpenAPIFieldName is the conventional field name (JSON/YAML) containing
|
||||
// supplementary OpenAPI definitions.
|
||||
const SupplementaryOpenAPIFieldName = "openAPI"
|
||||
|
||||
const Definitions = "definitions"
|
||||
|
||||
// AddSchemaFromFile reads the file at path and parses the OpenAPI definitions
|
||||
// from the field "openAPI", also returns a function to clean the added definitions
|
||||
// The returned clean function is a no-op on error, or else it's a function
|
||||
// that the caller should use to remove the added openAPI definitions from
|
||||
// global schema
|
||||
func SchemaFromFile(path string) (*spec.Schema, error) {
|
||||
object, err := parseOpenAPI(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return schemaUsingField(object, SupplementaryOpenAPIFieldName)
|
||||
}
|
||||
|
||||
// DefinitionRefs returns the list of openAPI definition references present in the
|
||||
// input openAPIPath
|
||||
func DefinitionRefs(openAPIPath string) ([]string, error) {
|
||||
object, err := parseOpenAPI(openAPIPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return definitionRefsFromRNode(object)
|
||||
}
|
||||
|
||||
// definitionRefsFromRNode returns the list of openAPI definitions keys from input
|
||||
// yaml RNode
|
||||
func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) {
|
||||
definitions, err := object.Pipe(yaml.Lookup(SupplementaryOpenAPIFieldName, Definitions))
|
||||
if definitions == nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return definitions.Fields()
|
||||
}
|
||||
|
||||
// parseOpenAPI reads openAPIPath yaml and converts it to RNode
|
||||
func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) {
|
||||
b, err := os.ReadFile(openAPIPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
object, err := yaml.Parse(string(b))
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("invalid file %q: %v", openAPIPath, err)
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// addSchemaUsingField parses the OpenAPI definitions from the specified field.
|
||||
// If field is the empty string, use the whole document as OpenAPI.
|
||||
func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) {
|
||||
if field != "" {
|
||||
// get the field containing the openAPI
|
||||
m := object.Field(field)
|
||||
if m.IsNilOrEmpty() {
|
||||
// doesn't contain openAPI definitions
|
||||
return nil, nil
|
||||
}
|
||||
object = m.Value
|
||||
}
|
||||
|
||||
oAPI, err := object.String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert the yaml openAPI to a JSON string by unmarshalling it to an
|
||||
// interface{} and the marshalling it to a string
|
||||
var o interface{}
|
||||
err = yaml.Unmarshal([]byte(oAPI), &o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sc spec.Schema
|
||||
err = sc.UnmarshalJSON(j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sc, nil
|
||||
}
|
||||
|
||||
// AddSchema parses s, and adds definitions from s to the global schema.
|
||||
func AddSchema(s []byte) error {
|
||||
return parse(s, JsonOrYaml)
|
||||
}
|
||||
|
||||
// ResetOpenAPI resets the openapi data to empty
|
||||
func ResetOpenAPI() {
|
||||
schemaLock.Lock()
|
||||
defer schemaLock.Unlock()
|
||||
|
||||
globalSchema = openapiData{}
|
||||
customSchema = nil
|
||||
kubernetesOpenAPIVersion = ""
|
||||
}
|
||||
|
||||
// AddDefinitions adds the definitions to the global schema.
|
||||
func AddDefinitions(definitions spec.Definitions) {
|
||||
// initialize values if they have not yet been set
|
||||
if globalSchema.schemaByResourceType == nil {
|
||||
globalSchema.schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{}
|
||||
}
|
||||
if globalSchema.schema.Definitions == nil {
|
||||
globalSchema.schema.Definitions = spec.Definitions{}
|
||||
}
|
||||
|
||||
// index the schema definitions so we can lookup them up for Resources
|
||||
for k := range definitions {
|
||||
// index by GVK, if no GVK is found then it is the schema for a subfield
|
||||
// of a Resource
|
||||
d := definitions[k]
|
||||
|
||||
// copy definitions to the schema
|
||||
globalSchema.schema.Definitions[k] = d
|
||||
gvk, found := d.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
// cast the extension to a []map[string]string
|
||||
exts, ok := gvk.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range exts {
|
||||
typeMeta, ok := toTypeMeta(exts[i])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
globalSchema.schemaByResourceType[typeMeta] = &d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) {
|
||||
m, ok := ext.(map[string]interface{})
|
||||
if !ok {
|
||||
return yaml.TypeMeta{}, false
|
||||
}
|
||||
|
||||
apiVersion := m[versionKey].(string)
|
||||
if g, ok := m[groupKey].(string); ok && g != "" {
|
||||
apiVersion = g + "/" + apiVersion
|
||||
}
|
||||
return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
|
||||
}
|
||||
|
||||
// Resolve resolves the reference against the global schema
|
||||
func Resolve(ref *spec.Ref, schema *spec.Schema) (*spec.Schema, error) {
|
||||
return resolve(schema, ref)
|
||||
}
|
||||
|
||||
// Schema returns the global schema
|
||||
func Schema() *spec.Schema {
|
||||
return rootSchema()
|
||||
}
|
||||
|
||||
// GetSchema parses s into a ResourceSchema, resolving References within the
|
||||
// global schema.
|
||||
func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
|
||||
var sc spec.Schema
|
||||
if err := sc.UnmarshalJSON([]byte(s)); err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if sc.Ref.String() != "" {
|
||||
r, err := Resolve(&sc.Ref, schema)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
sc = *r
|
||||
}
|
||||
|
||||
return &ResourceSchema{Schema: &sc}, nil
|
||||
}
|
||||
|
||||
// IsNamespaceScoped determines whether a resource is namespace or
|
||||
// cluster-scoped by looking at the information in the openapi schema.
|
||||
// The second return value tells whether the provided type could be found
|
||||
// in the openapi schema. If the value is false here, the scope of the
|
||||
// resource is not known. If the type is found, the first return value will
|
||||
// be true if the resource is namespace-scoped, and false if the type is
|
||||
// cluster-scoped.
|
||||
func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
|
||||
if isNamespaceScoped, found := precomputedIsNamespaceScoped[typeMeta]; found {
|
||||
return isNamespaceScoped, found
|
||||
}
|
||||
if isInitSchemaNeededForNamespaceScopeCheck() {
|
||||
initSchema()
|
||||
}
|
||||
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
|
||||
return isNamespaceScoped, found
|
||||
}
|
||||
|
||||
// isInitSchemaNeededForNamespaceScopeCheck returns true if initSchema is needed
|
||||
// to ensure globalSchema.namespaceabilityByResourceType is fully populated for
|
||||
// cases where a custom or non-default built-in schema is in use.
|
||||
func isInitSchemaNeededForNamespaceScopeCheck() bool {
|
||||
schemaLock.Lock()
|
||||
defer schemaLock.Unlock()
|
||||
|
||||
if globalSchema.schemaInit {
|
||||
return false // globalSchema already is initialized.
|
||||
}
|
||||
if customSchema != nil {
|
||||
return true // initSchema is needed.
|
||||
}
|
||||
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
|
||||
// The default built-in schema is in use. Since
|
||||
// precomputedIsNamespaceScoped aligns with the default built-in schema
|
||||
// (verified by TestIsNamespaceScopedPrecompute), there is no need to
|
||||
// call initSchema.
|
||||
if globalSchema.defaultBuiltInSchemaParseStatus == schemaNotParsed {
|
||||
// The schema may be needed for purposes other than namespace scope
|
||||
// checks. Flag it to be parsed when that need arises.
|
||||
globalSchema.defaultBuiltInSchemaParseStatus = schemaParseDelayed
|
||||
}
|
||||
return false
|
||||
}
|
||||
// A non-default built-in schema is in use. initSchema is needed.
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCertainlyClusterScoped returns true for Node, Namespace, etc. and
|
||||
// false for Pod, Deployment, etc. and kinds that aren't recognized in the
|
||||
// openapi data. See:
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces
|
||||
func IsCertainlyClusterScoped(typeMeta yaml.TypeMeta) bool {
|
||||
nsScoped, found := IsNamespaceScoped(typeMeta)
|
||||
return found && !nsScoped
|
||||
}
|
||||
|
||||
// SuppressBuiltInSchemaUse can be called to prevent using the built-in Kubernetes
|
||||
// schema as part of the global schema.
|
||||
// Must be called before the schema is used.
|
||||
func SuppressBuiltInSchemaUse() {
|
||||
globalSchema.noUseBuiltInSchema = true
|
||||
}
|
||||
|
||||
// Elements returns the Schema for the elements of an array.
|
||||
func (rs *ResourceSchema) Elements() *ResourceSchema {
|
||||
// load the schema from swagger files
|
||||
initSchema()
|
||||
|
||||
if len(rs.Schema.Type) != 1 || rs.Schema.Type[0] != "array" {
|
||||
// either not an array, or array has multiple types
|
||||
return nil
|
||||
}
|
||||
if rs == nil || rs.Schema == nil || rs.Schema.Items == nil {
|
||||
// no-scheme for the items
|
||||
return nil
|
||||
}
|
||||
s := *rs.Schema.Items.Schema
|
||||
for s.Ref.String() != "" {
|
||||
sc, e := Resolve(&s.Ref, Schema())
|
||||
if e != nil {
|
||||
return nil
|
||||
}
|
||||
s = *sc
|
||||
}
|
||||
return &ResourceSchema{Schema: &s}
|
||||
}
|
||||
|
||||
const Elements = "[]"
|
||||
|
||||
// Lookup calls either Field or Elements for each item in the path.
|
||||
// If the path item is "[]", then Elements is called, otherwise
|
||||
// Field is called.
|
||||
// If any Field or Elements call returns nil, then Lookup returns
|
||||
// nil immediately.
|
||||
func (rs *ResourceSchema) Lookup(path ...string) *ResourceSchema {
|
||||
s := rs
|
||||
for _, p := range path {
|
||||
if s == nil {
|
||||
break
|
||||
}
|
||||
if p == Elements {
|
||||
s = s.Elements()
|
||||
continue
|
||||
}
|
||||
s = s.Field(p)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Field returns the Schema for a field.
|
||||
func (rs *ResourceSchema) Field(field string) *ResourceSchema {
|
||||
// load the schema from swagger files
|
||||
initSchema()
|
||||
|
||||
// locate the Schema
|
||||
s, found := rs.Schema.Properties[field]
|
||||
switch {
|
||||
case found:
|
||||
// no-op, continue with s as the schema
|
||||
case rs.Schema.AdditionalProperties != nil && rs.Schema.AdditionalProperties.Schema != nil:
|
||||
// map field type -- use Schema of the value
|
||||
// (the key doesn't matter, they all have the same value type)
|
||||
s = *rs.Schema.AdditionalProperties.Schema
|
||||
default:
|
||||
// no Schema found from either swagger files or line comments
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolve the reference to the Schema if the Schema has one
|
||||
for s.Ref.String() != "" {
|
||||
sc, e := Resolve(&s.Ref, Schema())
|
||||
if e != nil {
|
||||
return nil
|
||||
}
|
||||
s = *sc
|
||||
}
|
||||
|
||||
// return the merged Schema
|
||||
return &ResourceSchema{Schema: &s}
|
||||
}
|
||||
|
||||
// PatchStrategyAndKeyList returns the patch strategy and complete merge key list
|
||||
func (rs *ResourceSchema) PatchStrategyAndKeyList() (string, []string) {
|
||||
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
|
||||
if !found {
|
||||
// empty patch strategy
|
||||
return "", []string{}
|
||||
}
|
||||
mkList, found := rs.Schema.Extensions[kubernetesMergeKeyMapList]
|
||||
if found {
|
||||
// mkList is []interface, convert to []string
|
||||
mkListStr := make([]string, len(mkList.([]interface{})))
|
||||
for i, v := range mkList.([]interface{}) {
|
||||
mkListStr[i] = v.(string)
|
||||
}
|
||||
return ps.(string), mkListStr
|
||||
}
|
||||
mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
|
||||
if !found {
|
||||
// no mergeKey -- may be a primitive associative list (e.g. finalizers)
|
||||
return ps.(string), []string{}
|
||||
}
|
||||
return ps.(string), []string{mk.(string)}
|
||||
}
|
||||
|
||||
// PatchStrategyAndKey returns the patch strategy and merge key extensions
|
||||
func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) {
|
||||
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
|
||||
if !found {
|
||||
// empty patch strategy
|
||||
return "", ""
|
||||
}
|
||||
|
||||
mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
|
||||
if !found {
|
||||
// no mergeKey -- may be a primitive associative list (e.g. finalizers)
|
||||
mk = ""
|
||||
}
|
||||
return ps.(string), mk.(string)
|
||||
}
|
||||
|
||||
const (
|
||||
// kubernetesOpenAPIDefaultVersion is the latest version number of the statically compiled in
|
||||
// OpenAPI schema for kubernetes built-in types
|
||||
kubernetesOpenAPIDefaultVersion = kubernetesapi.DefaultOpenAPI
|
||||
|
||||
// kustomizationAPIAssetName is the name of the asset containing the statically compiled in
|
||||
// OpenAPI definitions for Kustomization built-in types
|
||||
kustomizationAPIAssetName = "kustomizationapi/swagger.json"
|
||||
|
||||
// kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension
|
||||
// -- the extension is an array of objects containing a gvk
|
||||
kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind"
|
||||
|
||||
// kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension
|
||||
// -- the extension is a string
|
||||
kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key"
|
||||
|
||||
// kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy
|
||||
// extension -- the extension is a string
|
||||
kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy"
|
||||
|
||||
// kubernetesMergeKeyMapList is the list of merge keys when there needs to be multiple
|
||||
// -- the extension is an array of strings
|
||||
kubernetesMergeKeyMapList = "x-kubernetes-list-map-keys"
|
||||
|
||||
// groupKey is the key to lookup the group from the GVK extension
|
||||
groupKey = "group"
|
||||
// versionKey is the key to lookup the version from the GVK extension
|
||||
versionKey = "version"
|
||||
// kindKey is the to lookup the kind from the GVK extension
|
||||
kindKey = "kind"
|
||||
)
|
||||
|
||||
// SetSchema sets the kubernetes OpenAPI schema version to use
|
||||
func SetSchema(openAPIField map[string]string, schema []byte, reset bool) error {
|
||||
schemaLock.Lock()
|
||||
defer schemaLock.Unlock()
|
||||
|
||||
// this should only be set once
|
||||
schemaIsSet := (kubernetesOpenAPIVersion != "") || customSchema != nil
|
||||
if schemaIsSet && !reset {
|
||||
return nil
|
||||
}
|
||||
|
||||
version, versionProvided := openAPIField["version"]
|
||||
|
||||
// use custom schema
|
||||
if schema != nil {
|
||||
if versionProvided {
|
||||
return fmt.Errorf("builtin version and custom schema provided, cannot use both")
|
||||
}
|
||||
customSchema = schema
|
||||
kubernetesOpenAPIVersion = "custom"
|
||||
// if the schema is changed, initSchema should parse the new schema
|
||||
globalSchema.schemaInit = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// use builtin version
|
||||
kubernetesOpenAPIVersion = version
|
||||
if kubernetesOpenAPIVersion == "" {
|
||||
return nil
|
||||
}
|
||||
if _, ok := kubernetesapi.OpenAPIMustAsset[kubernetesOpenAPIVersion]; !ok {
|
||||
return fmt.Errorf("the specified OpenAPI version is not built in")
|
||||
}
|
||||
|
||||
customSchema = nil
|
||||
// if the schema is changed, initSchema should parse the new schema
|
||||
globalSchema.schemaInit = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSchemaVersion returns what kubernetes OpenAPI version is being used
|
||||
func GetSchemaVersion() string {
|
||||
schemaLock.RLock()
|
||||
defer schemaLock.RUnlock()
|
||||
|
||||
switch {
|
||||
case kubernetesOpenAPIVersion == "" && customSchema == nil:
|
||||
return kubernetesOpenAPIDefaultVersion
|
||||
case customSchema != nil:
|
||||
return "using custom schema from file provided"
|
||||
default:
|
||||
return kubernetesOpenAPIVersion
|
||||
}
|
||||
}
|
||||
|
||||
// initSchema parses the json schema
|
||||
func initSchema() {
|
||||
schemaLock.Lock()
|
||||
defer schemaLock.Unlock()
|
||||
|
||||
if globalSchema.schemaInit {
|
||||
return
|
||||
}
|
||||
globalSchema.schemaInit = true
|
||||
|
||||
// TODO(natasha41575): Accept proto-formatted schema files
|
||||
if customSchema != nil {
|
||||
err := parse(customSchema, JsonOrYaml)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid schema file: %w", err))
|
||||
}
|
||||
} else {
|
||||
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
|
||||
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
|
||||
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
|
||||
} else {
|
||||
parseBuiltinSchema(kubernetesOpenAPIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if globalSchema.defaultBuiltInSchemaParseStatus == schemaParseDelayed {
|
||||
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
|
||||
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
|
||||
}
|
||||
|
||||
if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName), JsonOrYaml); err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseBuiltinSchema calls parse to parse the json or proto schemas
|
||||
func parseBuiltinSchema(version string) {
|
||||
if globalSchema.noUseBuiltInSchema {
|
||||
// don't parse the built in schema
|
||||
return
|
||||
}
|
||||
// parse the swagger, this should never fail
|
||||
assetName := filepath.Join(
|
||||
"kubernetesapi",
|
||||
strings.ReplaceAll(version, ".", "_"),
|
||||
"swagger.pb")
|
||||
|
||||
if err := parse(kubernetesapi.OpenAPIMustAsset[version](assetName), Proto); err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// parse parses and indexes a single json or proto schema
|
||||
func parse(b []byte, format format) error {
|
||||
var swagger spec.Swagger
|
||||
switch {
|
||||
case format == Proto:
|
||||
doc := &openapi_v2.Document{}
|
||||
// We parse protobuf and get an openapi_v2.Document here.
|
||||
if err := proto.Unmarshal(b, doc); err != nil {
|
||||
return fmt.Errorf("openapi proto unmarshalling failed: %w", err)
|
||||
}
|
||||
// convert the openapi_v2.Document back to Swagger
|
||||
_, err := swagger.FromGnostic(doc)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
case format == JsonOrYaml:
|
||||
if len(b) > 0 && b[0] != byte('{') {
|
||||
var err error
|
||||
b, err = k8syaml.YAMLToJSON(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
if err := swagger.UnmarshalJSON(b); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
AddDefinitions(swagger.Definitions)
|
||||
findNamespaceability(swagger.Paths)
|
||||
return nil
|
||||
}
|
||||
|
||||
// findNamespaceability looks at the api paths for the resource to determine
|
||||
// if it is cluster-scoped or namespace-scoped. The gvk of the resource
|
||||
// for each path is found by looking at the x-kubernetes-group-version-kind
|
||||
// extension. If a path exists for the resource that contains a namespace path
|
||||
// parameter, the resource is namespace-scoped.
|
||||
func findNamespaceability(paths *spec.Paths) {
|
||||
if globalSchema.namespaceabilityByResourceType == nil {
|
||||
globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool)
|
||||
}
|
||||
|
||||
if paths == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for path, pathInfo := range paths.Paths {
|
||||
if pathInfo.Get == nil {
|
||||
continue
|
||||
}
|
||||
gvk, found := pathInfo.Get.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
typeMeta, found := toTypeMeta(gvk)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(path, "namespaces/{namespace}") {
|
||||
// if we find a namespace path parameter, we just update the map
|
||||
// directly
|
||||
globalSchema.namespaceabilityByResourceType[typeMeta] = true
|
||||
} else if _, found := globalSchema.namespaceabilityByResourceType[typeMeta]; !found {
|
||||
// if the resource doesn't have the namespace path parameter, we
|
||||
// only add it to the map if it doesn't already exist.
|
||||
globalSchema.namespaceabilityByResourceType[typeMeta] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) {
|
||||
if s, ok := root.(*spec.Schema); ok && s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res, _, err := ref.GetPointer().Get(root)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
switch sch := res.(type) {
|
||||
case spec.Schema:
|
||||
return &sch, nil
|
||||
case *spec.Schema:
|
||||
return sch, nil
|
||||
case map[string]interface{}:
|
||||
b, err := json.Marshal(sch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newSch := new(spec.Schema)
|
||||
if err = json.Unmarshal(b, newSch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSch, nil
|
||||
default:
|
||||
return nil, errors.Wrap(fmt.Errorf("unknown type for the resolved reference"))
|
||||
}
|
||||
}
|
||||
|
||||
func rootSchema() *spec.Schema {
|
||||
initSchema()
|
||||
return &globalSchema.schema
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package order
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// SyncOrder recursively sorts the map node keys in 'to' node to match the order of
|
||||
// map node keys in 'from' node at same tree depth, additional keys are moved to the end
|
||||
// Field order might be altered due to round-tripping in arbitrary functions.
|
||||
// This functionality helps to retain the original order of fields to avoid unnecessary diffs.
|
||||
func SyncOrder(from, to *yaml.RNode) error {
|
||||
// from node should not be modified, it should be just used as a reference
|
||||
fromCopy := from.Copy()
|
||||
if err := syncOrder(fromCopy, to); err != nil {
|
||||
return errors.Errorf("failed to sync field order: %q", err.Error())
|
||||
}
|
||||
rearrangeHeadCommentOfSeqNode(to.YNode())
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncOrder(from, to *yaml.RNode) error {
|
||||
if from.IsNilOrEmpty() || to.IsNilOrEmpty() {
|
||||
return nil
|
||||
}
|
||||
switch from.YNode().Kind {
|
||||
case yaml.DocumentNode:
|
||||
// Traverse the child of the documents
|
||||
return syncOrder(yaml.NewRNode(from.YNode()), yaml.NewRNode(to.YNode()))
|
||||
case yaml.MappingNode:
|
||||
return VisitFields(from, to, func(fNode, tNode *yaml.MapNode) error {
|
||||
// Traverse each field value
|
||||
if fNode == nil || tNode == nil {
|
||||
return nil
|
||||
}
|
||||
return syncOrder(fNode.Value, tNode.Value)
|
||||
})
|
||||
case yaml.SequenceNode:
|
||||
return VisitElements(from, to, syncOrder) // Traverse each list element
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitElements calls fn for each element in a SequenceNode.
|
||||
// Returns an error for non-SequenceNodes
|
||||
func VisitElements(from, to *yaml.RNode, fn func(fNode, tNode *yaml.RNode) error) error {
|
||||
fElements, err := from.Elements()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
tElements, err := to.Elements()
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
for i := range fElements {
|
||||
if i >= len(tElements) {
|
||||
return nil
|
||||
}
|
||||
if err := fn(fElements[i], tElements[i]); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitFields calls fn for each field in the RNode.
|
||||
// Returns an error for non-MappingNodes.
|
||||
func VisitFields(from, to *yaml.RNode, fn func(fNode, tNode *yaml.MapNode) error) error {
|
||||
srcFieldNames, err := from.Fields()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
yaml.SyncMapNodesOrder(from, to)
|
||||
// visit each field
|
||||
for _, fieldName := range srcFieldNames {
|
||||
if err := fn(from.Field(fieldName), to.Field(fieldName)); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rearrangeHeadCommentOfSeqNode addresses a remote corner case due to moving a
|
||||
// map node in a sequence node with a head comment to the top
|
||||
func rearrangeHeadCommentOfSeqNode(node *yaml.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
switch node.Kind {
|
||||
case yaml.DocumentNode:
|
||||
for _, node := range node.Content {
|
||||
rearrangeHeadCommentOfSeqNode(node)
|
||||
}
|
||||
|
||||
case yaml.MappingNode:
|
||||
for _, node := range node.Content {
|
||||
rearrangeHeadCommentOfSeqNode(node)
|
||||
}
|
||||
|
||||
case yaml.SequenceNode:
|
||||
for _, node := range node.Content {
|
||||
// for each child mapping node, transfer the head comment of it's
|
||||
// first child scalar node to the head comment of itself
|
||||
if len(node.Content) > 0 && node.Content[0].Kind == yaml.ScalarNode {
|
||||
if node.HeadComment == "" {
|
||||
node.HeadComment = node.Content[0].HeadComment
|
||||
continue
|
||||
}
|
||||
|
||||
if node.Content[0].HeadComment != "" {
|
||||
node.HeadComment += "\n" + node.Content[0].HeadComment
|
||||
node.Content[0].HeadComment = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resid
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Gvk identifies a Kubernetes API type.
|
||||
// https://git.k8s.io/design-proposals-archive/api-machinery/api-group.md
|
||||
type Gvk struct {
|
||||
Group string `json:"group,omitempty" yaml:"group,omitempty"`
|
||||
Version string `json:"version,omitempty" yaml:"version,omitempty"`
|
||||
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
|
||||
// isClusterScoped is true if the object is known, per the openapi
|
||||
// data in use, to be cluster scoped, and false otherwise.
|
||||
isClusterScoped bool
|
||||
}
|
||||
|
||||
func NewGvk(g, v, k string) Gvk {
|
||||
result := Gvk{Group: g, Version: v, Kind: k}
|
||||
result.isClusterScoped =
|
||||
openapi.IsCertainlyClusterScoped(result.AsTypeMeta())
|
||||
return result
|
||||
}
|
||||
|
||||
func GvkFromNode(r *yaml.RNode) Gvk {
|
||||
g, v := ParseGroupVersion(r.GetApiVersion())
|
||||
return NewGvk(g, v, r.GetKind())
|
||||
}
|
||||
|
||||
// FromKind makes a Gvk with only the kind specified.
|
||||
func FromKind(k string) Gvk {
|
||||
return NewGvk("", "", k)
|
||||
}
|
||||
|
||||
// ParseGroupVersion parses a KRM metadata apiVersion field.
|
||||
func ParseGroupVersion(apiVersion string) (group, version string) {
|
||||
if i := strings.Index(apiVersion, "/"); i > -1 {
|
||||
return apiVersion[:i], apiVersion[i+1:]
|
||||
}
|
||||
return "", apiVersion
|
||||
}
|
||||
|
||||
// GvkFromString makes a Gvk from the output of Gvk.String().
|
||||
func GvkFromString(s string) Gvk {
|
||||
values := strings.Split(s, fieldSep)
|
||||
if len(values) < 3 {
|
||||
// ...then the string didn't come from Gvk.String().
|
||||
return Gvk{
|
||||
Group: noGroup,
|
||||
Version: noVersion,
|
||||
Kind: noKind,
|
||||
}
|
||||
}
|
||||
k := values[0]
|
||||
if k == noKind {
|
||||
k = ""
|
||||
}
|
||||
v := values[1]
|
||||
if v == noVersion {
|
||||
v = ""
|
||||
}
|
||||
g := strings.Join(values[2:], fieldSep)
|
||||
if g == noGroup {
|
||||
g = ""
|
||||
}
|
||||
return NewGvk(g, v, k)
|
||||
}
|
||||
|
||||
// Values that are brief but meaningful in logs.
|
||||
const (
|
||||
noGroup = "[noGrp]"
|
||||
noVersion = "[noVer]"
|
||||
noKind = "[noKind]"
|
||||
fieldSep = "."
|
||||
)
|
||||
|
||||
// String returns a string representation of the GVK.
|
||||
func (x Gvk) String() string {
|
||||
g := x.Group
|
||||
if g == "" {
|
||||
g = noGroup
|
||||
}
|
||||
v := x.Version
|
||||
if v == "" {
|
||||
v = noVersion
|
||||
}
|
||||
k := x.Kind
|
||||
if k == "" {
|
||||
k = noKind
|
||||
}
|
||||
return strings.Join([]string{k, v, g}, fieldSep)
|
||||
}
|
||||
|
||||
// stableSortString returns a GVK representation that ensures determinism and
|
||||
// backwards-compatibility in testing, logging, ...
|
||||
func (x Gvk) stableSortString() string {
|
||||
stableNoGroup := "~G"
|
||||
stableNoVersion := "~V"
|
||||
stableNoKind := "~K"
|
||||
stableFieldSeparator := "_"
|
||||
|
||||
g := x.Group
|
||||
if g == "" {
|
||||
g = stableNoGroup
|
||||
}
|
||||
v := x.Version
|
||||
if v == "" {
|
||||
v = stableNoVersion
|
||||
}
|
||||
k := x.Kind
|
||||
if k == "" {
|
||||
k = stableNoKind
|
||||
}
|
||||
return strings.Join([]string{g, v, k}, stableFieldSeparator)
|
||||
}
|
||||
|
||||
// ApiVersion returns the combination of Group and Version
|
||||
func (x Gvk) ApiVersion() string {
|
||||
if x.Group != "" {
|
||||
return x.Group + "/" + x.Version
|
||||
}
|
||||
return x.Version
|
||||
}
|
||||
|
||||
// StringWoEmptyField returns a string representation of the GVK. Non-exist
|
||||
// fields will be omitted. This is called when generating a filename for the
|
||||
// resource.
|
||||
func (x Gvk) StringWoEmptyField() string {
|
||||
var s []string
|
||||
if x.Group != "" {
|
||||
s = append(s, x.Group)
|
||||
}
|
||||
if x.Version != "" {
|
||||
s = append(s, x.Version)
|
||||
}
|
||||
if x.Kind != "" {
|
||||
s = append(s, x.Kind)
|
||||
}
|
||||
return strings.Join(s, "_")
|
||||
}
|
||||
|
||||
// Equals returns true if the Gvk's have equal fields.
|
||||
func (x Gvk) Equals(o Gvk) bool {
|
||||
return x.Group == o.Group && x.Version == o.Version && x.Kind == o.Kind
|
||||
}
|
||||
|
||||
// An attempt to order things to help k8s, e.g.
|
||||
// a Service should come before things that refer to it.
|
||||
// Namespace should be first.
|
||||
// In some cases order just specified to provide determinism.
|
||||
var orderFirst = []string{
|
||||
"Namespace",
|
||||
"ResourceQuota",
|
||||
"StorageClass",
|
||||
"CustomResourceDefinition",
|
||||
"ServiceAccount",
|
||||
"PodSecurityPolicy",
|
||||
"Role",
|
||||
"ClusterRole",
|
||||
"RoleBinding",
|
||||
"ClusterRoleBinding",
|
||||
"ConfigMap",
|
||||
"Secret",
|
||||
"Endpoints",
|
||||
"Service",
|
||||
"LimitRange",
|
||||
"PriorityClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"Deployment",
|
||||
"StatefulSet",
|
||||
"CronJob",
|
||||
"PodDisruptionBudget",
|
||||
}
|
||||
var orderLast = []string{
|
||||
"MutatingWebhookConfiguration",
|
||||
"ValidatingWebhookConfiguration",
|
||||
}
|
||||
var typeOrders = func() map[string]int {
|
||||
m := map[string]int{}
|
||||
for i, n := range orderFirst {
|
||||
m[n] = -len(orderFirst) + i
|
||||
}
|
||||
for i, n := range orderLast {
|
||||
m[n] = 1 + i
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// IsLessThan returns true if self is less than the argument.
|
||||
func (x Gvk) IsLessThan(o Gvk) bool {
|
||||
indexI := typeOrders[x.Kind]
|
||||
indexJ := typeOrders[o.Kind]
|
||||
if indexI != indexJ {
|
||||
return indexI < indexJ
|
||||
}
|
||||
return x.stableSortString() < o.stableSortString()
|
||||
}
|
||||
|
||||
// IsSelected returns true if `selector` selects `x`; otherwise, false.
|
||||
// If `selector` and `x` are the same, return true.
|
||||
// If `selector` is nil, it is considered a wildcard match, returning true.
|
||||
// If selector fields are empty, they are considered wildcards matching
|
||||
// anything in the corresponding fields, e.g.
|
||||
//
|
||||
// this item:
|
||||
// <Group: "extensions", Version: "v1beta1", Kind: "Deployment">
|
||||
//
|
||||
// is selected by
|
||||
// <Group: "", Version: "", Kind: "Deployment">
|
||||
//
|
||||
// but rejected by
|
||||
// <Group: "apps", Version: "", Kind: "Deployment">
|
||||
//
|
||||
func (x Gvk) IsSelected(selector *Gvk) bool {
|
||||
if selector == nil {
|
||||
return true
|
||||
}
|
||||
if len(selector.Group) > 0 {
|
||||
if x.Group != selector.Group {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(selector.Version) > 0 {
|
||||
if x.Version != selector.Version {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(selector.Kind) > 0 {
|
||||
if x.Kind != selector.Kind {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AsTypeMeta returns a yaml.TypeMeta from x's information.
|
||||
func (x Gvk) AsTypeMeta() yaml.TypeMeta {
|
||||
return yaml.TypeMeta{
|
||||
APIVersion: x.ApiVersion(),
|
||||
Kind: x.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
// IsClusterScoped returns true if the Gvk is certainly cluster scoped
|
||||
// with respect to the available openapi data.
|
||||
func (x Gvk) IsClusterScoped() bool {
|
||||
return x.isClusterScoped
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package resid
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// ResId is an identifier of a k8s resource object.
|
||||
type ResId struct {
|
||||
// Gvk of the resource.
|
||||
Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
|
||||
|
||||
// Name of the resource.
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
|
||||
// Namespace the resource belongs to, if it can belong to a namespace.
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// NewResIdWithNamespace creates new ResId
|
||||
// in a given namespace.
|
||||
func NewResIdWithNamespace(k Gvk, n, ns string) ResId {
|
||||
return ResId{Gvk: k, Name: n, Namespace: ns}
|
||||
}
|
||||
|
||||
// NewResId creates new ResId.
|
||||
func NewResId(k Gvk, n string) ResId {
|
||||
return NewResIdWithNamespace(k, n, "")
|
||||
}
|
||||
|
||||
// NewResIdKindOnly creates a new ResId.
|
||||
func NewResIdKindOnly(k string, n string) ResId {
|
||||
return NewResId(FromKind(k), n)
|
||||
}
|
||||
|
||||
const (
|
||||
noNamespace = "[noNs]"
|
||||
noName = "[noName]"
|
||||
separator = "/"
|
||||
TotallyNotANamespace = "_non_namespaceable_"
|
||||
DefaultNamespace = "default"
|
||||
)
|
||||
|
||||
// String of ResId based on GVK, name and prefix
|
||||
func (id ResId) String() string {
|
||||
ns := id.Namespace
|
||||
if ns == "" {
|
||||
ns = noNamespace
|
||||
}
|
||||
nm := id.Name
|
||||
if nm == "" {
|
||||
nm = noName
|
||||
}
|
||||
return strings.Join(
|
||||
[]string{id.Gvk.String(), strings.Join([]string{nm, ns}, fieldSep)}, separator)
|
||||
}
|
||||
|
||||
func FromString(s string) ResId {
|
||||
values := strings.Split(s, separator)
|
||||
gvk := GvkFromString(values[0])
|
||||
|
||||
values = strings.Split(values[1], fieldSep)
|
||||
last := len(values) - 1
|
||||
|
||||
ns := values[last]
|
||||
if ns == noNamespace {
|
||||
ns = ""
|
||||
}
|
||||
nm := strings.Join(values[:last], fieldSep)
|
||||
if nm == noName {
|
||||
nm = ""
|
||||
}
|
||||
return ResId{
|
||||
Gvk: gvk,
|
||||
Namespace: ns,
|
||||
Name: nm,
|
||||
}
|
||||
}
|
||||
|
||||
// FromRNode returns the ResId for the RNode
|
||||
func FromRNode(rn *yaml.RNode) ResId {
|
||||
group, version := ParseGroupVersion(rn.GetApiVersion())
|
||||
return NewResIdWithNamespace(
|
||||
Gvk{Group: group, Version: version, Kind: rn.GetKind()}, rn.GetName(), rn.GetNamespace())
|
||||
}
|
||||
|
||||
// GvknEquals returns true if the other id matches
|
||||
// Group/Version/Kind/name.
|
||||
func (id ResId) GvknEquals(o ResId) bool {
|
||||
return id.Name == o.Name && id.Gvk.Equals(o.Gvk)
|
||||
}
|
||||
|
||||
// IsSelectedBy returns true if self is selected by the argument.
|
||||
func (id ResId) IsSelectedBy(selector ResId) bool {
|
||||
return (selector.Name == "" || selector.Name == id.Name) &&
|
||||
(selector.Namespace == "" || selector.IsNsEquals(id)) &&
|
||||
id.Gvk.IsSelected(&selector.Gvk)
|
||||
}
|
||||
|
||||
// Equals returns true if the other id matches
|
||||
// namespace/Group/Version/Kind/name.
|
||||
func (id ResId) Equals(o ResId) bool {
|
||||
return id.IsNsEquals(o) && id.GvknEquals(o)
|
||||
}
|
||||
|
||||
// IsNsEquals returns true if the id is in
|
||||
// the same effective namespace.
|
||||
func (id ResId) IsNsEquals(o ResId) bool {
|
||||
return id.EffectiveNamespace() == o.EffectiveNamespace()
|
||||
}
|
||||
|
||||
// IsInDefaultNs returns true if id is a namespaceable
|
||||
// ResId and the Namespace is either not set or set
|
||||
// to DefaultNamespace.
|
||||
func (id ResId) IsInDefaultNs() bool {
|
||||
return !id.IsClusterScoped() && id.isPutativelyDefaultNs()
|
||||
}
|
||||
|
||||
func (id ResId) isPutativelyDefaultNs() bool {
|
||||
return id.Namespace == "" || id.Namespace == DefaultNamespace
|
||||
}
|
||||
|
||||
// EffectiveNamespace returns a non-ambiguous, non-empty
|
||||
// namespace for use in reporting and equality tests.
|
||||
func (id ResId) EffectiveNamespace() string {
|
||||
// The order of these checks matters.
|
||||
if id.IsClusterScoped() {
|
||||
return TotallyNotANamespace
|
||||
}
|
||||
if id.isPutativelyDefaultNs() {
|
||||
return DefaultNamespace
|
||||
}
|
||||
return id.Namespace
|
||||
}
|
||||
|
||||
// IsEmpty returns true of all of the id's fields are
|
||||
// empty strings
|
||||
func (id ResId) IsEmpty() bool {
|
||||
return reflect.DeepEqual(id, ResId{})
|
||||
}
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package runfn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/container"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/exec"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// RunFns runs the set of configuration functions in a local directory against
|
||||
// the Resources in that directory
|
||||
type RunFns struct {
|
||||
StorageMounts []runtimeutil.StorageMount
|
||||
|
||||
// Path is the path to the directory containing functions
|
||||
Path string
|
||||
|
||||
// FunctionPaths Paths allows functions to be specified outside the configuration
|
||||
// directory.
|
||||
// Functions provided on FunctionPaths are globally scoped.
|
||||
// If FunctionPaths length is > 0, then NoFunctionsFromInput defaults to true
|
||||
FunctionPaths []string
|
||||
|
||||
// Functions is an explicit list of functions to run against the input.
|
||||
// Functions provided on Functions are globally scoped.
|
||||
// If Functions length is > 0, then NoFunctionsFromInput defaults to true
|
||||
Functions []*yaml.RNode
|
||||
|
||||
// GlobalScope if true, functions read from input will be scoped globally rather
|
||||
// than only to Resources under their subdirs.
|
||||
GlobalScope bool
|
||||
|
||||
// Input can be set to read the Resources from Input rather than from a directory
|
||||
Input io.Reader
|
||||
|
||||
// Network enables network access for functions that declare it
|
||||
Network bool
|
||||
|
||||
// Output can be set to write the result to Output rather than back to the directory
|
||||
Output io.Writer
|
||||
|
||||
// NoFunctionsFromInput if set to true will not read any functions from the input,
|
||||
// and only use explicit sources
|
||||
NoFunctionsFromInput *bool
|
||||
|
||||
// EnableExec will enable exec functions
|
||||
EnableExec bool
|
||||
|
||||
// DisableContainers will disable functions run as containers
|
||||
DisableContainers bool
|
||||
|
||||
// ResultsDir is where to write each functions results
|
||||
ResultsDir string
|
||||
|
||||
// LogSteps enables logging the function that is running.
|
||||
LogSteps bool
|
||||
|
||||
// LogWriter can be set to write the logs to LogWriter rather than stderr if LogSteps is enabled.
|
||||
LogWriter io.Writer
|
||||
|
||||
// resultsCount is used to generate the results filename for each container
|
||||
resultsCount uint32
|
||||
|
||||
// functionFilterProvider provides a filter to perform the function.
|
||||
// this is a variable so it can be mocked in tests
|
||||
functionFilterProvider func(
|
||||
filter runtimeutil.FunctionSpec, api *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error)
|
||||
|
||||
// AsCurrentUser is a boolean to indicate whether docker container should use
|
||||
// the uid and gid that run the command
|
||||
AsCurrentUser bool
|
||||
|
||||
// Env contains environment variables that will be exported to container
|
||||
Env []string
|
||||
|
||||
// ContinueOnEmptyResult configures what happens when the underlying pipeline
|
||||
// returns an empty result.
|
||||
// If it is false (default), subsequent functions will be skipped and the
|
||||
// result will be returned immediately.
|
||||
// If it is true, the empty result will be provided as input to the next
|
||||
// function in the list.
|
||||
ContinueOnEmptyResult bool
|
||||
|
||||
// WorkingDir specifies which working directory an exec function should run in.
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
// Execute runs the command
|
||||
func (r RunFns) Execute() error {
|
||||
// make the path absolute so it works on mac
|
||||
var err error
|
||||
r.Path, err = filepath.Abs(r.Path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
|
||||
// default the containerFilterProvider if it hasn't been override. Split out for testing.
|
||||
(&r).init()
|
||||
nodes, fltrs, output, err := r.getNodesAndFilters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.runFunctions(nodes, output, fltrs)
|
||||
}
|
||||
|
||||
func (r RunFns) getNodesAndFilters() (
|
||||
*kio.PackageBuffer, []kio.Filter, *kio.LocalPackageReadWriter, error) {
|
||||
// Read Resources from Directory or Input
|
||||
buff := &kio.PackageBuffer{}
|
||||
p := kio.Pipeline{Outputs: []kio.Writer{buff}}
|
||||
// save the output dir because we will need it to write back
|
||||
// the same one for reading must be used for writing if deleting Resources
|
||||
var outputPkg *kio.LocalPackageReadWriter
|
||||
if r.Path != "" {
|
||||
outputPkg = &kio.LocalPackageReadWriter{PackagePath: r.Path, MatchFilesGlob: kio.MatchAll}
|
||||
}
|
||||
|
||||
if r.Input == nil {
|
||||
p.Inputs = []kio.Reader{outputPkg}
|
||||
} else {
|
||||
p.Inputs = []kio.Reader{&kio.ByteReader{Reader: r.Input}}
|
||||
}
|
||||
if err := p.Execute(); err != nil {
|
||||
return nil, nil, outputPkg, err
|
||||
}
|
||||
|
||||
fltrs, err := r.getFilters(buff.Nodes)
|
||||
if err != nil {
|
||||
return nil, nil, outputPkg, err
|
||||
}
|
||||
return buff, fltrs, outputPkg, nil
|
||||
}
|
||||
|
||||
func (r RunFns) getFilters(nodes []*yaml.RNode) ([]kio.Filter, error) {
|
||||
var fltrs []kio.Filter
|
||||
|
||||
// fns from annotations on the input resources
|
||||
f, err := r.getFunctionsFromInput(nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fltrs = append(fltrs, f...)
|
||||
|
||||
// fns from directories specified on the struct
|
||||
f, err = r.getFunctionsFromFunctionPaths()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fltrs = append(fltrs, f...)
|
||||
|
||||
// explicit fns specified on the struct
|
||||
f, err = r.getFunctionsFromFunctions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fltrs = append(fltrs, f...)
|
||||
|
||||
return fltrs, nil
|
||||
}
|
||||
|
||||
// runFunctions runs the fltrs against the input and writes to either r.Output or output
|
||||
func (r RunFns) runFunctions(
|
||||
input kio.Reader, output kio.Writer, fltrs []kio.Filter) error {
|
||||
// use the previously read Resources as input
|
||||
var outputs []kio.Writer
|
||||
if r.Output == nil {
|
||||
// write back to the package
|
||||
outputs = append(outputs, output)
|
||||
} else {
|
||||
// write to the output instead of the directory if r.Output is specified or
|
||||
// the output is nil (reading from Input)
|
||||
outputs = append(outputs, kio.ByteWriter{Writer: r.Output})
|
||||
}
|
||||
|
||||
var err error
|
||||
pipeline := kio.Pipeline{
|
||||
Inputs: []kio.Reader{input},
|
||||
Filters: fltrs,
|
||||
Outputs: outputs,
|
||||
ContinueOnEmptyResult: r.ContinueOnEmptyResult,
|
||||
}
|
||||
if r.LogSteps {
|
||||
err = pipeline.ExecuteWithCallback(func(op kio.Filter) {
|
||||
var identifier string
|
||||
|
||||
switch filter := op.(type) {
|
||||
case *container.Filter:
|
||||
identifier = filter.Image
|
||||
case *exec.Filter:
|
||||
identifier = filter.Path
|
||||
default:
|
||||
identifier = "unknown-type function"
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(r.LogWriter, "Running %s\n", identifier)
|
||||
})
|
||||
} else {
|
||||
err = pipeline.Execute()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check for deferred function errors
|
||||
var errs []string
|
||||
for i := range fltrs {
|
||||
cf, ok := fltrs[i].(runtimeutil.DeferFailureFunction)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if cf.GetExit() != nil {
|
||||
errs = append(errs, cf.GetExit().Error())
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n---\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFunctionsFromInput scans the input for functions and runs them
|
||||
func (r RunFns) getFunctionsFromInput(nodes []*yaml.RNode) ([]kio.Filter, error) {
|
||||
if *r.NoFunctionsFromInput {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buff := &kio.PackageBuffer{}
|
||||
err := kio.Pipeline{
|
||||
Inputs: []kio.Reader{&kio.PackageBuffer{Nodes: nodes}},
|
||||
Filters: []kio.Filter{&runtimeutil.IsReconcilerFilter{}},
|
||||
Outputs: []kio.Writer{buff},
|
||||
}.Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = sortFns(buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.getFunctionFilters(false, buff.Nodes...)
|
||||
}
|
||||
|
||||
// getFunctionsFromFunctionPaths returns the set of functions read from r.FunctionPaths
|
||||
// as a slice of Filters
|
||||
func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) {
|
||||
buff := &kio.PackageBuffer{}
|
||||
for i := range r.FunctionPaths {
|
||||
err := kio.Pipeline{
|
||||
Inputs: []kio.Reader{
|
||||
kio.LocalPackageReader{PackagePath: r.FunctionPaths[i]},
|
||||
},
|
||||
Outputs: []kio.Writer{buff},
|
||||
}.Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r.getFunctionFilters(true, buff.Nodes...)
|
||||
}
|
||||
|
||||
// getFunctionsFromFunctions returns the set of explicitly provided functions as
|
||||
// Filters
|
||||
func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) {
|
||||
return r.getFunctionFilters(true, r.Functions...)
|
||||
}
|
||||
|
||||
// mergeContainerEnv is container-specific and will merge the envs specified by command line (imperative)
|
||||
// and config file (declarative). If they have same key, the imperative value will be respected.
|
||||
func (r RunFns) mergeContainerEnv(envs []string) []string {
|
||||
imperative := runtimeutil.NewContainerEnvFromStringSlice(r.Env)
|
||||
declarative := runtimeutil.NewContainerEnvFromStringSlice(envs)
|
||||
for key, value := range imperative.EnvVars {
|
||||
declarative.AddKeyValue(key, value)
|
||||
}
|
||||
|
||||
for _, key := range imperative.VarsToExport {
|
||||
declarative.AddKey(key)
|
||||
}
|
||||
|
||||
return declarative.Raw()
|
||||
}
|
||||
|
||||
// mergeExecEnv will merge the envs specified by command line (imperative) and config
|
||||
// file (declarative). If they have same key, the imperative value will be respected.
|
||||
func (r RunFns) mergeExecEnv(envs []string) []string {
|
||||
envMap := map[string]string{}
|
||||
|
||||
for _, env := range append(envs, r.Env...) {
|
||||
res := strings.Split(env, "=")
|
||||
//nolint:gomnd
|
||||
if len(res) == 2 {
|
||||
envMap[res[0]] = res[1]
|
||||
}
|
||||
}
|
||||
|
||||
mergedEnv := []string{}
|
||||
for key, value := range envMap {
|
||||
mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
// Sort the envs to make the output deterministic
|
||||
sort.Strings(mergedEnv)
|
||||
return mergedEnv
|
||||
}
|
||||
|
||||
func (r RunFns) getFunctionFilters(global bool, fns ...*yaml.RNode) (
|
||||
[]kio.Filter, error) {
|
||||
var fltrs []kio.Filter
|
||||
for i := range fns {
|
||||
api := fns[i]
|
||||
spec, err := runtimeutil.GetFunctionSpec(api)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get FunctionSpec: %w", err)
|
||||
}
|
||||
if spec == nil {
|
||||
// resource doesn't have function spec
|
||||
continue
|
||||
}
|
||||
if spec.Container.Network && !r.Network {
|
||||
// TODO(eddiezane): Provide error info about which function needs the network
|
||||
return fltrs, errors.Errorf("network required but not enabled with --network")
|
||||
}
|
||||
// merge envs from imperative and declarative
|
||||
spec.Container.Env = r.mergeContainerEnv(spec.Container.Env)
|
||||
|
||||
c, err := r.functionFilterProvider(*spec, api, user.Current)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
cf, ok := c.(*container.Filter)
|
||||
if ok {
|
||||
if global {
|
||||
cf.Exec.GlobalScope = true
|
||||
}
|
||||
cf.Exec.WorkingDir = r.WorkingDir
|
||||
}
|
||||
fltrs = append(fltrs, c)
|
||||
}
|
||||
return fltrs, nil
|
||||
}
|
||||
|
||||
// sortFns sorts functions so that functions with the longest paths come first
|
||||
func sortFns(buff *kio.PackageBuffer) error {
|
||||
var outerErr error
|
||||
// sort the nodes so that we traverse them depth first
|
||||
// functions deeper in the file system tree should be run first
|
||||
sort.Slice(buff.Nodes, func(i, j int) bool {
|
||||
if err := kioutil.CopyLegacyAnnotations(buff.Nodes[i]); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := kioutil.CopyLegacyAnnotations(buff.Nodes[j]); err != nil {
|
||||
return false
|
||||
}
|
||||
mi, _ := buff.Nodes[i].GetMeta()
|
||||
pi := filepath.ToSlash(mi.Annotations[kioutil.PathAnnotation])
|
||||
|
||||
mj, _ := buff.Nodes[j].GetMeta()
|
||||
pj := filepath.ToSlash(mj.Annotations[kioutil.PathAnnotation])
|
||||
|
||||
// If the path is the same, we decide the ordering based on the
|
||||
// index annotation.
|
||||
if pi == pj {
|
||||
iIndex, err := strconv.Atoi(mi.Annotations[kioutil.IndexAnnotation])
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return false
|
||||
}
|
||||
jIndex, err := strconv.Atoi(mj.Annotations[kioutil.IndexAnnotation])
|
||||
if err != nil {
|
||||
outerErr = err
|
||||
return false
|
||||
}
|
||||
return iIndex < jIndex
|
||||
}
|
||||
|
||||
if filepath.Base(path.Dir(pi)) == "functions" {
|
||||
// don't count the functions dir, the functions are scoped 1 level above
|
||||
pi = filepath.Dir(path.Dir(pi))
|
||||
} else {
|
||||
pi = filepath.Dir(pi)
|
||||
}
|
||||
|
||||
if filepath.Base(path.Dir(pj)) == "functions" {
|
||||
// don't count the functions dir, the functions are scoped 1 level above
|
||||
pj = filepath.Dir(path.Dir(pj))
|
||||
} else {
|
||||
pj = filepath.Dir(pj)
|
||||
}
|
||||
|
||||
// i is "less" than j (comes earlier) if its depth is greater -- e.g. run
|
||||
// i before j if it is deeper in the directory structure
|
||||
li := len(strings.Split(pi, "/"))
|
||||
if pi == "." {
|
||||
// local dir should have 0 path elements instead of 1
|
||||
li = 0
|
||||
}
|
||||
lj := len(strings.Split(pj, "/"))
|
||||
if pj == "." {
|
||||
// local dir should have 0 path elements instead of 1
|
||||
lj = 0
|
||||
}
|
||||
if li != lj {
|
||||
// use greater-than because we want to sort with the longest
|
||||
// paths FIRST rather than last
|
||||
return li > lj
|
||||
}
|
||||
|
||||
// sort by path names if depths are equal
|
||||
return pi < pj
|
||||
})
|
||||
return outerErr
|
||||
}
|
||||
|
||||
// init initializes the RunFns with a containerFilterProvider.
|
||||
func (r *RunFns) init() {
|
||||
if r.NoFunctionsFromInput == nil {
|
||||
// default no functions from input if any function sources are explicitly provided
|
||||
nfn := len(r.FunctionPaths) > 0 || len(r.Functions) > 0
|
||||
r.NoFunctionsFromInput = &nfn
|
||||
}
|
||||
|
||||
// if no path is specified, default reading from stdin and writing to stdout
|
||||
if r.Path == "" {
|
||||
if r.Output == nil {
|
||||
r.Output = os.Stdout
|
||||
}
|
||||
if r.Input == nil {
|
||||
r.Input = os.Stdin
|
||||
}
|
||||
}
|
||||
|
||||
// functionFilterProvider set the filter provider
|
||||
if r.functionFilterProvider == nil {
|
||||
r.functionFilterProvider = r.ffp
|
||||
}
|
||||
|
||||
// if LogSteps is enabled and LogWriter is not specified, use stderr
|
||||
if r.LogSteps && r.LogWriter == nil {
|
||||
r.LogWriter = os.Stderr
|
||||
}
|
||||
}
|
||||
|
||||
type currentUserFunc func() (*user.User, error)
|
||||
|
||||
// getUIDGID will return "nobody" if asCurrentUser is false. Otherwise
|
||||
// return "uid:gid" according to the return from currentUser function.
|
||||
func getUIDGID(asCurrentUser bool, currentUser currentUserFunc) (string, error) {
|
||||
if !asCurrentUser {
|
||||
return "nobody", nil
|
||||
}
|
||||
|
||||
u, err := currentUser()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", u.Uid, u.Gid), nil
|
||||
}
|
||||
|
||||
// ffp provides function filters
|
||||
func (r *RunFns) ffp(spec runtimeutil.FunctionSpec, api *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
|
||||
var resultsFile string
|
||||
if r.ResultsDir != "" {
|
||||
resultsFile = filepath.Join(r.ResultsDir, fmt.Sprintf(
|
||||
"results-%v.yaml", r.resultsCount))
|
||||
atomic.AddUint32(&r.resultsCount, 1)
|
||||
}
|
||||
if !r.DisableContainers && spec.Container.Image != "" {
|
||||
// TODO: Add a test for this behavior
|
||||
uidgid, err := getUIDGID(r.AsCurrentUser, currentUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Storage mounts can either come from kustomize fn run --mounts,
|
||||
// or from the declarative function mounts field.
|
||||
storageMounts := spec.Container.StorageMounts
|
||||
storageMounts = append(storageMounts, r.StorageMounts...)
|
||||
|
||||
c := container.NewContainer(
|
||||
runtimeutil.ContainerSpec{
|
||||
Image: spec.Container.Image,
|
||||
Network: spec.Container.Network,
|
||||
StorageMounts: storageMounts,
|
||||
Env: spec.Container.Env,
|
||||
},
|
||||
uidgid,
|
||||
)
|
||||
cf := &c
|
||||
cf.Exec.FunctionConfig = api
|
||||
cf.Exec.GlobalScope = r.GlobalScope
|
||||
cf.Exec.ResultsFile = resultsFile
|
||||
cf.Exec.DeferFailure = spec.DeferFailure
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
if r.EnableExec && spec.Exec.Path != "" {
|
||||
ef := &exec.Filter{
|
||||
Path: spec.Exec.Path,
|
||||
Args: spec.Exec.Args,
|
||||
Env: r.mergeExecEnv(spec.Exec.Env),
|
||||
WorkingDir: r.WorkingDir,
|
||||
}
|
||||
|
||||
ef.FunctionConfig = api
|
||||
ef.GlobalScope = r.GlobalScope
|
||||
ef.ResultsFile = resultsFile
|
||||
ef.DeferFailure = spec.DeferFailure
|
||||
return ef, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sets
|
||||
|
||||
type String map[string]interface{}
|
||||
|
||||
func (s String) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s String) List() []string {
|
||||
val := make([]string, 0, len(s))
|
||||
for k := range s {
|
||||
val = append(val, k)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (s String) Has(val string) bool {
|
||||
_, found := s[val]
|
||||
return found
|
||||
}
|
||||
|
||||
func (s String) Insert(vals ...string) {
|
||||
for _, val := range vals {
|
||||
s[val] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s String) Difference(s2 String) String {
|
||||
s3 := String{}
|
||||
for k := range s {
|
||||
if _, found := s2[k]; !found {
|
||||
s3.Insert(k)
|
||||
}
|
||||
}
|
||||
return s3
|
||||
}
|
||||
|
||||
func (s String) SymmetricDifference(s2 String) String {
|
||||
s3 := String{}
|
||||
for k := range s {
|
||||
if _, found := s2[k]; !found {
|
||||
s3.Insert(k)
|
||||
}
|
||||
}
|
||||
for k := range s2 {
|
||||
if _, found := s[k]; !found {
|
||||
s3.Insert(k)
|
||||
}
|
||||
}
|
||||
return s3
|
||||
}
|
||||
|
||||
func (s String) Intersection(s2 String) String {
|
||||
s3 := String{}
|
||||
for k := range s {
|
||||
if _, found := s2[k]; found {
|
||||
s3.Insert(k)
|
||||
}
|
||||
}
|
||||
return s3
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sets
|
||||
|
||||
// StringList is a set, where each element of
|
||||
// the set is a string slice.
|
||||
type StringList [][]string
|
||||
|
||||
func (s StringList) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s StringList) Insert(val []string) StringList {
|
||||
if !s.Has(val) {
|
||||
return append(s, val)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s StringList) Has(val []string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range s {
|
||||
if isStringSliceEqual(s[i], val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isStringSliceEqual(s []string, t []string) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i := range s {
|
||||
if s[i] != t[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sliceutil
|
||||
|
||||
// Contains return true if string e is present in slice s
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove removes the first occurrence of r in slice s
|
||||
// and returns remaining slice
|
||||
func Remove(s []string, r string) []string {
|
||||
for i, v := range s {
|
||||
if v == r {
|
||||
return append(s[:i], s[i+1:]...)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
// TODO: Move these to kyaml
|
||||
|
||||
// PathSplitter splits a delimited string, permitting escaped delimiters.
|
||||
func PathSplitter(path string, delimiter string) []string {
|
||||
ps := strings.Split(path, delimiter)
|
||||
var res []string
|
||||
|
||||
// allow path to start with forward slash
|
||||
// i.e. /a/b/c
|
||||
if len(ps) > 1 && ps[0] == "" {
|
||||
ps = ps[1:]
|
||||
}
|
||||
|
||||
res = append(res, ps[0])
|
||||
for i := 1; i < len(ps); i++ {
|
||||
last := len(res) - 1
|
||||
if strings.HasSuffix(res[last], `\`) {
|
||||
res[last] = strings.TrimSuffix(res[last], `\`) + delimiter + ps[i]
|
||||
} else {
|
||||
res = append(res, ps[i])
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// SmarterPathSplitter splits a path, retaining bracketed elements.
|
||||
// If the element is a list entry identifier (defined by the '='),
|
||||
// it will retain the brackets.
|
||||
// E.g. "[name=com.foo.someapp]" survives as one thing after splitting
|
||||
// "spec.template.spec.containers.[name=com.foo.someapp].image"
|
||||
// See kyaml/yaml/match.go for use of list entry identifiers.
|
||||
// If the element is a mapping entry identifier, it will remove the
|
||||
// brackets.
|
||||
// E.g. "a.b.c" survives as one thing after splitting
|
||||
// "metadata.annotations.[a.b.c]
|
||||
// This function uses `PathSplitter`, so it also respects escaped delimiters.
|
||||
func SmarterPathSplitter(path string, delimiter string) []string {
|
||||
var result []string
|
||||
split := PathSplitter(path, delimiter)
|
||||
|
||||
for i := 0; i < len(split); i++ {
|
||||
elem := split[i]
|
||||
if strings.HasPrefix(elem, "[") && !strings.HasSuffix(elem, "]") {
|
||||
// continue until we find the matching "]"
|
||||
bracketed := []string{elem}
|
||||
for i < len(split)-1 {
|
||||
i++
|
||||
bracketed = append(bracketed, split[i])
|
||||
if strings.HasSuffix(split[i], "]") {
|
||||
break
|
||||
}
|
||||
}
|
||||
bracketedStr := strings.Join(bracketed, delimiter)
|
||||
if strings.Contains(bracketedStr, "=") {
|
||||
result = append(result, bracketedStr)
|
||||
} else {
|
||||
result = append(result, strings.Trim(bracketedStr, "[]"))
|
||||
}
|
||||
} else {
|
||||
result = append(result, elem)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
WideSequenceStyle SequenceIndentStyle = "wide"
|
||||
CompactSequenceStyle SequenceIndentStyle = "compact"
|
||||
DefaultIndent = 2
|
||||
// BareSeqNodeWrappingKey kyaml uses reader annotations to track resources, it is not possible to
|
||||
// add them to bare sequence nodes, this key is used to wrap such bare
|
||||
// sequence nodes into map node, byteio_writer unwraps it while writing back
|
||||
BareSeqNodeWrappingKey = "bareSeqNodeWrappingKey"
|
||||
)
|
||||
|
||||
// SequenceIndentStyle holds the indentation style for sequence nodes
|
||||
type SequenceIndentStyle string
|
||||
|
||||
// EncoderOptions are options that can be used to configure the encoder,
|
||||
// do not expose new options without considerable justification
|
||||
type EncoderOptions struct {
|
||||
// SeqIndent is the indentation style for YAML Sequence nodes
|
||||
SeqIndent SequenceIndentStyle
|
||||
}
|
||||
|
||||
// Expose the yaml.v3 functions so this package can be used as a replacement
|
||||
|
||||
type Decoder = yaml.Decoder
|
||||
type Encoder = yaml.Encoder
|
||||
type IsZeroer = yaml.IsZeroer
|
||||
type Kind = yaml.Kind
|
||||
type Marshaler = yaml.Marshaler
|
||||
type Node = yaml.Node
|
||||
type Style = yaml.Style
|
||||
type TypeError = yaml.TypeError
|
||||
type Unmarshaler = yaml.Unmarshaler
|
||||
|
||||
var Marshal = func(in interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := NewEncoder(&buf).Encode(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
var Unmarshal = yaml.Unmarshal
|
||||
var NewDecoder = yaml.NewDecoder
|
||||
var NewEncoder = func(w io.Writer) *yaml.Encoder {
|
||||
e := yaml.NewEncoder(w)
|
||||
e.SetIndent(DefaultIndent)
|
||||
e.CompactSeqIndent()
|
||||
return e
|
||||
}
|
||||
|
||||
// MarshalWithOptions marshals the input interface with provided options
|
||||
func MarshalWithOptions(in interface{}, opts *EncoderOptions) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := NewEncoderWithOptions(&buf, opts).Encode(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// NewEncoderWithOptions returns the encoder with provided options
|
||||
func NewEncoderWithOptions(w io.Writer, opts *EncoderOptions) *yaml.Encoder {
|
||||
encoder := NewEncoder(w)
|
||||
encoder.SetIndent(DefaultIndent)
|
||||
if opts.SeqIndent == WideSequenceStyle {
|
||||
encoder.DefaultSeqIndent()
|
||||
} else {
|
||||
encoder.CompactSeqIndent()
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
|
||||
var AliasNode yaml.Kind = yaml.AliasNode
|
||||
var DocumentNode yaml.Kind = yaml.DocumentNode
|
||||
var MappingNode yaml.Kind = yaml.MappingNode
|
||||
var ScalarNode yaml.Kind = yaml.ScalarNode
|
||||
var SequenceNode yaml.Kind = yaml.SequenceNode
|
||||
|
||||
func nodeKindString(k yaml.Kind) string {
|
||||
return map[yaml.Kind]string{
|
||||
yaml.SequenceNode: "SequenceNode",
|
||||
yaml.MappingNode: "MappingNode",
|
||||
yaml.ScalarNode: "ScalarNode",
|
||||
yaml.DocumentNode: "DocumentNode",
|
||||
yaml.AliasNode: "AliasNode",
|
||||
}[k]
|
||||
}
|
||||
|
||||
var DoubleQuotedStyle yaml.Style = yaml.DoubleQuotedStyle
|
||||
var FlowStyle yaml.Style = yaml.FlowStyle
|
||||
var FoldedStyle yaml.Style = yaml.FoldedStyle
|
||||
var LiteralStyle yaml.Style = yaml.LiteralStyle
|
||||
var SingleQuotedStyle yaml.Style = yaml.SingleQuotedStyle
|
||||
var TaggedStyle yaml.Style = yaml.TaggedStyle
|
||||
|
||||
const (
|
||||
MergeTag = "!!merge"
|
||||
)
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
y1_1 "go.yaml.in/yaml/v2"
|
||||
y1_2 "go.yaml.in/yaml/v3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
// typeToTag maps OpenAPI schema types to yaml 1.2 tags
|
||||
var typeToTag = map[string]string{
|
||||
"string": NodeTagString,
|
||||
"integer": NodeTagInt,
|
||||
"boolean": NodeTagBool,
|
||||
"number": NodeTagFloat,
|
||||
}
|
||||
|
||||
// FormatNonStringStyle makes sure that values which parse as non-string values in yaml 1.1
|
||||
// are correctly formatted given the Schema type.
|
||||
func FormatNonStringStyle(node *Node, schema spec.Schema) {
|
||||
if len(schema.Type) != 1 {
|
||||
return
|
||||
}
|
||||
t := schema.Type[0]
|
||||
|
||||
if !IsYaml1_1NonString(node) {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case t == "string" && schema.Format != "int-or-string":
|
||||
if (node.Style&DoubleQuotedStyle == 0) && (node.Style&SingleQuotedStyle == 0) {
|
||||
// must quote values so they are parsed as strings
|
||||
node.Style = DoubleQuotedStyle
|
||||
}
|
||||
case t == "boolean" || t == "integer" || t == "number":
|
||||
if (node.Style&DoubleQuotedStyle != 0) || (node.Style&SingleQuotedStyle != 0) {
|
||||
// must NOT quote the values so they aren't parsed as strings
|
||||
node.Style = 0
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// if the node tag is null, make sure we don't add any non-null tags
|
||||
// https://github.com/kptdev/kpt/issues/2321
|
||||
if node.Tag == NodeTagNull {
|
||||
// must NOT quote null values
|
||||
node.Style = 0
|
||||
return
|
||||
}
|
||||
if tag, found := typeToTag[t]; found {
|
||||
// make sure the right tag is set
|
||||
node.Tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
// IsYaml1_1NonString returns true if the value parses as a non-string value in yaml 1.1
|
||||
// when unquoted.
|
||||
//
|
||||
// Note: yaml 1.2 uses different keywords than yaml 1.1. Example: yaml 1.2 interprets
|
||||
// `field: on` and `field: "on"` as equivalent (both strings). However Yaml 1.1 interprets
|
||||
// `field: on` as on being a bool and `field: "on"` as on being a string.
|
||||
// If an input is read with `field: "on"`, and the style is changed from DoubleQuote to 0,
|
||||
// it will change the type of the field from a string to a bool. For this reason, fields
|
||||
// which are keywords in yaml 1.1 should never have their style changed, as it would break
|
||||
// backwards compatibility with yaml 1.1 -- which is what is used by the Kubernetes apiserver.
|
||||
func IsYaml1_1NonString(node *Node) bool {
|
||||
if node.Kind != y1_2.ScalarNode {
|
||||
// not a keyword
|
||||
return false
|
||||
}
|
||||
return IsValueNonString(node.Value)
|
||||
}
|
||||
|
||||
func IsValueNonString(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(value, "\n") {
|
||||
// multi-line strings will fail to unmarshal
|
||||
return false
|
||||
}
|
||||
// check if the value will unmarshal into a non-string value using a yaml 1.1 parser
|
||||
var i1 interface{}
|
||||
if err := y1_1.Unmarshal([]byte(value), &i1); err != nil {
|
||||
return false
|
||||
}
|
||||
if reflect.TypeOf(i1) != stringType {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var stringType = reflect.TypeOf("string")
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
const (
|
||||
// NodeTagNull is the tag set for a yaml.Document that contains no data;
|
||||
// e.g. it isn't a Map, Slice, Document, etc
|
||||
NodeTagNull = "!!null"
|
||||
NodeTagFloat = "!!float"
|
||||
NodeTagString = "!!str"
|
||||
NodeTagBool = "!!bool"
|
||||
NodeTagInt = "!!int"
|
||||
NodeTagMap = "!!map"
|
||||
NodeTagSeq = "!!seq"
|
||||
NodeTagEmpty = ""
|
||||
)
|
||||
|
||||
// Field names
|
||||
const (
|
||||
AnnotationsField = "annotations"
|
||||
APIVersionField = "apiVersion"
|
||||
KindField = "kind"
|
||||
MetadataField = "metadata"
|
||||
DataField = "data"
|
||||
BinaryDataField = "binaryData"
|
||||
NameField = "name"
|
||||
NamespaceField = "namespace"
|
||||
LabelsField = "labels"
|
||||
)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// SortedMapKeys returns a sorted slice of keys to the given map.
|
||||
// Writing this function never gets old.
|
||||
func SortedMapKeys(m map[string]string) []string {
|
||||
keys := make([]string, len(m))
|
||||
i := 0
|
||||
for k := range m {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (rn *RNode) LoadMapIntoConfigMapData(m map[string]string) error {
|
||||
for _, k := range SortedMapKeys(m) {
|
||||
fldName, vrN := makeConfigMapValueRNode(m[k])
|
||||
if _, err := rn.Pipe(
|
||||
LookupCreate(MappingNode, fldName),
|
||||
SetField(k, vrN)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rn *RNode) LoadMapIntoConfigMapBinaryData(m map[string]string) error {
|
||||
for _, k := range SortedMapKeys(m) {
|
||||
_, vrN := makeConfigMapValueRNode(m[k])
|
||||
// we know this is binary data
|
||||
fldName := BinaryDataField
|
||||
if _, err := rn.Pipe(
|
||||
LookupCreate(MappingNode, fldName),
|
||||
SetField(k, vrN)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeConfigMapValueRNode(s string) (field string, rN *RNode) {
|
||||
yN := &Node{Kind: ScalarNode}
|
||||
yN.Tag = NodeTagString
|
||||
if utf8.ValidString(s) {
|
||||
field = DataField
|
||||
yN.Value = s
|
||||
} else {
|
||||
field = BinaryDataField
|
||||
yN.Value = encodeBase64(s)
|
||||
}
|
||||
if strings.Contains(yN.Value, "\n") {
|
||||
yN.Style = LiteralStyle
|
||||
}
|
||||
return field, NewRNode(yN)
|
||||
}
|
||||
|
||||
func (rn *RNode) LoadMapIntoSecretData(m map[string]string) error {
|
||||
mapNode, err := rn.Pipe(LookupCreate(MappingNode, DataField))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, k := range SortedMapKeys(m) {
|
||||
vrN := makeSecretValueRNode(m[k])
|
||||
if _, err := mapNode.Pipe(SetField(k, vrN)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// In a secret, all data is base64 encoded, regardless of its conformance
|
||||
// or lack thereof to UTF-8.
|
||||
func makeSecretValueRNode(s string) *RNode {
|
||||
yN := &Node{Kind: ScalarNode}
|
||||
// Purposely don't use YAML tags to identify the data as being plain text or
|
||||
// binary. It kubernetes Secrets the values in the `data` map are expected
|
||||
// to be base64 encoded, and in ConfigMaps that same can be said for the
|
||||
// values in the `binaryData` field.
|
||||
yN.Tag = NodeTagString
|
||||
yN.Value = encodeBase64(s)
|
||||
if strings.Contains(yN.Value, "\n") {
|
||||
yN.Style = LiteralStyle
|
||||
}
|
||||
return NewRNode(yN)
|
||||
}
|
||||
|
||||
// encodeBase64 encodes s as base64 that is broken up into multiple lines
|
||||
// as appropriate for the resulting length.
|
||||
func encodeBase64(s string) string {
|
||||
const lineLen = 70
|
||||
encLen := base64.StdEncoding.EncodedLen(len(s))
|
||||
lines := encLen/lineLen + 1
|
||||
buf := make([]byte, encLen*2+lines)
|
||||
in := buf[0:encLen]
|
||||
out := buf[encLen:]
|
||||
base64.StdEncoding.Encode(in, []byte(s))
|
||||
k := 0
|
||||
for i := 0; i < len(in); i += lineLen {
|
||||
j := i + lineLen
|
||||
if j > len(in) {
|
||||
j = len(in)
|
||||
}
|
||||
k += copy(out[k:], in[i:j])
|
||||
if lines > 1 {
|
||||
out[k] = '\n'
|
||||
k++
|
||||
}
|
||||
}
|
||||
return string(out[:k])
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package yaml contains libraries for manipulating individual Kubernetes Resource
|
||||
// Configuration as yaml, keeping yaml structure and comments.
|
||||
//
|
||||
// Parsing Resources
|
||||
//
|
||||
// Typically Resources will be initialized as collections through the kio package libraries.
|
||||
// However it is possible to directly initialize Resources using Parse.
|
||||
// resource, err := yaml.Parse("apiVersion: apps/v1\nkind: Deployment")
|
||||
//
|
||||
// Processing Resources
|
||||
//
|
||||
// Individual Resources are manipulated using the Pipe and PipeE to apply Filter functions
|
||||
// to transform the Resource data.
|
||||
// err := resource.PipeE(yaml.SetAnnotation("key", "value"))
|
||||
//
|
||||
// If multiple Filter functions are provided to Pipe or PipeE, each function is applied to
|
||||
// the result of the last function -- e.g. yaml.Lookup(...), yaml.SetField(...)
|
||||
//
|
||||
// Field values may also be retrieved using Pipe.
|
||||
// annotationValue, err := resource.Pipe(yaml.GetAnnotation("key"))
|
||||
//
|
||||
// See http://www.linfo.org/filters.html for a definition of filters.
|
||||
//
|
||||
// Common Filters
|
||||
//
|
||||
// There are a number of standard filter functions provided by the yaml package.
|
||||
//
|
||||
// Working with annotations:
|
||||
// [AnnotationSetter{}, AnnotationGetter{}, AnnotationClearer{}]
|
||||
//
|
||||
// Working with fields by path:
|
||||
// [PathMatcher{}, PathGetter{}]
|
||||
//
|
||||
// Working with individual fields on Maps and Objects:
|
||||
// [FieldMatcher{}, FieldSetter{}, FieldGetter{}]
|
||||
//
|
||||
// Working with individual elements in Sequences:
|
||||
// [ElementAppender{}, ElementSetter{}, ElementMatcher{}]
|
||||
//
|
||||
// Writing Filters
|
||||
//
|
||||
// Users may implement their own filter functions. When doing so, can be necessary to work with
|
||||
// the RNode directly rather than through Pipe. RNode provides a number of functions for doing
|
||||
// so. See:
|
||||
// [GetMeta(), Fields(), Elements(), String()]
|
||||
package yaml
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Filters is the list of serializable Pipeline Filters
|
||||
var Filters = map[string]func() Filter{
|
||||
"AnnotationClearer": func() Filter { return &AnnotationClearer{} },
|
||||
"AnnotationGetter": func() Filter { return &AnnotationGetter{} },
|
||||
"AnnotationSetter": func() Filter { return &AnnotationSetter{} },
|
||||
"LabelSetter": func() Filter { return &LabelSetter{} },
|
||||
"ElementAppender": func() Filter { return &ElementAppender{} },
|
||||
"ElementMatcher": func() Filter { return &ElementMatcher{} },
|
||||
"FieldClearer": func() Filter { return &FieldClearer{} },
|
||||
"FilterMatcher": func() Filter { return &FilterMatcher{} },
|
||||
"FieldMatcher": func() Filter { return &FieldMatcher{} },
|
||||
"FieldSetter": func() Filter { return &FieldSetter{} },
|
||||
"PathGetter": func() Filter { return &PathGetter{} },
|
||||
"PathMatcher": func() Filter { return &PathMatcher{} },
|
||||
"Parser": func() Filter { return &Parser{} },
|
||||
"PrefixSetter": func() Filter { return &PrefixSetter{} },
|
||||
"ValueReplacer": func() Filter { return &ValueReplacer{} },
|
||||
"SuffixSetter": func() Filter { return &SuffixSetter{} },
|
||||
"TeePiper": func() Filter { return &TeePiper{} },
|
||||
}
|
||||
|
||||
// YFilter wraps the Filter interface so the filter can be represented as
|
||||
// data and can be unmarshalled into a struct from a yaml config file.
|
||||
// This allows Pipelines to be expressed as data rather than code.
|
||||
type YFilter struct {
|
||||
Filter
|
||||
}
|
||||
|
||||
func (y YFilter) MarshalYAML() (interface{}, error) {
|
||||
return y.Filter, nil
|
||||
}
|
||||
|
||||
func (y *YFilter) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
meta := &ResourceMeta{}
|
||||
if err := unmarshal(meta); err != nil {
|
||||
return err
|
||||
}
|
||||
filter, found := Filters[meta.Kind]
|
||||
if !found {
|
||||
var knownFilters []string
|
||||
for k := range Filters {
|
||||
knownFilters = append(knownFilters, k)
|
||||
}
|
||||
sort.Strings(knownFilters)
|
||||
return fmt.Errorf("unsupported Filter Kind %s: may be one of: [%s]",
|
||||
meta.Kind, strings.Join(knownFilters, ","))
|
||||
}
|
||||
y.Filter = filter()
|
||||
|
||||
if err := unmarshal(y.Filter); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type YFilters []YFilter
|
||||
|
||||
func (y YFilters) Filters() []Filter {
|
||||
f := make([]Filter, 0, len(y))
|
||||
for i := range y {
|
||||
f = append(f, y[i].Filter)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type FilterMatcher struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
// Filters are the set of Filters run by TeePiper.
|
||||
Filters YFilters `yaml:"pipeline,omitempty"`
|
||||
}
|
||||
|
||||
func (t FilterMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
v, err := rn.Pipe(t.Filters.Filters()...)
|
||||
if v == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// return the original input if the pipeline resolves to true
|
||||
return rn, err
|
||||
}
|
||||
|
||||
type ValueReplacer struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
StringMatch string `yaml:"stringMatch"`
|
||||
RegexMatch string `yaml:"regexMatch"`
|
||||
Replace string `yaml:"replace"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
|
||||
func (s ValueReplacer) Filter(object *RNode) (*RNode, error) {
|
||||
if s.Count == 0 {
|
||||
s.Count = -1
|
||||
}
|
||||
switch {
|
||||
case s.StringMatch != "":
|
||||
object.value.Value = strings.Replace(object.value.Value, s.StringMatch, s.Replace, s.Count)
|
||||
case s.RegexMatch != "":
|
||||
r, err := regexp.Compile(s.RegexMatch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ValueReplacer RegexMatch does not compile: %v", err)
|
||||
}
|
||||
object.value.Value = r.ReplaceAllString(object.value.Value, s.Replace)
|
||||
default:
|
||||
return nil, fmt.Errorf("ValueReplacer missing StringMatch and RegexMatch")
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
type PrefixSetter struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
func (s PrefixSetter) Filter(object *RNode) (*RNode, error) {
|
||||
if !strings.HasPrefix(object.value.Value, s.Value) {
|
||||
object.value.Value = s.Value + object.value.Value
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
type SuffixSetter struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
func (s SuffixSetter) Filter(object *RNode) (*RNode, error) {
|
||||
if !strings.HasSuffix(object.value.Value, s.Value) {
|
||||
object.value.Value += s.Value
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
+893
@@ -0,0 +1,893 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
// Append creates an ElementAppender
|
||||
func Append(elements ...*yaml.Node) ElementAppender {
|
||||
return ElementAppender{Elements: elements}
|
||||
}
|
||||
|
||||
// ElementAppender adds all element to a SequenceNode's Content.
|
||||
// Returns Elements[0] if len(Elements) == 1, otherwise returns nil.
|
||||
type ElementAppender struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Elem is the value to append.
|
||||
Elements []*yaml.Node `yaml:"elements,omitempty"`
|
||||
}
|
||||
|
||||
func (a ElementAppender) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range a.Elements {
|
||||
rn.YNode().Content = append(rn.Content(), a.Elements[i])
|
||||
}
|
||||
if len(a.Elements) == 1 {
|
||||
return NewRNode(a.Elements[0]), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ElementSetter sets the value for an Element in an associative list.
|
||||
// ElementSetter will append, replace or delete an element in an associative list.
|
||||
// To append, user a key-value pair that doesn't exist in the sequence. this
|
||||
// behavior is intended to handle the case that not matching element found. It's
|
||||
// not designed for this purpose. To append an element, please use ElementAppender.
|
||||
// To replace, set the key-value pair and a non-nil Element.
|
||||
// To delete, set the key-value pair and leave the Element as nil.
|
||||
// Every key must have a corresponding value.
|
||||
type ElementSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Element is the new value to set -- remove the existing element if nil
|
||||
Element *Node
|
||||
|
||||
// Key is a list of fields on the elements. It is used to find matching elements to
|
||||
// update / delete
|
||||
Keys []string
|
||||
|
||||
// Value is a list of field values on the elements corresponding to the keys. It is
|
||||
// used to find matching elements to update / delete.
|
||||
Values []string
|
||||
}
|
||||
|
||||
// isMappingNode returns whether node is a mapping node
|
||||
func (e ElementSetter) isMappingNode(node *RNode) bool {
|
||||
return ErrorIfInvalid(node, yaml.MappingNode) == nil
|
||||
}
|
||||
|
||||
// isMappingSetter returns is this setter intended to set a mapping node
|
||||
func (e ElementSetter) isMappingSetter() bool {
|
||||
return len(e.Keys) > 0 && e.Keys[0] != "" &&
|
||||
len(e.Values) > 0 && e.Values[0] != ""
|
||||
}
|
||||
|
||||
func (e ElementSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
if len(e.Keys) == 0 {
|
||||
e.Keys = append(e.Keys, "")
|
||||
}
|
||||
|
||||
if err := ErrorIfInvalid(rn, SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// build the new Content slice
|
||||
var newContent []*yaml.Node
|
||||
matchingElementFound := false
|
||||
for i := range rn.YNode().Content {
|
||||
elem := rn.Content()[i]
|
||||
newNode := NewRNode(elem)
|
||||
|
||||
// empty elements are not valid -- they at least need an associative key
|
||||
if IsMissingOrNull(newNode) || IsEmptyMap(newNode) {
|
||||
continue
|
||||
}
|
||||
// keep non-mapping node in the Content when we want to match a mapping.
|
||||
if !e.isMappingNode(newNode) && e.isMappingSetter() {
|
||||
newContent = append(newContent, elem)
|
||||
continue
|
||||
}
|
||||
|
||||
// check if this is the element we are matching
|
||||
var val *RNode
|
||||
var err error
|
||||
found := true
|
||||
for j := range e.Keys {
|
||||
if j < len(e.Values) {
|
||||
val, err = newNode.Pipe(FieldMatcher{Name: e.Keys[j], StringValue: e.Values[j]})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val == nil {
|
||||
found = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// not the element we are looking for, keep it in the Content
|
||||
if len(e.Values) > 0 {
|
||||
newContent = append(newContent, elem)
|
||||
}
|
||||
continue
|
||||
}
|
||||
matchingElementFound = true
|
||||
|
||||
// deletion operation -- remove the element from the new Content
|
||||
if e.Element == nil {
|
||||
continue
|
||||
}
|
||||
// replace operation -- replace the element in the Content
|
||||
newContent = append(newContent, e.Element)
|
||||
}
|
||||
rn.YNode().Content = newContent
|
||||
|
||||
// deletion operation -- return nil
|
||||
if IsMissingOrNull(NewRNode(e.Element)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// append operation -- add the element to the Content
|
||||
if !matchingElementFound {
|
||||
rn.YNode().Content = append(rn.YNode().Content, e.Element)
|
||||
}
|
||||
|
||||
return NewRNode(e.Element), nil
|
||||
}
|
||||
|
||||
// GetElementByIndex will return a Filter which can be applied to a sequence
|
||||
// node to get the element specified by the index
|
||||
func GetElementByIndex(index int) ElementIndexer {
|
||||
return ElementIndexer{Index: index}
|
||||
}
|
||||
|
||||
// ElementIndexer picks the element with a specified index. Index starts from
|
||||
// 0 to len(list) - 1. a hyphen ("-") means the last index.
|
||||
type ElementIndexer struct {
|
||||
Index int
|
||||
}
|
||||
|
||||
// Filter implements Filter
|
||||
func (i ElementIndexer) Filter(rn *RNode) (*RNode, error) {
|
||||
// rn.Elements will return error if rn is not a sequence node.
|
||||
elems, err := rn.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i.Index < 0 {
|
||||
return elems[len(elems)-1], nil
|
||||
}
|
||||
if i.Index >= len(elems) {
|
||||
return nil, nil
|
||||
}
|
||||
return elems[i.Index], nil
|
||||
}
|
||||
|
||||
// Clear returns a FieldClearer
|
||||
func Clear(name string) FieldClearer {
|
||||
return FieldClearer{Name: name}
|
||||
}
|
||||
|
||||
// FieldClearer removes the field or map key.
|
||||
// Returns a RNode with the removed field or map entry.
|
||||
type FieldClearer struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name is the name of the field or key in the map.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
IfEmpty bool `yaml:"ifEmpty,omitempty"`
|
||||
}
|
||||
|
||||
func (c FieldClearer) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var removed *RNode
|
||||
visitFieldsWhileTrue(rn.Content(), func(key, value *yaml.Node, keyIndex int) bool {
|
||||
if key.Value != c.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
// the name matches: remove these 2 elements from the list because
|
||||
// they are treated as a fieldName/fieldValue pair.
|
||||
if c.IfEmpty {
|
||||
if len(value.Content) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// save the item we are about to remove
|
||||
removed = NewRNode(value)
|
||||
if len(rn.YNode().Content) > keyIndex+2 {
|
||||
l := len(rn.YNode().Content)
|
||||
// remove from the middle of the list
|
||||
rn.YNode().Content = rn.Content()[:keyIndex]
|
||||
rn.YNode().Content = append(
|
||||
rn.YNode().Content,
|
||||
rn.Content()[keyIndex+2:l]...)
|
||||
} else {
|
||||
// remove from the end of the list
|
||||
rn.YNode().Content = rn.Content()[:keyIndex]
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func MatchElement(field, value string) ElementMatcher {
|
||||
return ElementMatcher{Keys: []string{field}, Values: []string{value}}
|
||||
}
|
||||
|
||||
func MatchElementList(keys []string, values []string) ElementMatcher {
|
||||
return ElementMatcher{Keys: keys, Values: values}
|
||||
}
|
||||
|
||||
func GetElementByKey(key string) ElementMatcher {
|
||||
return ElementMatcher{Keys: []string{key}, MatchAnyValue: true}
|
||||
}
|
||||
|
||||
// ElementMatcher returns the first element from a Sequence matching the
|
||||
// specified key-value pairs. If there's no match, and no configuration error,
|
||||
// the matcher returns nil, nil.
|
||||
type ElementMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Keys are the list of fields upon which to match this element.
|
||||
Keys []string
|
||||
|
||||
// Values are the list of values upon which to match this element.
|
||||
Values []string
|
||||
|
||||
// Create will create the Element if it is not found
|
||||
Create *RNode `yaml:"create,omitempty"`
|
||||
|
||||
// MatchAnyValue indicates that matcher should only consider the key and ignore
|
||||
// the actual value in the list. Values must be empty when MatchAnyValue is
|
||||
// set to true.
|
||||
MatchAnyValue bool `yaml:"noValue,omitempty"`
|
||||
}
|
||||
|
||||
func (e ElementMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
if len(e.Keys) == 0 {
|
||||
e.Keys = append(e.Keys, "")
|
||||
}
|
||||
if len(e.Values) == 0 {
|
||||
e.Values = append(e.Values, "")
|
||||
}
|
||||
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.MatchAnyValue && len(e.Values) != 0 && e.Values[0] != "" {
|
||||
return nil, fmt.Errorf("Values must be empty when MatchAnyValue is set to true")
|
||||
}
|
||||
|
||||
// SequenceNode Content is a slice of ScalarNodes. Each ScalarNode has a
|
||||
// YNode containing the primitive data.
|
||||
if len(e.Keys) == 0 || len(e.Keys[0]) == 0 {
|
||||
for i := range rn.Content() {
|
||||
if rn.Content()[i].Value == e.Values[0] {
|
||||
return &RNode{value: rn.Content()[i]}, nil
|
||||
}
|
||||
}
|
||||
if e.Create != nil {
|
||||
return rn.Pipe(Append(e.Create.YNode()))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SequenceNode Content is a slice of MappingNodes. Each MappingNode has Content
|
||||
// with a slice of key-value pairs containing the fields.
|
||||
for i := range rn.Content() {
|
||||
// cast the entry to a RNode so we can operate on it
|
||||
elem := NewRNode(rn.Content()[i])
|
||||
var field *RNode
|
||||
var err error
|
||||
|
||||
// only check mapping node
|
||||
if err = ErrorIfInvalid(elem, yaml.MappingNode); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !e.MatchAnyValue && len(e.Keys) != len(e.Values) {
|
||||
return nil, fmt.Errorf("length of keys must equal length of values when MatchAnyValue is false")
|
||||
}
|
||||
|
||||
matchesElement := true
|
||||
for i := range e.Keys {
|
||||
if e.MatchAnyValue {
|
||||
field, err = elem.Pipe(Get(e.Keys[i]))
|
||||
} else {
|
||||
field, err = elem.Pipe(MatchField(e.Keys[i], e.Values[i]))
|
||||
}
|
||||
if !IsFoundOrError(field, err) {
|
||||
// this is not the element we are looking for
|
||||
matchesElement = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchesElement {
|
||||
return elem, err
|
||||
}
|
||||
}
|
||||
|
||||
// create the element
|
||||
if e.Create != nil {
|
||||
return rn.Pipe(Append(e.Create.YNode()))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Get(name string) FieldMatcher {
|
||||
return FieldMatcher{Name: name}
|
||||
}
|
||||
|
||||
func MatchField(name, value string) FieldMatcher {
|
||||
return FieldMatcher{Name: name, Value: NewScalarRNode(value)}
|
||||
}
|
||||
|
||||
func Match(value string) FieldMatcher {
|
||||
return FieldMatcher{Value: NewScalarRNode(value)}
|
||||
}
|
||||
|
||||
// FieldMatcher returns the value of a named field or map entry.
|
||||
type FieldMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name of the field to return
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// YNode of the field to return.
|
||||
// Optional. Will only need to match field name if unset.
|
||||
Value *RNode `yaml:"value,omitempty"`
|
||||
|
||||
StringValue string `yaml:"stringValue,omitempty"`
|
||||
|
||||
StringRegexValue string `yaml:"stringRegexValue,omitempty"`
|
||||
|
||||
// Create will cause the field to be created with this value
|
||||
// if it is set.
|
||||
Create *RNode `yaml:"create,omitempty"`
|
||||
}
|
||||
|
||||
func (f FieldMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
if f.StringValue != "" && f.Value == nil {
|
||||
f.Value = NewScalarRNode(f.StringValue)
|
||||
}
|
||||
|
||||
// never match nil or null fields
|
||||
if IsMissingOrNull(rn) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if f.Name == "" {
|
||||
if err := ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case f.StringRegexValue != "":
|
||||
// TODO(pwittrock): pre-compile this when unmarshalling and cache to a field
|
||||
rg, err := regexp.Compile(f.StringRegexValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match := rg.MatchString(rn.value.Value); match {
|
||||
return rn, nil
|
||||
}
|
||||
return nil, nil
|
||||
case GetValue(rn) == GetValue(f.Value):
|
||||
return rn, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var returnNode *RNode
|
||||
requireMatchFieldValue := f.Value != nil
|
||||
visitMappingNodeFields(rn.Content(), func(key, value *yaml.Node) {
|
||||
if !requireMatchFieldValue || value.Value == f.Value.YNode().Value {
|
||||
returnNode = NewRNode(value)
|
||||
}
|
||||
}, f.Name)
|
||||
if returnNode != nil {
|
||||
return returnNode, nil
|
||||
}
|
||||
|
||||
if f.Create != nil {
|
||||
return rn.Pipe(SetField(f.Name, f.Create))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Lookup returns a PathGetter to lookup a field by its path.
|
||||
func Lookup(path ...string) PathGetter {
|
||||
return PathGetter{Path: path}
|
||||
}
|
||||
|
||||
// LookupCreate returns a PathGetter to lookup a field by its path and create it if it doesn't already
|
||||
// exist.
|
||||
func LookupCreate(kind yaml.Kind, path ...string) PathGetter {
|
||||
return PathGetter{Path: path, Create: kind}
|
||||
}
|
||||
|
||||
// ConventionalContainerPaths is a list of paths at which containers typically appear in workload APIs.
|
||||
// It is intended for use with LookupFirstMatch.
|
||||
var ConventionalContainerPaths = [][]string{
|
||||
// e.g. Deployment, ReplicaSet, DaemonSet, Job, StatefulSet
|
||||
{"spec", "template", "spec", "containers"},
|
||||
// e.g. CronJob
|
||||
{"spec", "jobTemplate", "spec", "template", "spec", "containers"},
|
||||
// e.g. Pod
|
||||
{"spec", "containers"},
|
||||
// e.g. PodTemplate
|
||||
{"template", "spec", "containers"},
|
||||
}
|
||||
|
||||
// LookupFirstMatch returns a Filter for locating a value that may exist at one of several possible paths.
|
||||
// For example, it can be used with ConventionalContainerPaths to find the containers field in a standard workload resource.
|
||||
// If more than one of the paths exists in the resource, the first will be returned. If none exist,
|
||||
// nil will be returned. If an error is encountered during lookup, it will be returned.
|
||||
func LookupFirstMatch(paths [][]string) Filter {
|
||||
return FilterFunc(func(object *RNode) (*RNode, error) {
|
||||
var result *RNode
|
||||
var err error
|
||||
for _, path := range paths {
|
||||
result, err = object.Pipe(PathGetter{Path: path})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if result != nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
// PathGetter returns the RNode under Path.
|
||||
type PathGetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Path is a slice of parts leading to the RNode to lookup.
|
||||
// Each path part may be one of:
|
||||
// * FieldMatcher -- e.g. "spec"
|
||||
// * Map Key -- e.g. "app.k8s.io/version"
|
||||
// * List Entry -- e.g. "[name=nginx]" or "[=-jar]" or "0" or "-"
|
||||
//
|
||||
// Map Keys and Fields are equivalent.
|
||||
// See FieldMatcher for more on Fields and Map Keys.
|
||||
//
|
||||
// List Entries can be specified as map entry to match [fieldName=fieldValue]
|
||||
// or a positional index like 0 to get the element. - (unquoted hyphen) is
|
||||
// special and means the last element.
|
||||
//
|
||||
// See Elem for more on List Entries.
|
||||
//
|
||||
// Examples:
|
||||
// * spec.template.spec.container with matching name: [name=nginx]
|
||||
// * spec.template.spec.container.argument matching a value: [=-jar]
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
|
||||
// Create will cause missing path parts to be created as they are walked.
|
||||
//
|
||||
// * The leaf Node (final path) will be created with a Kind matching Create
|
||||
// * Intermediary Nodes will be created as either a MappingNodes or
|
||||
// SequenceNodes as appropriate for each's Path location.
|
||||
// * If a list item is specified by a index (an offset or "-"), this item will
|
||||
// not be created even Create is set.
|
||||
Create yaml.Kind `yaml:"create,omitempty"`
|
||||
|
||||
// Style is the style to apply to created value Nodes.
|
||||
// Created key Nodes keep an unspecified Style.
|
||||
Style yaml.Style `yaml:"style,omitempty"`
|
||||
}
|
||||
|
||||
func (l PathGetter) Filter(rn *RNode) (*RNode, error) {
|
||||
var err error
|
||||
fieldPath := append([]string{}, rn.FieldPath()...)
|
||||
match := rn
|
||||
|
||||
// iterate over path until encountering an error or missing value
|
||||
l.Path = cleanPath(l.Path)
|
||||
for i := range l.Path {
|
||||
var part, nextPart string
|
||||
part = l.Path[i]
|
||||
if len(l.Path) > i+1 {
|
||||
nextPart = l.Path[i+1]
|
||||
}
|
||||
var fltr Filter
|
||||
fltr, err = l.getFilter(part, nextPart, &fieldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
match, err = match.Pipe(fltr)
|
||||
if IsMissingOrError(match, err) {
|
||||
return nil, err
|
||||
}
|
||||
match.AppendToFieldPath(fieldPath...)
|
||||
}
|
||||
return match, nil
|
||||
}
|
||||
|
||||
func (l PathGetter) getFilter(part, nextPart string, fieldPath *[]string) (Filter, error) {
|
||||
idx, err := strconv.Atoi(part)
|
||||
switch {
|
||||
case err == nil:
|
||||
// part is a number
|
||||
if idx < 0 {
|
||||
return nil, fmt.Errorf("array index %d cannot be negative", idx)
|
||||
}
|
||||
return GetElementByIndex(idx), nil
|
||||
case part == "-":
|
||||
// part is a hyphen
|
||||
return GetElementByIndex(-1), nil
|
||||
case part == "*":
|
||||
// PathGetter is not support for wildcard matching
|
||||
return nil, errors.Errorf("wildcard is not supported in PathGetter")
|
||||
case IsListIndex(part):
|
||||
// part is surrounded by brackets
|
||||
return l.elemFilter(part)
|
||||
default:
|
||||
// mapping node
|
||||
*fieldPath = append(*fieldPath, part)
|
||||
return l.fieldFilter(part, getPathPartKind(nextPart, l.Create))
|
||||
}
|
||||
}
|
||||
|
||||
func (l PathGetter) elemFilter(part string) (Filter, error) {
|
||||
name, value, err := SplitIndexNameValue(part)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
if !IsCreate(l.Create) {
|
||||
return MatchElement(name, value), nil
|
||||
}
|
||||
|
||||
var elem *RNode
|
||||
primitiveElement := len(name) == 0
|
||||
if primitiveElement {
|
||||
// append a ScalarNode
|
||||
elem = NewScalarRNode(value)
|
||||
elem.YNode().Style = l.Style
|
||||
} else {
|
||||
// append a MappingNode
|
||||
match := NewRNode(&yaml.Node{Kind: yaml.ScalarNode, Value: value, Style: l.Style})
|
||||
elem = NewRNode(&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: name}, match.YNode()},
|
||||
Style: l.Style,
|
||||
})
|
||||
}
|
||||
// Append the Node
|
||||
return ElementMatcher{Keys: []string{name}, Values: []string{value}, Create: elem}, nil
|
||||
}
|
||||
|
||||
func (l PathGetter) fieldFilter(
|
||||
name string, kind yaml.Kind) (Filter, error) {
|
||||
if !IsCreate(l.Create) {
|
||||
return Get(name), nil
|
||||
}
|
||||
return FieldMatcher{Name: name, Create: &RNode{value: &yaml.Node{Kind: kind, Style: l.Style}}}, nil
|
||||
}
|
||||
|
||||
func getPathPartKind(nextPart string, defaultKind yaml.Kind) yaml.Kind {
|
||||
if IsListIndex(nextPart) {
|
||||
// if nextPart is of the form [a=b], then it is an index into a Sequence
|
||||
// so the current part must be a SequenceNode
|
||||
return yaml.SequenceNode
|
||||
}
|
||||
if IsIdxNumber(nextPart) {
|
||||
return yaml.SequenceNode
|
||||
}
|
||||
if nextPart == "" {
|
||||
// final name in the path, use the default kind provided
|
||||
return defaultKind
|
||||
}
|
||||
|
||||
// non-sequence intermediate Node
|
||||
return yaml.MappingNode
|
||||
}
|
||||
|
||||
func SetField(name string, value *RNode) FieldSetter {
|
||||
return FieldSetter{Name: name, Value: value}
|
||||
}
|
||||
|
||||
func Set(value *RNode) FieldSetter {
|
||||
return FieldSetter{Value: value}
|
||||
}
|
||||
|
||||
// MapEntrySetter sets a map entry to a value. If it finds a key with the same
|
||||
// value, it will override both Key and Value RNodes, including style and any
|
||||
// other metadata. If it doesn't find the key, it will insert a new map entry.
|
||||
// It will set the field, even if it's empty or nil, unlike the FieldSetter.
|
||||
// This is useful for rebuilding some pre-existing RNode structure.
|
||||
type MapEntrySetter struct {
|
||||
// Name is the name of the field or key to lookup in a MappingNode.
|
||||
// If Name is unspecified, it will use the Key's Value
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// Value is the value to set.
|
||||
Value *RNode `yaml:"value,omitempty"`
|
||||
|
||||
// Key is the map key to set.
|
||||
Key *RNode `yaml:"key,omitempty"`
|
||||
}
|
||||
|
||||
func (s MapEntrySetter) Filter(rn *RNode) (*RNode, error) {
|
||||
if rn == nil {
|
||||
return nil, errors.Errorf("Can't set map entry on a nil RNode")
|
||||
}
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.Name == "" {
|
||||
s.Name = GetValue(s.Key)
|
||||
}
|
||||
|
||||
content := rn.Content()
|
||||
fieldStillNotFound := true
|
||||
visitFieldsWhileTrue(content, func(key, value *yaml.Node, keyIndex int) bool {
|
||||
if key.Value == s.Name {
|
||||
content[keyIndex] = s.Key.YNode()
|
||||
content[keyIndex+1] = s.Value.YNode()
|
||||
fieldStillNotFound = false
|
||||
}
|
||||
return fieldStillNotFound
|
||||
})
|
||||
if !fieldStillNotFound {
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
// create the field
|
||||
rn.YNode().Content = append(
|
||||
rn.YNode().Content,
|
||||
s.Key.YNode(),
|
||||
s.Value.YNode())
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
// FieldSetter sets a field or map entry to a value.
|
||||
type FieldSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name is the name of the field or key to lookup in a MappingNode.
|
||||
// If Name is unspecified, and the input is a ScalarNode, FieldSetter will set the
|
||||
// value on the ScalarNode.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// Comments for the field
|
||||
Comments Comments `yaml:"comments,omitempty"`
|
||||
|
||||
// Value is the value to set.
|
||||
// Optional if Kind is set.
|
||||
Value *RNode `yaml:"value,omitempty"`
|
||||
|
||||
StringValue string `yaml:"stringValue,omitempty"`
|
||||
|
||||
// OverrideStyle can be set to override the style of the existing node
|
||||
// when setting it. Otherwise, if an existing node is found, the style is
|
||||
// retained.
|
||||
OverrideStyle bool `yaml:"overrideStyle,omitempty"`
|
||||
|
||||
// AppendKeyStyle defines the style of the key when no existing node is
|
||||
// found, and a new node is appended.
|
||||
AppendKeyStyle Style `yaml:"appendKeyStyle,omitempty"`
|
||||
}
|
||||
|
||||
func (s FieldSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
if s.StringValue != "" && s.Value == nil {
|
||||
s.Value = NewScalarRNode(s.StringValue)
|
||||
}
|
||||
|
||||
// need to set style for strings not recognized by yaml 1.1 to quoted if not previously set
|
||||
// TODO: fix in upstream yaml library so this can be handled with yaml SetString
|
||||
if s.Value.IsStringValue() && !s.OverrideStyle && s.Value.YNode().Style == 0 && IsYaml1_1NonString(s.Value.YNode()) {
|
||||
s.Value.YNode().Style = yaml.DoubleQuotedStyle
|
||||
}
|
||||
|
||||
if s.Name == "" {
|
||||
if err := ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
|
||||
return rn, err
|
||||
}
|
||||
if IsMissingOrNull(s.Value) {
|
||||
return rn, nil
|
||||
}
|
||||
// only apply the style if there is not an existing style
|
||||
// or we want to override it
|
||||
if !s.OverrideStyle || s.Value.YNode().Style == 0 {
|
||||
// keep the original style if it exists
|
||||
s.Value.YNode().Style = rn.YNode().Style
|
||||
}
|
||||
rn.SetYNode(s.Value.YNode())
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
// Clearing nil fields:
|
||||
// 1. Clear any fields with no value
|
||||
// 2. Clear any "null" YAML fields unless we explicitly want to keep them
|
||||
// This is to balance
|
||||
// 1. Persisting 'null' values passed by the user (see issue #4628)
|
||||
// 2. Avoiding producing noisy documents that add any field defaulting to
|
||||
// 'nil' even if they weren't present in the source document
|
||||
if s.Value == nil || (s.Value.IsTaggedNull() && !s.Value.ShouldKeep) {
|
||||
return rn.Pipe(Clear(s.Name))
|
||||
}
|
||||
|
||||
field, err := rn.Pipe(FieldMatcher{Name: s.Name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if field != nil {
|
||||
// only apply the style if there is not an existing style
|
||||
// or we want to override it
|
||||
if !s.OverrideStyle || field.YNode().Style == 0 {
|
||||
// keep the original style if it exists
|
||||
s.Value.YNode().Style = field.YNode().Style
|
||||
}
|
||||
// need to def ref the Node since field is ephemeral
|
||||
field.SetYNode(s.Value.YNode())
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// create the field
|
||||
rn.YNode().Content = append(
|
||||
rn.YNode().Content,
|
||||
&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: s.Name,
|
||||
Style: s.AppendKeyStyle,
|
||||
HeadComment: s.Comments.HeadComment,
|
||||
LineComment: s.Comments.LineComment,
|
||||
FootComment: s.Comments.FootComment,
|
||||
},
|
||||
s.Value.YNode())
|
||||
return s.Value, nil
|
||||
}
|
||||
|
||||
// Tee calls the provided Filters, and returns its argument rather than the result
|
||||
// of the filters.
|
||||
// May be used to fork sub-filters from a call.
|
||||
// e.g. locate field, set value; locate another field, set another value
|
||||
func Tee(filters ...Filter) Filter {
|
||||
return TeePiper{Filters: filters}
|
||||
}
|
||||
|
||||
// TeePiper Calls a slice of Filters and returns its input.
|
||||
// May be used to fork sub-filters from a call.
|
||||
// e.g. locate field, set value; locate another field, set another value
|
||||
type TeePiper struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Filters are the set of Filters run by TeePiper.
|
||||
Filters []Filter `yaml:"filters,omitempty"`
|
||||
}
|
||||
|
||||
func (t TeePiper) Filter(rn *RNode) (*RNode, error) {
|
||||
_, err := rn.Pipe(t.Filters...)
|
||||
return rn, err
|
||||
}
|
||||
|
||||
// IsCreate returns true if kind is specified
|
||||
func IsCreate(kind yaml.Kind) bool {
|
||||
return kind != 0
|
||||
}
|
||||
|
||||
// IsMissingOrError returns true if rn is NOT found or err is non-nil
|
||||
func IsMissingOrError(rn *RNode, err error) bool {
|
||||
return rn == nil || err != nil
|
||||
}
|
||||
|
||||
// IsFoundOrError returns true if rn is found or err is non-nil
|
||||
func IsFoundOrError(rn *RNode, err error) bool {
|
||||
return rn != nil || err != nil
|
||||
}
|
||||
|
||||
func ErrorIfAnyInvalidAndNonNull(kind yaml.Kind, rn ...*RNode) error {
|
||||
for i := range rn {
|
||||
if IsMissingOrNull(rn[i]) {
|
||||
continue
|
||||
}
|
||||
if err := ErrorIfInvalid(rn[i], kind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InvalidNodeKindError struct {
|
||||
expectedKind yaml.Kind
|
||||
node *RNode
|
||||
}
|
||||
|
||||
func (e *InvalidNodeKindError) Error() string {
|
||||
msg := fmt.Sprintf("wrong node kind: expected %s but got %s",
|
||||
nodeKindString(e.expectedKind), nodeKindString(e.node.YNode().Kind))
|
||||
if content, err := e.node.String(); err == nil {
|
||||
msg += fmt.Sprintf(": node contents:\n%s", content)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *InvalidNodeKindError) Unwrap() error {
|
||||
return errors.Errorf("InvalidNodeKindError")
|
||||
}
|
||||
|
||||
func (e *InvalidNodeKindError) ActualNodeKind() Kind {
|
||||
return e.node.YNode().Kind
|
||||
}
|
||||
|
||||
func ErrorIfInvalid(rn *RNode, kind yaml.Kind) error {
|
||||
if IsMissingOrNull(rn) {
|
||||
// node has no type, pass validation
|
||||
return nil
|
||||
}
|
||||
|
||||
if rn.YNode().Kind != kind {
|
||||
return &InvalidNodeKindError{node: rn, expectedKind: kind}
|
||||
}
|
||||
|
||||
if kind == yaml.MappingNode {
|
||||
if len(rn.YNode().Content)%2 != 0 {
|
||||
return errors.Errorf(
|
||||
"yaml MappingNodes must have even length contents: %v", spew.Sdump(rn))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsListIndex returns true if p is an index into a Val.
|
||||
// e.g. [fieldName=fieldValue]
|
||||
// e.g. [=primitiveValue]
|
||||
func IsListIndex(p string) bool {
|
||||
return strings.HasPrefix(p, "[") && strings.HasSuffix(p, "]")
|
||||
}
|
||||
|
||||
// IsIdxNumber returns true if p is an index number.
|
||||
// e.g. 1
|
||||
func IsIdxNumber(p string) bool {
|
||||
idx, err := strconv.Atoi(p)
|
||||
return err == nil && idx >= 0
|
||||
}
|
||||
|
||||
// IsWildcard returns true if p is matching every elements.
|
||||
// e.g. "*"
|
||||
func IsWildcard(p string) bool {
|
||||
return p == "*"
|
||||
}
|
||||
|
||||
// SplitIndexNameValue splits a lookup part Val index into the field name
|
||||
// and field value to match.
|
||||
// e.g. splits [name=nginx] into (name, nginx)
|
||||
// e.g. splits [=-jar] into ("", -jar)
|
||||
func SplitIndexNameValue(p string) (string, string, error) {
|
||||
elem := strings.TrimSuffix(p, "]")
|
||||
elem = strings.TrimPrefix(elem, "[")
|
||||
parts := strings.SplitN(elem, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
return "", "", fmt.Errorf("list path element must contain fieldName=fieldValue for element to match")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/labels/zz_generated.deepcopy.go
|
||||
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
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 labels
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Requirement) DeepCopyInto(out *Requirement) {
|
||||
*out = *in
|
||||
if in.strValues != nil {
|
||||
in, out := &in.strValues, &out.strValues
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Requirement.
|
||||
func (in *Requirement) DeepCopy() *Requirement {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Requirement)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/labels/labels.go
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Labels allows you to present labels independently from their storage.
|
||||
type Labels interface {
|
||||
// Has returns whether the provided label exists.
|
||||
Has(label string) (exists bool)
|
||||
|
||||
// Get returns the value for the provided label.
|
||||
Get(label string) (value string)
|
||||
}
|
||||
|
||||
// Set is a map of label:value. It implements Labels.
|
||||
type Set map[string]string
|
||||
|
||||
// String returns all labels listed as a human readable string.
|
||||
// Conveniently, exactly the format that ParseSelector takes.
|
||||
func (ls Set) String() string {
|
||||
selector := make([]string, 0, len(ls))
|
||||
for key, value := range ls {
|
||||
selector = append(selector, key+"="+value)
|
||||
}
|
||||
// Sort for determinism.
|
||||
sort.StringSlice(selector).Sort()
|
||||
return strings.Join(selector, ",")
|
||||
}
|
||||
|
||||
// Has returns whether the provided label exists in the map.
|
||||
func (ls Set) Has(label string) bool {
|
||||
_, exists := ls[label]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Get returns the value in the map for the provided label.
|
||||
func (ls Set) Get(label string) string {
|
||||
return ls[label]
|
||||
}
|
||||
|
||||
// AsSelector converts labels into a selectors. It does not
|
||||
// perform any validation, which means the server will reject
|
||||
// the request if the Set contains invalid values.
|
||||
func (ls Set) AsSelector() Selector {
|
||||
return SelectorFromSet(ls)
|
||||
}
|
||||
|
||||
// AsValidatedSelector converts labels into a selectors.
|
||||
// The Set is validated client-side, which allows to catch errors early.
|
||||
func (ls Set) AsValidatedSelector() (Selector, error) {
|
||||
return ValidatedSelectorFromSet(ls)
|
||||
}
|
||||
|
||||
// AsSelectorPreValidated converts labels into a selector, but
|
||||
// assumes that labels are already validated and thus doesn't
|
||||
// perform any validation.
|
||||
// According to our measurements this is significantly faster
|
||||
// in codepaths that matter at high scale.
|
||||
func (ls Set) AsSelectorPreValidated() Selector {
|
||||
return SelectorFromValidatedSet(ls)
|
||||
}
|
||||
|
||||
// FormatLabels convert label map into plain string
|
||||
func FormatLabels(labelMap map[string]string) string {
|
||||
l := Set(labelMap).String()
|
||||
if l == "" {
|
||||
l = "<none>"
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Conflicts takes 2 maps and returns true if there a key match between
|
||||
// the maps but the value doesn't match, and returns false in other cases
|
||||
func Conflicts(labels1, labels2 Set) bool {
|
||||
small := labels1
|
||||
big := labels2
|
||||
if len(labels2) < len(labels1) {
|
||||
small = labels2
|
||||
big = labels1
|
||||
}
|
||||
|
||||
for k, v := range small {
|
||||
if val, match := big[k]; match {
|
||||
if val != v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Merge combines given maps, and does not check for any conflicts
|
||||
// between the maps. In case of conflicts, second map (labels2) wins
|
||||
func Merge(labels1, labels2 Set) Set {
|
||||
mergedMap := Set{}
|
||||
|
||||
for k, v := range labels1 {
|
||||
mergedMap[k] = v
|
||||
}
|
||||
for k, v := range labels2 {
|
||||
mergedMap[k] = v
|
||||
}
|
||||
return mergedMap
|
||||
}
|
||||
|
||||
// Equals returns true if the given maps are equal
|
||||
func Equals(labels1, labels2 Set) bool {
|
||||
if len(labels1) != len(labels2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, v := range labels1 {
|
||||
value, ok := labels2[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if value != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AreLabelsInWhiteList verifies if the provided label list
|
||||
// is in the provided whitelist and returns true, otherwise false.
|
||||
func AreLabelsInWhiteList(labels, whitelist Set) bool {
|
||||
if len(whitelist) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for k, v := range labels {
|
||||
value, ok := whitelist[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if value != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ConvertSelectorToLabelsMap converts selector string to labels map
|
||||
// and validates keys and values
|
||||
func ConvertSelectorToLabelsMap(selector string) (Set, error) {
|
||||
labelsMap := Set{}
|
||||
|
||||
if len(selector) == 0 {
|
||||
return labelsMap, nil
|
||||
}
|
||||
|
||||
labels := strings.Split(selector, ",")
|
||||
for _, label := range labels {
|
||||
l := strings.Split(label, "=")
|
||||
if len(l) != 2 {
|
||||
return labelsMap, fmt.Errorf("invalid selector: %s", l)
|
||||
}
|
||||
key := strings.TrimSpace(l[0])
|
||||
if err := validateLabelKey(key); err != nil {
|
||||
return labelsMap, err
|
||||
}
|
||||
value := strings.TrimSpace(l[1])
|
||||
if err := validateLabelValue(key, value); err != nil {
|
||||
return labelsMap, err
|
||||
}
|
||||
labelsMap[key] = value
|
||||
}
|
||||
return labelsMap, nil
|
||||
}
|
||||
+925
@@ -0,0 +1,925 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/labels/selector.go
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 labels
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"log"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/selection"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/validation"
|
||||
)
|
||||
|
||||
// Requirements is AND of all requirements.
|
||||
type Requirements []Requirement
|
||||
|
||||
// Selector represents a label selector.
|
||||
type Selector interface {
|
||||
// Matches returns true if this selector matches the given set of labels.
|
||||
Matches(Labels) bool
|
||||
|
||||
// Empty returns true if this selector does not restrict the selection space.
|
||||
Empty() bool
|
||||
|
||||
// String returns a human readable string that represents this selector.
|
||||
String() string
|
||||
|
||||
// Add adds requirements to the Selector
|
||||
Add(r ...Requirement) Selector
|
||||
|
||||
// Requirements converts this interface into Requirements to expose
|
||||
// more detailed selection information.
|
||||
// If there are querying parameters, it will return converted requirements and selectable=true.
|
||||
// If this selector doesn't want to select anything, it will return selectable=false.
|
||||
Requirements() (requirements Requirements, selectable bool)
|
||||
|
||||
// Make a deep copy of the selector.
|
||||
DeepCopySelector() Selector
|
||||
|
||||
// RequiresExactMatch allows a caller to introspect whether a given selector
|
||||
// requires a single specific label to be set, and if so returns the value it
|
||||
// requires.
|
||||
RequiresExactMatch(label string) (value string, found bool)
|
||||
}
|
||||
|
||||
// Everything returns a selector that matches all labels.
|
||||
func Everything() Selector {
|
||||
return internalSelector{}
|
||||
}
|
||||
|
||||
type nothingSelector struct{}
|
||||
|
||||
func (n nothingSelector) Matches(_ Labels) bool { return false }
|
||||
func (n nothingSelector) Empty() bool { return false }
|
||||
func (n nothingSelector) String() string { return "" }
|
||||
func (n nothingSelector) Add(_ ...Requirement) Selector { return n }
|
||||
func (n nothingSelector) Requirements() (Requirements, bool) { return nil, false }
|
||||
func (n nothingSelector) DeepCopySelector() Selector { return n }
|
||||
func (n nothingSelector) RequiresExactMatch(label string) (value string, found bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Nothing returns a selector that matches no labels
|
||||
func Nothing() Selector {
|
||||
return nothingSelector{}
|
||||
}
|
||||
|
||||
// NewSelector returns a nil selector
|
||||
func NewSelector() Selector {
|
||||
return internalSelector(nil)
|
||||
}
|
||||
|
||||
type internalSelector []Requirement
|
||||
|
||||
func (s internalSelector) DeepCopy() internalSelector {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]Requirement, len(s))
|
||||
for i := range s {
|
||||
s[i].DeepCopyInto(&result[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s internalSelector) DeepCopySelector() Selector {
|
||||
return s.DeepCopy()
|
||||
}
|
||||
|
||||
// ByKey sorts requirements by key to obtain deterministic parser
|
||||
type ByKey []Requirement
|
||||
|
||||
func (a ByKey) Len() int { return len(a) }
|
||||
|
||||
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func (a ByKey) Less(i, j int) bool { return a[i].key < a[j].key }
|
||||
|
||||
// Requirement contains values, a key, and an operator that relates the key and values.
|
||||
// The zero value of Requirement is invalid.
|
||||
// Requirement implements both set based match and exact match
|
||||
// Requirement should be initialized via NewRequirement constructor for creating a valid Requirement.
|
||||
type Requirement struct {
|
||||
key string
|
||||
operator selection.Operator
|
||||
// In huge majority of cases we have at most one value here.
|
||||
// It is generally faster to operate on a single-element slice
|
||||
// than on a single-element map, so we have a slice here.
|
||||
strValues []string
|
||||
}
|
||||
|
||||
// NewRequirement is the constructor for a Requirement.
|
||||
// If any of these rules is violated, an error is returned:
|
||||
// (1) The operator can only be In, NotIn, Equals, DoubleEquals, NotEquals, Exists, or DoesNotExist.
|
||||
// (2) If the operator is In or NotIn, the values set must be non-empty.
|
||||
// (3) If the operator is Equals, DoubleEquals, or NotEquals, the values set must contain one value.
|
||||
// (4) If the operator is Exists or DoesNotExist, the value set must be empty.
|
||||
// (5) If the operator is Gt or Lt, the values set must contain only one value, which will be interpreted as an integer.
|
||||
// (6) The key is invalid due to its length, or sequence
|
||||
// of characters. See validateLabelKey for more details.
|
||||
//
|
||||
// The empty string is a valid value in the input values set.
|
||||
func NewRequirement(key string, op selection.Operator, vals []string) (*Requirement, error) {
|
||||
if err := validateLabelKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch op {
|
||||
case selection.In, selection.NotIn:
|
||||
if len(vals) == 0 {
|
||||
return nil, fmt.Errorf("for 'in', 'notin' operators, values set can't be empty")
|
||||
}
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals:
|
||||
if len(vals) != 1 {
|
||||
return nil, fmt.Errorf("exact-match compatibility requires one single value")
|
||||
}
|
||||
case selection.Exists, selection.DoesNotExist:
|
||||
if len(vals) != 0 {
|
||||
return nil, fmt.Errorf("values set must be empty for exists and does not exist")
|
||||
}
|
||||
case selection.GreaterThan, selection.LessThan:
|
||||
if len(vals) != 1 {
|
||||
return nil, fmt.Errorf("for 'Gt', 'Lt' operators, exactly one value is required")
|
||||
}
|
||||
for i := range vals {
|
||||
if _, err := strconv.ParseInt(vals[i], 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("for 'Gt', 'Lt' operators, the value must be an integer")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("operator '%v' is not recognized", op)
|
||||
}
|
||||
|
||||
for i := range vals {
|
||||
if err := validateLabelValue(key, vals[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Requirement{key: key, operator: op, strValues: vals}, nil
|
||||
}
|
||||
|
||||
func (r *Requirement) hasValue(value string) bool {
|
||||
for i := range r.strValues {
|
||||
if r.strValues[i] == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Matches returns true if the Requirement matches the input Labels.
|
||||
// There is a match in the following cases:
|
||||
// (1) The operator is Exists and Labels has the Requirement's key.
|
||||
// (2) The operator is In, Labels has the Requirement's key and Labels'
|
||||
// value for that key is in Requirement's value set.
|
||||
// (3) The operator is NotIn, Labels has the Requirement's key and
|
||||
// Labels' value for that key is not in Requirement's value set.
|
||||
// (4) The operator is DoesNotExist or NotIn and Labels does not have the
|
||||
// Requirement's key.
|
||||
// (5) The operator is GreaterThanOperator or LessThanOperator, and Labels has
|
||||
// the Requirement's key and the corresponding value satisfies mathematical inequality.
|
||||
func (r *Requirement) Matches(ls Labels) bool {
|
||||
switch r.operator {
|
||||
case selection.In, selection.Equals, selection.DoubleEquals:
|
||||
if !ls.Has(r.key) {
|
||||
return false
|
||||
}
|
||||
return r.hasValue(ls.Get(r.key))
|
||||
case selection.NotIn, selection.NotEquals:
|
||||
if !ls.Has(r.key) {
|
||||
return true
|
||||
}
|
||||
return !r.hasValue(ls.Get(r.key))
|
||||
case selection.Exists:
|
||||
return ls.Has(r.key)
|
||||
case selection.DoesNotExist:
|
||||
return !ls.Has(r.key)
|
||||
case selection.GreaterThan, selection.LessThan:
|
||||
if !ls.Has(r.key) {
|
||||
return false
|
||||
}
|
||||
lsValue, err := strconv.ParseInt(ls.Get(r.key), 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("ParseInt failed for value %+v in label %+v, %+v", ls.Get(r.key), ls, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// There should be only one strValue in r.strValues, and can be converted to an integer.
|
||||
if len(r.strValues) != 1 {
|
||||
log.Printf("Invalid values count %+v of requirement %#v, for 'Gt', 'Lt' operators, exactly one value is required", len(r.strValues), r)
|
||||
return false
|
||||
}
|
||||
|
||||
var rValue int64
|
||||
for i := range r.strValues {
|
||||
rValue, err = strconv.ParseInt(r.strValues[i], 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("ParseInt failed for value %+v in requirement %#v, for 'Gt', 'Lt' operators, the value must be an integer", r.strValues[i], r)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (r.operator == selection.GreaterThan && lsValue > rValue) || (r.operator == selection.LessThan && lsValue < rValue)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns requirement key
|
||||
func (r *Requirement) Key() string {
|
||||
return r.key
|
||||
}
|
||||
|
||||
// Operator returns requirement operator
|
||||
func (r *Requirement) Operator() selection.Operator {
|
||||
return r.operator
|
||||
}
|
||||
|
||||
// Values returns requirement values
|
||||
func (r *Requirement) Values() sets.String {
|
||||
ret := sets.String{}
|
||||
for i := range r.strValues {
|
||||
ret.Insert(r.strValues[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Empty returns true if the internalSelector doesn't restrict selection space
|
||||
func (lsel internalSelector) Empty() bool {
|
||||
if lsel == nil {
|
||||
return true
|
||||
}
|
||||
return len(lsel) == 0
|
||||
}
|
||||
|
||||
// String returns a human-readable string that represents this
|
||||
// Requirement. If called on an invalid Requirement, an error is
|
||||
// returned. See NewRequirement for creating a valid Requirement.
|
||||
func (r *Requirement) String() string {
|
||||
var buffer bytes.Buffer
|
||||
if r.operator == selection.DoesNotExist {
|
||||
buffer.WriteString("!")
|
||||
}
|
||||
buffer.WriteString(r.key)
|
||||
|
||||
switch r.operator {
|
||||
case selection.Equals:
|
||||
buffer.WriteString("=")
|
||||
case selection.DoubleEquals:
|
||||
buffer.WriteString("==")
|
||||
case selection.NotEquals:
|
||||
buffer.WriteString("!=")
|
||||
case selection.In:
|
||||
buffer.WriteString(" in ")
|
||||
case selection.NotIn:
|
||||
buffer.WriteString(" notin ")
|
||||
case selection.GreaterThan:
|
||||
buffer.WriteString(">")
|
||||
case selection.LessThan:
|
||||
buffer.WriteString("<")
|
||||
case selection.Exists, selection.DoesNotExist:
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
switch r.operator {
|
||||
case selection.In, selection.NotIn:
|
||||
buffer.WriteString("(")
|
||||
}
|
||||
if len(r.strValues) == 1 {
|
||||
buffer.WriteString(r.strValues[0])
|
||||
} else { // only > 1 since == 0 prohibited by NewRequirement
|
||||
// normalizes value order on output, without mutating the in-memory selector representation
|
||||
// also avoids normalization when it is not required, and ensures we do not mutate shared data
|
||||
buffer.WriteString(strings.Join(safeSort(r.strValues), ","))
|
||||
}
|
||||
|
||||
switch r.operator {
|
||||
case selection.In, selection.NotIn:
|
||||
buffer.WriteString(")")
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// safeSort sort input strings without modification
|
||||
func safeSort(in []string) []string {
|
||||
if sort.StringsAreSorted(in) {
|
||||
return in
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
copy(out, in)
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Add adds requirements to the selector. It copies the current selector returning a new one
|
||||
func (lsel internalSelector) Add(reqs ...Requirement) Selector {
|
||||
var sel internalSelector
|
||||
for ix := range lsel {
|
||||
sel = append(sel, lsel[ix])
|
||||
}
|
||||
for _, r := range reqs {
|
||||
sel = append(sel, r)
|
||||
}
|
||||
sort.Sort(ByKey(sel))
|
||||
return sel
|
||||
}
|
||||
|
||||
// Matches for a internalSelector returns true if all
|
||||
// its Requirements match the input Labels. If any
|
||||
// Requirement does not match, false is returned.
|
||||
func (lsel internalSelector) Matches(l Labels) bool {
|
||||
for ix := range lsel {
|
||||
if matches := lsel[ix].Matches(l); !matches {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (lsel internalSelector) Requirements() (Requirements, bool) { return Requirements(lsel), true }
|
||||
|
||||
// String returns a comma-separated string of all
|
||||
// the internalSelector Requirements' human-readable strings.
|
||||
func (lsel internalSelector) String() string {
|
||||
var reqs []string
|
||||
for ix := range lsel {
|
||||
reqs = append(reqs, lsel[ix].String())
|
||||
}
|
||||
return strings.Join(reqs, ",")
|
||||
}
|
||||
|
||||
// RequiresExactMatch introspect whether a given selector requires a single specific field
|
||||
// to be set, and if so returns the value it requires.
|
||||
func (lsel internalSelector) RequiresExactMatch(label string) (value string, found bool) {
|
||||
for ix := range lsel {
|
||||
if lsel[ix].key == label {
|
||||
switch lsel[ix].operator {
|
||||
case selection.Equals, selection.DoubleEquals, selection.In:
|
||||
if len(lsel[ix].strValues) == 1 {
|
||||
return lsel[ix].strValues[0], true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Token represents constant definition for lexer token
|
||||
type Token int
|
||||
|
||||
const (
|
||||
// ErrorToken represents scan error
|
||||
ErrorToken Token = iota
|
||||
// EndOfStringToken represents end of string
|
||||
EndOfStringToken
|
||||
// ClosedParToken represents close parenthesis
|
||||
ClosedParToken
|
||||
// CommaToken represents the comma
|
||||
CommaToken
|
||||
// DoesNotExistToken represents logic not
|
||||
DoesNotExistToken
|
||||
// DoubleEqualsToken represents double equals
|
||||
DoubleEqualsToken
|
||||
// EqualsToken represents equal
|
||||
EqualsToken
|
||||
// GreaterThanToken represents greater than
|
||||
GreaterThanToken
|
||||
// IdentifierToken represents identifier, e.g. keys and values
|
||||
IdentifierToken
|
||||
// InToken represents in
|
||||
InToken
|
||||
// LessThanToken represents less than
|
||||
LessThanToken
|
||||
// NotEqualsToken represents not equal
|
||||
NotEqualsToken
|
||||
// NotInToken represents not in
|
||||
NotInToken
|
||||
// OpenParToken represents open parenthesis
|
||||
OpenParToken
|
||||
)
|
||||
|
||||
// string2token contains the mapping between lexer Token and token literal
|
||||
// (except IdentifierToken, EndOfStringToken and ErrorToken since it makes no sense)
|
||||
var string2token = map[string]Token{
|
||||
")": ClosedParToken,
|
||||
",": CommaToken,
|
||||
"!": DoesNotExistToken,
|
||||
"==": DoubleEqualsToken,
|
||||
"=": EqualsToken,
|
||||
">": GreaterThanToken,
|
||||
"in": InToken,
|
||||
"<": LessThanToken,
|
||||
"!=": NotEqualsToken,
|
||||
"notin": NotInToken,
|
||||
"(": OpenParToken,
|
||||
}
|
||||
|
||||
// ScannedItem contains the Token and the literal produced by the lexer.
|
||||
type ScannedItem struct {
|
||||
tok Token
|
||||
literal string
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the rune is a space, tab, or newline.
|
||||
func isWhitespace(ch byte) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
|
||||
}
|
||||
|
||||
// isSpecialSymbol detect if the character ch can be an operator
|
||||
func isSpecialSymbol(ch byte) bool {
|
||||
switch ch {
|
||||
case '=', '!', '(', ')', ',', '>', '<':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Lexer represents the Lexer struct for label selector.
|
||||
// It contains necessary informationt to tokenize the input string
|
||||
type Lexer struct {
|
||||
// s stores the string to be tokenized
|
||||
s string
|
||||
// pos is the position currently tokenized
|
||||
pos int
|
||||
}
|
||||
|
||||
// read return the character currently lexed
|
||||
// increment the position and check the buffer overflow
|
||||
func (l *Lexer) read() (b byte) {
|
||||
b = 0
|
||||
if l.pos < len(l.s) {
|
||||
b = l.s[l.pos]
|
||||
l.pos++
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// unread 'undoes' the last read character
|
||||
func (l *Lexer) unread() {
|
||||
l.pos--
|
||||
}
|
||||
|
||||
// scanIDOrKeyword scans string to recognize literal token (for example 'in') or an identifier.
|
||||
func (l *Lexer) scanIDOrKeyword() (tok Token, lit string) {
|
||||
var buffer []byte
|
||||
IdentifierLoop:
|
||||
for {
|
||||
switch ch := l.read(); {
|
||||
case ch == 0:
|
||||
break IdentifierLoop
|
||||
case isSpecialSymbol(ch) || isWhitespace(ch):
|
||||
l.unread()
|
||||
break IdentifierLoop
|
||||
default:
|
||||
buffer = append(buffer, ch)
|
||||
}
|
||||
}
|
||||
s := string(buffer)
|
||||
if val, ok := string2token[s]; ok { // is a literal token?
|
||||
return val, s
|
||||
}
|
||||
return IdentifierToken, s // otherwise is an identifier
|
||||
}
|
||||
|
||||
// scanSpecialSymbol scans string starting with special symbol.
|
||||
// special symbol identify non literal operators. "!=", "==", "="
|
||||
func (l *Lexer) scanSpecialSymbol() (Token, string) {
|
||||
lastScannedItem := ScannedItem{}
|
||||
var buffer []byte
|
||||
SpecialSymbolLoop:
|
||||
for {
|
||||
switch ch := l.read(); {
|
||||
case ch == 0:
|
||||
break SpecialSymbolLoop
|
||||
case isSpecialSymbol(ch):
|
||||
buffer = append(buffer, ch)
|
||||
if token, ok := string2token[string(buffer)]; ok {
|
||||
lastScannedItem = ScannedItem{tok: token, literal: string(buffer)}
|
||||
} else if lastScannedItem.tok != 0 {
|
||||
l.unread()
|
||||
break SpecialSymbolLoop
|
||||
}
|
||||
default:
|
||||
l.unread()
|
||||
break SpecialSymbolLoop
|
||||
}
|
||||
}
|
||||
if lastScannedItem.tok == 0 {
|
||||
return ErrorToken, fmt.Sprintf("error expected: keyword found '%s'", buffer)
|
||||
}
|
||||
return lastScannedItem.tok, lastScannedItem.literal
|
||||
}
|
||||
|
||||
// skipWhiteSpaces consumes all blank characters
|
||||
// returning the first non blank character
|
||||
func (l *Lexer) skipWhiteSpaces(ch byte) byte {
|
||||
for {
|
||||
if !isWhitespace(ch) {
|
||||
return ch
|
||||
}
|
||||
ch = l.read()
|
||||
}
|
||||
}
|
||||
|
||||
// Lex returns a pair of Token and the literal
|
||||
// literal is meaningfull only for IdentifierToken token
|
||||
func (l *Lexer) Lex() (tok Token, lit string) {
|
||||
switch ch := l.skipWhiteSpaces(l.read()); {
|
||||
case ch == 0:
|
||||
return EndOfStringToken, ""
|
||||
case isSpecialSymbol(ch):
|
||||
l.unread()
|
||||
return l.scanSpecialSymbol()
|
||||
default:
|
||||
l.unread()
|
||||
return l.scanIDOrKeyword()
|
||||
}
|
||||
}
|
||||
|
||||
// Parser data structure contains the label selector parser data structure
|
||||
type Parser struct {
|
||||
l *Lexer
|
||||
scannedItems []ScannedItem
|
||||
position int
|
||||
}
|
||||
|
||||
// ParserContext represents context during parsing:
|
||||
// some literal for example 'in' and 'notin' can be
|
||||
// recognized as operator for example 'x in (a)' but
|
||||
// it can be recognized as value for example 'value in (in)'
|
||||
type ParserContext int
|
||||
|
||||
const (
|
||||
// KeyAndOperator represents key and operator
|
||||
KeyAndOperator ParserContext = iota
|
||||
// Values represents values
|
||||
Values
|
||||
)
|
||||
|
||||
// lookahead func returns the current token and string. No increment of current position
|
||||
func (p *Parser) lookahead(context ParserContext) (Token, string) {
|
||||
tok, lit := p.scannedItems[p.position].tok, p.scannedItems[p.position].literal
|
||||
if context == Values {
|
||||
switch tok {
|
||||
case InToken, NotInToken:
|
||||
tok = IdentifierToken
|
||||
}
|
||||
}
|
||||
return tok, lit
|
||||
}
|
||||
|
||||
// consume returns current token and string. Increments the position
|
||||
func (p *Parser) consume(context ParserContext) (Token, string) {
|
||||
p.position++
|
||||
tok, lit := p.scannedItems[p.position-1].tok, p.scannedItems[p.position-1].literal
|
||||
if context == Values {
|
||||
switch tok {
|
||||
case InToken, NotInToken:
|
||||
tok = IdentifierToken
|
||||
}
|
||||
}
|
||||
return tok, lit
|
||||
}
|
||||
|
||||
// scan runs through the input string and stores the ScannedItem in an array
|
||||
// Parser can now lookahead and consume the tokens
|
||||
func (p *Parser) scan() {
|
||||
for {
|
||||
token, literal := p.l.Lex()
|
||||
p.scannedItems = append(p.scannedItems, ScannedItem{token, literal})
|
||||
if token == EndOfStringToken {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse runs the left recursive descending algorithm
|
||||
// on input string. It returns a list of Requirement objects.
|
||||
func (p *Parser) parse() (internalSelector, error) {
|
||||
p.scan() // init scannedItems
|
||||
|
||||
var requirements internalSelector
|
||||
for {
|
||||
tok, lit := p.lookahead(Values)
|
||||
switch tok {
|
||||
case IdentifierToken, DoesNotExistToken:
|
||||
r, err := p.parseRequirement()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse requirement: %v", err)
|
||||
}
|
||||
requirements = append(requirements, *r)
|
||||
t, l := p.consume(Values)
|
||||
switch t {
|
||||
case EndOfStringToken:
|
||||
return requirements, nil
|
||||
case CommaToken:
|
||||
t2, l2 := p.lookahead(Values)
|
||||
if t2 != IdentifierToken && t2 != DoesNotExistToken {
|
||||
return nil, fmt.Errorf("found '%s', expected: identifier after ','", l2)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("found '%s', expected: ',' or 'end of string'", l)
|
||||
}
|
||||
case EndOfStringToken:
|
||||
return requirements, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("found '%s', expected: !, identifier, or 'end of string'", lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseRequirement() (*Requirement, error) {
|
||||
key, operator, err := p.parseKeyAndInferOperator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if operator == selection.Exists || operator == selection.DoesNotExist { // operator found lookahead set checked
|
||||
return NewRequirement(key, operator, []string{})
|
||||
}
|
||||
operator, err = p.parseOperator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var values sets.String
|
||||
switch operator {
|
||||
case selection.In, selection.NotIn:
|
||||
values, err = p.parseValues()
|
||||
case selection.Equals, selection.DoubleEquals, selection.NotEquals, selection.GreaterThan, selection.LessThan:
|
||||
values, err = p.parseExactValue()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRequirement(key, operator, values.List())
|
||||
|
||||
}
|
||||
|
||||
// parseKeyAndInferOperator parse literals.
|
||||
// in case of no operator '!, in, notin, ==, =, !=' are found
|
||||
// the 'exists' operator is inferred
|
||||
func (p *Parser) parseKeyAndInferOperator() (string, selection.Operator, error) {
|
||||
var operator selection.Operator
|
||||
tok, literal := p.consume(Values)
|
||||
if tok == DoesNotExistToken {
|
||||
operator = selection.DoesNotExist
|
||||
tok, literal = p.consume(Values)
|
||||
}
|
||||
if tok != IdentifierToken {
|
||||
err := fmt.Errorf("found '%s', expected: identifier", literal)
|
||||
return "", "", err
|
||||
}
|
||||
if err := validateLabelKey(literal); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if t, _ := p.lookahead(Values); t == EndOfStringToken || t == CommaToken {
|
||||
if operator != selection.DoesNotExist {
|
||||
operator = selection.Exists
|
||||
}
|
||||
}
|
||||
return literal, operator, nil
|
||||
}
|
||||
|
||||
// parseOperator return operator and eventually matchType
|
||||
// matchType can be exact
|
||||
func (p *Parser) parseOperator() (op selection.Operator, err error) {
|
||||
tok, lit := p.consume(KeyAndOperator)
|
||||
switch tok {
|
||||
// DoesNotExistToken shouldn't be here because it's a unary operator, not a binary operator
|
||||
case InToken:
|
||||
op = selection.In
|
||||
case EqualsToken:
|
||||
op = selection.Equals
|
||||
case DoubleEqualsToken:
|
||||
op = selection.DoubleEquals
|
||||
case GreaterThanToken:
|
||||
op = selection.GreaterThan
|
||||
case LessThanToken:
|
||||
op = selection.LessThan
|
||||
case NotInToken:
|
||||
op = selection.NotIn
|
||||
case NotEqualsToken:
|
||||
op = selection.NotEquals
|
||||
default:
|
||||
return "", fmt.Errorf("found '%s', expected: '=', '!=', '==', 'in', notin'", lit)
|
||||
}
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// parseValues parses the values for set based matching (x,y,z)
|
||||
func (p *Parser) parseValues() (sets.String, error) {
|
||||
tok, lit := p.consume(Values)
|
||||
if tok != OpenParToken {
|
||||
return nil, fmt.Errorf("found '%s' expected: '('", lit)
|
||||
}
|
||||
tok, lit = p.lookahead(Values)
|
||||
switch tok {
|
||||
case IdentifierToken, CommaToken:
|
||||
s, err := p.parseIdentifiersList() // handles general cases
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if tok, _ = p.consume(Values); tok != ClosedParToken {
|
||||
return nil, fmt.Errorf("found '%s', expected: ')'", lit)
|
||||
}
|
||||
return s, nil
|
||||
case ClosedParToken: // handles "()"
|
||||
p.consume(Values)
|
||||
return sets.NewString(""), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("found '%s', expected: ',', ')' or identifier", lit)
|
||||
}
|
||||
}
|
||||
|
||||
// parseIdentifiersList parses a (possibly empty) list of
|
||||
// of comma separated (possibly empty) identifiers
|
||||
func (p *Parser) parseIdentifiersList() (sets.String, error) {
|
||||
s := sets.NewString()
|
||||
for {
|
||||
tok, lit := p.consume(Values)
|
||||
switch tok {
|
||||
case IdentifierToken:
|
||||
s.Insert(lit)
|
||||
tok2, lit2 := p.lookahead(Values)
|
||||
switch tok2 {
|
||||
case CommaToken:
|
||||
continue
|
||||
case ClosedParToken:
|
||||
return s, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("found '%s', expected: ',' or ')'", lit2)
|
||||
}
|
||||
case CommaToken: // handled here since we can have "(,"
|
||||
if s.Len() == 0 {
|
||||
s.Insert("") // to handle (,
|
||||
}
|
||||
tok2, _ := p.lookahead(Values)
|
||||
if tok2 == ClosedParToken {
|
||||
s.Insert("") // to handle ,) Double "" removed by StringSet
|
||||
return s, nil
|
||||
}
|
||||
if tok2 == CommaToken {
|
||||
p.consume(Values)
|
||||
s.Insert("") // to handle ,, Double "" removed by StringSet
|
||||
}
|
||||
default: // it can be operator
|
||||
return s, fmt.Errorf("found '%s', expected: ',', or identifier", lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseExactValue parses the only value for exact match style
|
||||
func (p *Parser) parseExactValue() (sets.String, error) {
|
||||
s := sets.NewString()
|
||||
tok, lit := p.lookahead(Values)
|
||||
if tok == EndOfStringToken || tok == CommaToken {
|
||||
s.Insert("")
|
||||
return s, nil
|
||||
}
|
||||
tok, lit = p.consume(Values)
|
||||
if tok == IdentifierToken {
|
||||
s.Insert(lit)
|
||||
return s, nil
|
||||
}
|
||||
return nil, fmt.Errorf("found '%s', expected: identifier", lit)
|
||||
}
|
||||
|
||||
// Parse takes a string representing a selector and returns a selector
|
||||
// object, or an error. This parsing function differs from ParseSelector
|
||||
// as they parse different selectors with different syntaxes.
|
||||
// The input will cause an error if it does not follow this form:
|
||||
//
|
||||
// <selector-syntax> ::= <requirement> | <requirement> "," <selector-syntax>
|
||||
// <requirement> ::= [!] KEY [ <set-based-restriction> | <exact-match-restriction> ]
|
||||
// <set-based-restriction> ::= "" | <inclusion-exclusion> <value-set>
|
||||
// <inclusion-exclusion> ::= <inclusion> | <exclusion>
|
||||
// <exclusion> ::= "notin"
|
||||
// <inclusion> ::= "in"
|
||||
// <value-set> ::= "(" <values> ")"
|
||||
// <values> ::= VALUE | VALUE "," <values>
|
||||
// <exact-match-restriction> ::= ["="|"=="|"!="] VALUE
|
||||
//
|
||||
// KEY is a sequence of one or more characters following [ DNS_SUBDOMAIN "/" ] DNS_LABEL. Max length is 63 characters.
|
||||
// VALUE is a sequence of zero or more characters "([A-Za-z0-9_-\.])". Max length is 63 characters.
|
||||
// Delimiter is white space: (' ', '\t')
|
||||
// Example of valid syntax:
|
||||
// "x in (foo,,baz),y,z notin ()"
|
||||
//
|
||||
// Note:
|
||||
// (1) Inclusion - " in " - denotes that the KEY exists and is equal to any of the
|
||||
// VALUEs in its requirement
|
||||
// (2) Exclusion - " notin " - denotes that the KEY is not equal to any
|
||||
// of the VALUEs in its requirement or does not exist
|
||||
// (3) The empty string is a valid VALUE
|
||||
// (4) A requirement with just a KEY - as in "y" above - denotes that
|
||||
// the KEY exists and can be any VALUE.
|
||||
// (5) A requirement with just !KEY requires that the KEY not exist.
|
||||
//
|
||||
func Parse(selector string) (Selector, error) {
|
||||
parsedSelector, err := parse(selector)
|
||||
if err == nil {
|
||||
return parsedSelector, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse parses the string representation of the selector and returns the internalSelector struct.
|
||||
// The callers of this method can then decide how to return the internalSelector struct to their
|
||||
// callers. This function has two callers now, one returns a Selector interface and the other
|
||||
// returns a list of requirements.
|
||||
func parse(selector string) (internalSelector, error) {
|
||||
p := &Parser{l: &Lexer{s: selector, pos: 0}}
|
||||
items, err := p.parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Sort(ByKey(items)) // sort to grant determistic parsing
|
||||
return internalSelector(items), err
|
||||
}
|
||||
|
||||
func validateLabelKey(k string) error {
|
||||
if errs := validation.IsQualifiedName(k); len(errs) != 0 {
|
||||
return fmt.Errorf("invalid label key %q: %s", k, strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabelValue(k, v string) error {
|
||||
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
|
||||
return fmt.Errorf("invalid label value: %q: at key: %q: %s", v, k, strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectorFromSet returns a Selector which will match exactly the given Set. A
|
||||
// nil and empty Sets are considered equivalent to Everything().
|
||||
// It does not perform any validation, which means the server will reject
|
||||
// the request if the Set contains invalid values.
|
||||
func SelectorFromSet(ls Set) Selector {
|
||||
return SelectorFromValidatedSet(ls)
|
||||
}
|
||||
|
||||
// ValidatedSelectorFromSet returns a Selector which will match exactly the given Set. A
|
||||
// nil and empty Sets are considered equivalent to Everything().
|
||||
// The Set is validated client-side, which allows to catch errors early.
|
||||
func ValidatedSelectorFromSet(ls Set) (Selector, error) {
|
||||
if ls == nil || len(ls) == 0 {
|
||||
return internalSelector{}, nil
|
||||
}
|
||||
requirements := make([]Requirement, 0, len(ls))
|
||||
for label, value := range ls {
|
||||
r, err := NewRequirement(label, selection.Equals, []string{value})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requirements = append(requirements, *r)
|
||||
}
|
||||
// sort to have deterministic string representation
|
||||
sort.Sort(ByKey(requirements))
|
||||
return internalSelector(requirements), nil
|
||||
}
|
||||
|
||||
// SelectorFromValidatedSet returns a Selector which will match exactly the given Set.
|
||||
// A nil and empty Sets are considered equivalent to Everything().
|
||||
// It assumes that Set is already validated and doesn't do any validation.
|
||||
func SelectorFromValidatedSet(ls Set) Selector {
|
||||
if ls == nil || len(ls) == 0 {
|
||||
return internalSelector{}
|
||||
}
|
||||
requirements := make([]Requirement, 0, len(ls))
|
||||
for label, value := range ls {
|
||||
requirements = append(requirements, Requirement{key: label, operator: selection.Equals, strValues: []string{value}})
|
||||
}
|
||||
// sort to have deterministic string representation
|
||||
sort.Sort(ByKey(requirements))
|
||||
return internalSelector(requirements)
|
||||
}
|
||||
|
||||
// ParseToRequirements takes a string representing a selector and returns a list of
|
||||
// requirements. This function is suitable for those callers that perform additional
|
||||
// processing on selector requirements.
|
||||
// See the documentation for Parse() function for more details.
|
||||
// TODO: Consider exporting the internalSelector type instead.
|
||||
func ParseToRequirements(selector string) ([]Requirement, error) {
|
||||
return parse(selector)
|
||||
}
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/selection/operator.go
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
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 selection
|
||||
|
||||
// Operator represents a key/field's relationship to value(s).
|
||||
// See labels.Requirement and fields.Requirement for more details.
|
||||
type Operator string
|
||||
|
||||
const (
|
||||
DoesNotExist Operator = "!"
|
||||
Equals Operator = "="
|
||||
DoubleEquals Operator = "=="
|
||||
In Operator = "in"
|
||||
NotEquals Operator = "!="
|
||||
NotIn Operator = "notin"
|
||||
Exists Operator = "exists"
|
||||
GreaterThan Operator = "gt"
|
||||
LessThan Operator = "lt"
|
||||
)
|
||||
Generated
Vendored
+252
@@ -0,0 +1,252 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/errors/errors.go
|
||||
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
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 errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/sets"
|
||||
)
|
||||
|
||||
// MessageCountMap contains occurrence for each error message.
|
||||
type MessageCountMap map[string]int
|
||||
|
||||
// Aggregate represents an object that contains multiple errors, but does not
|
||||
// necessarily have singular semantic meaning.
|
||||
// The aggregate can be used with `errors.Is()` to check for the occurrence of
|
||||
// a specific error type.
|
||||
// Errors.As() is not supported, because the caller presumably cares about a
|
||||
// specific error of potentially multiple that match the given type.
|
||||
type Aggregate interface {
|
||||
error
|
||||
Errors() []error
|
||||
Is(error) bool
|
||||
}
|
||||
|
||||
// NewAggregate converts a slice of errors into an Aggregate interface, which
|
||||
// is itself an implementation of the error interface. If the slice is empty,
|
||||
// this returns nil.
|
||||
// It will check if any of the element of input error list is nil, to avoid
|
||||
// nil pointer panic when call Error().
|
||||
func NewAggregate(errlist []error) Aggregate {
|
||||
if len(errlist) == 0 {
|
||||
return nil
|
||||
}
|
||||
// In case of input error list contains nil
|
||||
var errs []error
|
||||
for _, e := range errlist {
|
||||
if e != nil {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return aggregate(errs)
|
||||
}
|
||||
|
||||
// This helper implements the error and Errors interfaces. Keeping it private
|
||||
// prevents people from making an aggregate of 0 errors, which is not
|
||||
// an error, but does satisfy the error interface.
|
||||
type aggregate []error
|
||||
|
||||
// Error is part of the error interface.
|
||||
func (agg aggregate) Error() string {
|
||||
if len(agg) == 0 {
|
||||
// This should never happen, really.
|
||||
return ""
|
||||
}
|
||||
if len(agg) == 1 {
|
||||
return agg[0].Error()
|
||||
}
|
||||
seenerrs := sets.NewString()
|
||||
result := ""
|
||||
agg.visit(func(err error) bool {
|
||||
msg := err.Error()
|
||||
if seenerrs.Has(msg) {
|
||||
return false
|
||||
}
|
||||
seenerrs.Insert(msg)
|
||||
if len(seenerrs) > 1 {
|
||||
result += ", "
|
||||
}
|
||||
result += msg
|
||||
return false
|
||||
})
|
||||
if len(seenerrs) == 1 {
|
||||
return result
|
||||
}
|
||||
return "[" + result + "]"
|
||||
}
|
||||
|
||||
func (agg aggregate) Is(target error) bool {
|
||||
return agg.visit(func(err error) bool {
|
||||
return errors.Is(err, target)
|
||||
})
|
||||
}
|
||||
|
||||
func (agg aggregate) visit(f func(err error) bool) bool {
|
||||
for _, err := range agg {
|
||||
switch err := err.(type) {
|
||||
case aggregate:
|
||||
if match := err.visit(f); match {
|
||||
return match
|
||||
}
|
||||
case Aggregate:
|
||||
for _, nestedErr := range err.Errors() {
|
||||
if match := f(nestedErr); match {
|
||||
return match
|
||||
}
|
||||
}
|
||||
default:
|
||||
if match := f(err); match {
|
||||
return match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Errors is part of the Aggregate interface.
|
||||
func (agg aggregate) Errors() []error {
|
||||
return []error(agg)
|
||||
}
|
||||
|
||||
// Matcher is used to match errors. Returns true if the error matches.
|
||||
type Matcher func(error) bool
|
||||
|
||||
// FilterOut removes all errors that match any of the matchers from the input
|
||||
// error. If the input is a singular error, only that error is tested. If the
|
||||
// input implements the Aggregate interface, the list of errors will be
|
||||
// processed recursively.
|
||||
//
|
||||
// This can be used, for example, to remove known-OK errors (such as io.EOF or
|
||||
// os.PathNotFound) from a list of errors.
|
||||
func FilterOut(err error, fns ...Matcher) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if agg, ok := err.(Aggregate); ok {
|
||||
return NewAggregate(filterErrors(agg.Errors(), fns...))
|
||||
}
|
||||
if !matchesError(err, fns...) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchesError returns true if any Matcher returns true
|
||||
func matchesError(err error, fns ...Matcher) bool {
|
||||
for _, fn := range fns {
|
||||
if fn(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filterErrors returns any errors (or nested errors, if the list contains
|
||||
// nested Errors) for which all fns return false. If no errors
|
||||
// remain a nil list is returned. The resulting silec will have all
|
||||
// nested slices flattened as a side effect.
|
||||
func filterErrors(list []error, fns ...Matcher) []error {
|
||||
result := []error{}
|
||||
for _, err := range list {
|
||||
r := FilterOut(err, fns...)
|
||||
if r != nil {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Flatten takes an Aggregate, which may hold other Aggregates in arbitrary
|
||||
// nesting, and flattens them all into a single Aggregate, recursively.
|
||||
func Flatten(agg Aggregate) Aggregate {
|
||||
result := []error{}
|
||||
if agg == nil {
|
||||
return nil
|
||||
}
|
||||
for _, err := range agg.Errors() {
|
||||
if a, ok := err.(Aggregate); ok {
|
||||
r := Flatten(a)
|
||||
if r != nil {
|
||||
result = append(result, r.Errors()...)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
result = append(result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewAggregate(result)
|
||||
}
|
||||
|
||||
// CreateAggregateFromMessageCountMap converts MessageCountMap Aggregate
|
||||
func CreateAggregateFromMessageCountMap(m MessageCountMap) Aggregate {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]error, 0, len(m))
|
||||
for errStr, count := range m {
|
||||
var countStr string
|
||||
if count > 1 {
|
||||
countStr = fmt.Sprintf(" (repeated %v times)", count)
|
||||
}
|
||||
result = append(result, fmt.Errorf("%v%v", errStr, countStr))
|
||||
}
|
||||
return NewAggregate(result)
|
||||
}
|
||||
|
||||
// Reduce will return err or, if err is an Aggregate and only has one item,
|
||||
// the first item in the aggregate.
|
||||
func Reduce(err error) error {
|
||||
if agg, ok := err.(Aggregate); ok && err != nil {
|
||||
switch len(agg.Errors()) {
|
||||
case 1:
|
||||
return agg.Errors()[0]
|
||||
case 0:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AggregateGoroutines runs the provided functions in parallel, stuffing all
|
||||
// non-nil errors into the returned Aggregate.
|
||||
// Returns nil if all the functions complete successfully.
|
||||
func AggregateGoroutines(funcs ...func() error) Aggregate {
|
||||
errChan := make(chan error, len(funcs))
|
||||
for _, f := range funcs {
|
||||
go func(f func() error) { errChan <- f() }(f)
|
||||
}
|
||||
errs := make([]error, 0)
|
||||
for i := 0; i < cap(errChan); i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return NewAggregate(errs)
|
||||
}
|
||||
|
||||
// ErrPreconditionViolated is returned when the precondition is violated
|
||||
var ErrPreconditionViolated = errors.New("precondition is violated")
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/sets/empty.go
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
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 sets
|
||||
|
||||
// Empty is public since it is used by some internal API objects for conversions between external
|
||||
// string arrays and internal sets, and conversion logic requires public types today.
|
||||
type Empty struct{}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/sets/string.go
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
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 sets
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// sets.String is a set of strings, implemented via map[string]struct{} for minimal memory consumption.
|
||||
type String map[string]Empty
|
||||
|
||||
// NewString creates a String from a list of values.
|
||||
func NewString(items ...string) String {
|
||||
ss := String{}
|
||||
ss.Insert(items...)
|
||||
return ss
|
||||
}
|
||||
|
||||
// StringKeySet creates a String from a keys of a map[string](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func StringKeySet(theMap interface{}) String {
|
||||
v := reflect.ValueOf(theMap)
|
||||
ret := String{}
|
||||
|
||||
for _, keyValue := range v.MapKeys() {
|
||||
ret.Insert(keyValue.Interface().(string))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s String) Insert(items ...string) String {
|
||||
for _, item := range items {
|
||||
s[item] = Empty{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s String) Delete(items ...string) String {
|
||||
for _, item := range items {
|
||||
delete(s, item)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s String) Has(item string) bool {
|
||||
_, contained := s[item]
|
||||
return contained
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s String) HasAll(items ...string) bool {
|
||||
for _, item := range items {
|
||||
if !s.Has(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s String) HasAny(items ...string) bool {
|
||||
for _, item := range items {
|
||||
if s.Has(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s String) Difference(s2 String) String {
|
||||
result := NewString()
|
||||
for key := range s {
|
||||
if !s2.Has(key) {
|
||||
result.Insert(key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 String) Union(s2 String) String {
|
||||
result := NewString()
|
||||
for key := range s1 {
|
||||
result.Insert(key)
|
||||
}
|
||||
for key := range s2 {
|
||||
result.Insert(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 String) Intersection(s2 String) String {
|
||||
var walk, other String
|
||||
result := NewString()
|
||||
if s1.Len() < s2.Len() {
|
||||
walk = s1
|
||||
other = s2
|
||||
} else {
|
||||
walk = s2
|
||||
other = s1
|
||||
}
|
||||
for key := range walk {
|
||||
if other.Has(key) {
|
||||
result.Insert(key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 String) IsSuperset(s2 String) bool {
|
||||
for item := range s2 {
|
||||
if !s1.Has(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 String) Equal(s2 String) bool {
|
||||
return len(s1) == len(s2) && s1.IsSuperset(s2)
|
||||
}
|
||||
|
||||
type sortableSliceOfString []string
|
||||
|
||||
func (s sortableSliceOfString) Len() int { return len(s) }
|
||||
func (s sortableSliceOfString) Less(i, j int) bool { return lessString(s[i], s[j]) }
|
||||
func (s sortableSliceOfString) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
// List returns the contents as a sorted string slice.
|
||||
func (s String) List() []string {
|
||||
res := make(sortableSliceOfString, 0, len(s))
|
||||
for key := range s {
|
||||
res = append(res, key)
|
||||
}
|
||||
sort.Sort(res)
|
||||
return []string(res)
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s String) UnsortedList() []string {
|
||||
res := make([]string, 0, len(s))
|
||||
for key := range s {
|
||||
res = append(res, key)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns a single element from the set.
|
||||
func (s String) PopAny() (string, bool) {
|
||||
for key := range s {
|
||||
s.Delete(key)
|
||||
return key, true
|
||||
}
|
||||
var zeroValue string
|
||||
return zeroValue, false
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s String) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func lessString(lhs, rhs string) bool {
|
||||
return lhs < rhs
|
||||
}
|
||||
Generated
Vendored
+275
@@ -0,0 +1,275 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/validation/field/errors.go
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 field
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
utilerrors "sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/sets"
|
||||
)
|
||||
|
||||
// Error is an implementation of the 'error' interface, which represents a
|
||||
// field-level validation error.
|
||||
type Error struct {
|
||||
Type ErrorType
|
||||
Field string
|
||||
BadValue interface{}
|
||||
Detail string
|
||||
}
|
||||
|
||||
var _ error = &Error{}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (v *Error) Error() string {
|
||||
return fmt.Sprintf("%s: %s", v.Field, v.ErrorBody())
|
||||
}
|
||||
|
||||
// ErrorBody returns the error message without the field name. This is useful
|
||||
// for building nice-looking higher-level error reporting.
|
||||
func (v *Error) ErrorBody() string {
|
||||
var s string
|
||||
switch v.Type {
|
||||
case ErrorTypeRequired, ErrorTypeForbidden, ErrorTypeTooLong, ErrorTypeInternal:
|
||||
s = v.Type.String()
|
||||
default:
|
||||
value := v.BadValue
|
||||
valueType := reflect.TypeOf(value)
|
||||
if value == nil || valueType == nil {
|
||||
value = "null"
|
||||
} else if valueType.Kind() == reflect.Ptr {
|
||||
if reflectValue := reflect.ValueOf(value); reflectValue.IsNil() {
|
||||
value = "null"
|
||||
} else {
|
||||
value = reflectValue.Elem().Interface()
|
||||
}
|
||||
}
|
||||
switch t := value.(type) {
|
||||
case int64, int32, float64, float32, bool:
|
||||
// use simple printer for simple types
|
||||
s = fmt.Sprintf("%s: %v", v.Type, value)
|
||||
case string:
|
||||
s = fmt.Sprintf("%s: %q", v.Type, t)
|
||||
case fmt.Stringer:
|
||||
// anything that defines String() is better than raw struct
|
||||
s = fmt.Sprintf("%s: %s", v.Type, t.String())
|
||||
default:
|
||||
// fallback to raw struct
|
||||
// TODO: internal types have panic guards against json.Marshalling to prevent
|
||||
// accidental use of internal types in external serialized form. For now, use
|
||||
// %#v, although it would be better to show a more expressive output in the future
|
||||
s = fmt.Sprintf("%s: %#v", v.Type, value)
|
||||
}
|
||||
}
|
||||
if len(v.Detail) != 0 {
|
||||
s += fmt.Sprintf(": %s", v.Detail)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ErrorType is a machine readable value providing more detail about why
|
||||
// a field is invalid. These values are expected to match 1-1 with
|
||||
// CauseType in api/types.go.
|
||||
type ErrorType string
|
||||
|
||||
// TODO: These values are duplicated in api/types.go, but there's a circular dep. Fix it.
|
||||
const (
|
||||
// ErrorTypeNotFound is used to report failure to find a requested value
|
||||
// (e.g. looking up an ID). See NotFound().
|
||||
ErrorTypeNotFound ErrorType = "FieldValueNotFound"
|
||||
// ErrorTypeRequired is used to report required values that are not
|
||||
// provided (e.g. empty strings, null values, or empty arrays). See
|
||||
// Required().
|
||||
ErrorTypeRequired ErrorType = "FieldValueRequired"
|
||||
// ErrorTypeDuplicate is used to report collisions of values that must be
|
||||
// unique (e.g. unique IDs). See Duplicate().
|
||||
ErrorTypeDuplicate ErrorType = "FieldValueDuplicate"
|
||||
// ErrorTypeInvalid is used to report malformed values (e.g. failed regex
|
||||
// match, too long, out of bounds). See Invalid().
|
||||
ErrorTypeInvalid ErrorType = "FieldValueInvalid"
|
||||
// ErrorTypeNotSupported is used to report unknown values for enumerated
|
||||
// fields (e.g. a list of valid values). See NotSupported().
|
||||
ErrorTypeNotSupported ErrorType = "FieldValueNotSupported"
|
||||
// ErrorTypeForbidden is used to report valid (as per formatting rules)
|
||||
// values which would be accepted under some conditions, but which are not
|
||||
// permitted by the current conditions (such as security policy). See
|
||||
// Forbidden().
|
||||
ErrorTypeForbidden ErrorType = "FieldValueForbidden"
|
||||
// ErrorTypeTooLong is used to report that the given value is too long.
|
||||
// This is similar to ErrorTypeInvalid, but the error will not include the
|
||||
// too-long value. See TooLong().
|
||||
ErrorTypeTooLong ErrorType = "FieldValueTooLong"
|
||||
// ErrorTypeTooMany is used to report "too many". This is used to
|
||||
// report that a given list has too many items. This is similar to FieldValueTooLong,
|
||||
// but the error indicates quantity instead of length.
|
||||
ErrorTypeTooMany ErrorType = "FieldValueTooMany"
|
||||
// ErrorTypeInternal is used to report other errors that are not related
|
||||
// to user input. See InternalError().
|
||||
ErrorTypeInternal ErrorType = "InternalError"
|
||||
)
|
||||
|
||||
// String converts a ErrorType into its corresponding canonical error message.
|
||||
func (t ErrorType) String() string {
|
||||
switch t {
|
||||
case ErrorTypeNotFound:
|
||||
return "Not found"
|
||||
case ErrorTypeRequired:
|
||||
return "Required value"
|
||||
case ErrorTypeDuplicate:
|
||||
return "Duplicate value"
|
||||
case ErrorTypeInvalid:
|
||||
return "Invalid value"
|
||||
case ErrorTypeNotSupported:
|
||||
return "Unsupported value"
|
||||
case ErrorTypeForbidden:
|
||||
return "Forbidden"
|
||||
case ErrorTypeTooLong:
|
||||
return "Too long"
|
||||
case ErrorTypeTooMany:
|
||||
return "Too many"
|
||||
case ErrorTypeInternal:
|
||||
return "Internal error"
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized validation error: %q", string(t)))
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound returns a *Error indicating "value not found". This is
|
||||
// used to report failure to find a requested value (e.g. looking up an ID).
|
||||
func NotFound(field *Path, value interface{}) *Error {
|
||||
return &Error{ErrorTypeNotFound, field.String(), value, ""}
|
||||
}
|
||||
|
||||
// Required returns a *Error indicating "value required". This is used
|
||||
// to report required values that are not provided (e.g. empty strings, null
|
||||
// values, or empty arrays).
|
||||
func Required(field *Path, detail string) *Error {
|
||||
return &Error{ErrorTypeRequired, field.String(), "", detail}
|
||||
}
|
||||
|
||||
// Duplicate returns a *Error indicating "duplicate value". This is
|
||||
// used to report collisions of values that must be unique (e.g. names or IDs).
|
||||
func Duplicate(field *Path, value interface{}) *Error {
|
||||
return &Error{ErrorTypeDuplicate, field.String(), value, ""}
|
||||
}
|
||||
|
||||
// Invalid returns a *Error indicating "invalid value". This is used
|
||||
// to report malformed values (e.g. failed regex match, too long, out of bounds).
|
||||
func Invalid(field *Path, value interface{}, detail string) *Error {
|
||||
return &Error{ErrorTypeInvalid, field.String(), value, detail}
|
||||
}
|
||||
|
||||
// NotSupported returns a *Error indicating "unsupported value".
|
||||
// This is used to report unknown values for enumerated fields (e.g. a list of
|
||||
// valid values).
|
||||
func NotSupported(field *Path, value interface{}, validValues []string) *Error {
|
||||
detail := ""
|
||||
if validValues != nil && len(validValues) > 0 {
|
||||
quotedValues := make([]string, len(validValues))
|
||||
for i, v := range validValues {
|
||||
quotedValues[i] = strconv.Quote(v)
|
||||
}
|
||||
detail = "supported values: " + strings.Join(quotedValues, ", ")
|
||||
}
|
||||
return &Error{ErrorTypeNotSupported, field.String(), value, detail}
|
||||
}
|
||||
|
||||
// Forbidden returns a *Error indicating "forbidden". This is used to
|
||||
// report valid (as per formatting rules) values which would be accepted under
|
||||
// some conditions, but which are not permitted by current conditions (e.g.
|
||||
// security policy).
|
||||
func Forbidden(field *Path, detail string) *Error {
|
||||
return &Error{ErrorTypeForbidden, field.String(), "", detail}
|
||||
}
|
||||
|
||||
// TooLong returns a *Error indicating "too long". This is used to
|
||||
// report that the given value is too long. This is similar to
|
||||
// Invalid, but the returned error will not include the too-long
|
||||
// value.
|
||||
func TooLong(field *Path, value interface{}, maxLength int) *Error {
|
||||
return &Error{ErrorTypeTooLong, field.String(), value, fmt.Sprintf("must have at most %d bytes", maxLength)}
|
||||
}
|
||||
|
||||
// TooMany returns a *Error indicating "too many". This is used to
|
||||
// report that a given list has too many items. This is similar to TooLong,
|
||||
// but the returned error indicates quantity instead of length.
|
||||
func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
|
||||
return &Error{ErrorTypeTooMany, field.String(), actualQuantity, fmt.Sprintf("must have at most %d items", maxQuantity)}
|
||||
}
|
||||
|
||||
// InternalError returns a *Error indicating "internal error". This is used
|
||||
// to signal that an error was found that was not directly related to user
|
||||
// input. The err argument must be non-nil.
|
||||
func InternalError(field *Path, err error) *Error {
|
||||
return &Error{ErrorTypeInternal, field.String(), nil, err.Error()}
|
||||
}
|
||||
|
||||
// ErrorList holds a set of Errors. It is plausible that we might one day have
|
||||
// non-field errors in this same umbrella package, but for now we don't, so
|
||||
// we can keep it simple and leave ErrorList here.
|
||||
type ErrorList []*Error
|
||||
|
||||
// NewErrorTypeMatcher returns an errors.Matcher that returns true
|
||||
// if the provided error is a Error and has the provided ErrorType.
|
||||
func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher {
|
||||
return func(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Type == t
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ToAggregate converts the ErrorList into an errors.Aggregate.
|
||||
func (list ErrorList) ToAggregate() utilerrors.Aggregate {
|
||||
errs := make([]error, 0, len(list))
|
||||
errorMsgs := sets.NewString()
|
||||
for _, err := range list {
|
||||
msg := fmt.Sprintf("%v", err)
|
||||
if errorMsgs.Has(msg) {
|
||||
continue
|
||||
}
|
||||
errorMsgs.Insert(msg)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func fromAggregate(agg utilerrors.Aggregate) ErrorList {
|
||||
errs := agg.Errors()
|
||||
list := make(ErrorList, len(errs))
|
||||
for i := range errs {
|
||||
list[i] = errs[i].(*Error)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Filter removes items from the ErrorList that match the provided fns.
|
||||
func (list ErrorList) Filter(fns ...utilerrors.Matcher) ErrorList {
|
||||
err := utilerrors.FilterOut(list.ToAggregate(), fns...)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// FilterOut takes an Aggregate and returns an Aggregate
|
||||
return fromAggregate(err.(utilerrors.Aggregate))
|
||||
}
|
||||
Generated
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/validation/field/path.go
|
||||
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
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 field
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Path represents the path from some root to a particular field.
|
||||
type Path struct {
|
||||
name string // the name of this field or "" if this is an index
|
||||
index string // if name == "", this is a subscript (index or map key) of the previous element
|
||||
parent *Path // nil if this is the root element
|
||||
}
|
||||
|
||||
// NewPath creates a root Path object.
|
||||
func NewPath(name string, moreNames ...string) *Path {
|
||||
r := &Path{name: name, parent: nil}
|
||||
for _, anotherName := range moreNames {
|
||||
r = &Path{name: anotherName, parent: r}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Root returns the root element of this Path.
|
||||
func (p *Path) Root() *Path {
|
||||
for ; p.parent != nil; p = p.parent {
|
||||
// Do nothing.
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Child creates a new Path that is a child of the method receiver.
|
||||
func (p *Path) Child(name string, moreNames ...string) *Path {
|
||||
r := NewPath(name, moreNames...)
|
||||
r.Root().parent = p
|
||||
return r
|
||||
}
|
||||
|
||||
// Index indicates that the previous Path is to be subscripted by an int.
|
||||
// This sets the same underlying value as Key.
|
||||
func (p *Path) Index(index int) *Path {
|
||||
return &Path{index: strconv.Itoa(index), parent: p}
|
||||
}
|
||||
|
||||
// Key indicates that the previous Path is to be subscripted by a string.
|
||||
// This sets the same underlying value as Index.
|
||||
func (p *Path) Key(key string) *Path {
|
||||
return &Path{index: key, parent: p}
|
||||
}
|
||||
|
||||
// String produces a string representation of the Path.
|
||||
func (p *Path) String() string {
|
||||
// make a slice to iterate
|
||||
elems := []*Path{}
|
||||
for ; p != nil; p = p.parent {
|
||||
elems = append(elems, p)
|
||||
}
|
||||
|
||||
// iterate, but it has to be backwards
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for i := range elems {
|
||||
p := elems[len(elems)-1-i]
|
||||
if p.parent != nil && len(p.name) > 0 {
|
||||
// This is either the root or it is a subscript.
|
||||
buf.WriteString(".")
|
||||
}
|
||||
if len(p.name) > 0 {
|
||||
buf.WriteString(p.name)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "[%s]", p.index)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
Generated
Vendored
+506
@@ -0,0 +1,506 @@
|
||||
// Code generated by k8scopy from k8s.io/apimachinery@v0.19.8; DO NOT EDIT.
|
||||
// File content copied from k8s.io/apimachinery@v0.19.8/pkg/util/validation/validation.go
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/internal/k8sgen/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
const qnameCharFmt string = "[A-Za-z0-9]"
|
||||
const qnameExtCharFmt string = "[-A-Za-z0-9_.]"
|
||||
const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt
|
||||
const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
|
||||
const qualifiedNameMaxLength int = 63
|
||||
|
||||
var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$")
|
||||
|
||||
// IsQualifiedName tests whether the value passed is what Kubernetes calls a
|
||||
// "qualified name". This is a format used in various places throughout the
|
||||
// system. If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
func IsQualifiedName(value string) []string {
|
||||
var errs []string
|
||||
parts := strings.Split(value, "/")
|
||||
var name string
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
name = parts[0]
|
||||
case 2:
|
||||
var prefix string
|
||||
prefix, name = parts[0], parts[1]
|
||||
if len(prefix) == 0 {
|
||||
errs = append(errs, "prefix part "+EmptyError())
|
||||
} else if msgs := IsDNS1123Subdomain(prefix); len(msgs) != 0 {
|
||||
errs = append(errs, prefixEach(msgs, "prefix part ")...)
|
||||
}
|
||||
default:
|
||||
return append(errs, "a qualified name "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+
|
||||
" with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')")
|
||||
}
|
||||
|
||||
if len(name) == 0 {
|
||||
errs = append(errs, "name part "+EmptyError())
|
||||
} else if len(name) > qualifiedNameMaxLength {
|
||||
errs = append(errs, "name part "+MaxLenError(qualifiedNameMaxLength))
|
||||
}
|
||||
if !qualifiedNameRegexp.MatchString(name) {
|
||||
errs = append(errs, "name part "+RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsFullyQualifiedName checks if the name is fully qualified. This is similar
|
||||
// to IsFullyQualifiedDomainName but requires a minimum of 3 segments instead of
|
||||
// 2 and does not accept a trailing . as valid.
|
||||
// TODO: This function is deprecated and preserved until all callers migrate to
|
||||
// IsFullyQualifiedDomainName; please don't add new callers.
|
||||
func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(name) == 0 {
|
||||
return append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
|
||||
}
|
||||
if len(strings.Split(name, ".")) < 3 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least three segments separated by dots"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// IsFullyQualifiedDomainName checks if the domain name is fully qualified. This
|
||||
// is similar to IsFullyQualifiedName but only requires a minimum of 2 segments
|
||||
// instead of 3 and accepts a trailing . as valid.
|
||||
func IsFullyQualifiedDomainName(fldPath *field.Path, name string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(name) == 0 {
|
||||
return append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
if strings.HasSuffix(name, ".") {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
|
||||
}
|
||||
if len(strings.Split(name, ".")) < 2 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least two segments separated by dots"))
|
||||
}
|
||||
for _, label := range strings.Split(name, ".") {
|
||||
if errs := IsDNS1123Label(label); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, label, strings.Join(errs, ",")))
|
||||
}
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// Allowed characters in an HTTP Path as defined by RFC 3986. A HTTP path may
|
||||
// contain:
|
||||
// * unreserved characters (alphanumeric, '-', '.', '_', '~')
|
||||
// * percent-encoded octets
|
||||
// * sub-delims ("!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=")
|
||||
// * a colon character (":")
|
||||
const httpPathFmt string = `[A-Za-z0-9/\-._~%!$&'()*+,;=:]+`
|
||||
|
||||
var httpPathRegexp = regexp.MustCompile("^" + httpPathFmt + "$")
|
||||
|
||||
// IsDomainPrefixedPath checks if the given string is a domain-prefixed path
|
||||
// (e.g. acme.io/foo). All characters before the first "/" must be a valid
|
||||
// subdomain as defined by RFC 1123. All characters trailing the first "/" must
|
||||
// be valid HTTP Path characters as defined by RFC 3986.
|
||||
func IsDomainPrefixedPath(fldPath *field.Path, dpPath string) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if len(dpPath) == 0 {
|
||||
return append(allErrs, field.Required(fldPath, ""))
|
||||
}
|
||||
|
||||
segments := strings.SplitN(dpPath, "/", 2)
|
||||
if len(segments) != 2 || len(segments[0]) == 0 || len(segments[1]) == 0 {
|
||||
return append(allErrs, field.Invalid(fldPath, dpPath, "must be a domain-prefixed path (such as \"acme.io/foo\")"))
|
||||
}
|
||||
|
||||
host := segments[0]
|
||||
for _, err := range IsDNS1123Subdomain(host) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, host, err))
|
||||
}
|
||||
|
||||
path := segments[1]
|
||||
if !httpPathRegexp.MatchString(path) {
|
||||
return append(allErrs, field.Invalid(fldPath, path, RegexError("Invalid path", httpPathFmt)))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
const labelValueFmt string = "(" + qualifiedNameFmt + ")?"
|
||||
const labelValueErrMsg string = "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
// LabelValueMaxLength is a label's max length
|
||||
const LabelValueMaxLength int = 63
|
||||
|
||||
var labelValueRegexp = regexp.MustCompile("^" + labelValueFmt + "$")
|
||||
|
||||
// IsValidLabelValue tests whether the value passed is a valid label value. If
|
||||
// the value is not valid, a list of error strings is returned. Otherwise an
|
||||
// empty list (or nil) is returned.
|
||||
func IsValidLabelValue(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > LabelValueMaxLength {
|
||||
errs = append(errs, MaxLenError(LabelValueMaxLength))
|
||||
}
|
||||
if !labelValueRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
|
||||
const dns1123LabelErrMsg string = "a DNS-1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character"
|
||||
|
||||
// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123)
|
||||
const DNS1123LabelMaxLength int = 63
|
||||
|
||||
var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")
|
||||
|
||||
// IsDNS1123Label tests for a string that conforms to the definition of a label in
|
||||
// DNS (RFC 1123).
|
||||
func IsDNS1123Label(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123LabelMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123LabelMaxLength))
|
||||
}
|
||||
if !dns1123LabelRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
|
||||
const dns1123SubdomainErrorMsg string = "a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
|
||||
const DNS1123SubdomainMaxLength int = 253
|
||||
|
||||
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
|
||||
|
||||
// IsDNS1123Subdomain tests for a string that conforms to the definition of a
|
||||
// subdomain in DNS (RFC 1123).
|
||||
func IsDNS1123Subdomain(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !dns1123SubdomainRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
|
||||
const dns1035LabelErrMsg string = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"
|
||||
|
||||
// DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035)
|
||||
const DNS1035LabelMaxLength int = 63
|
||||
|
||||
var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$")
|
||||
|
||||
// IsDNS1035Label tests for a string that conforms to the definition of a label in
|
||||
// DNS (RFC 1035).
|
||||
func IsDNS1035Label(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1035LabelMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1035LabelMaxLength))
|
||||
}
|
||||
if !dns1035LabelRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// wildcard definition - RFC 1034 section 4.3.3.
|
||||
// examples:
|
||||
// - valid: *.bar.com, *.foo.bar.com
|
||||
// - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, *
|
||||
const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt
|
||||
const wildcardDNS1123SubdomainErrMsg = "a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character"
|
||||
|
||||
// IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a
|
||||
// wildcard subdomain in DNS (RFC 1034 section 4.3.3).
|
||||
func IsWildcardDNS1123Subdomain(value string) []string {
|
||||
wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$")
|
||||
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !wildcardDNS1123SubdomainRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*"
|
||||
const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'"
|
||||
|
||||
var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$")
|
||||
|
||||
// IsCIdentifier tests for a string that conforms the definition of an identifier
|
||||
// in C. This checks the format, but not the length.
|
||||
func IsCIdentifier(value string) []string {
|
||||
if !cIdentifierRegexp.MatchString(value) {
|
||||
return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidPortNum tests that the argument is a valid, non-zero port number.
|
||||
func IsValidPortNum(port int) []string {
|
||||
if 1 <= port && port <= 65535 {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(1, 65535)}
|
||||
}
|
||||
|
||||
// IsInRange tests that the argument is in an inclusive range.
|
||||
func IsInRange(value int, min int, max int) []string {
|
||||
if value >= min && value <= max {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(min, max)}
|
||||
}
|
||||
|
||||
// Now in libcontainer UID/GID limits is 0 ~ 1<<31 - 1
|
||||
// TODO: once we have a type for UID/GID we should make these that type.
|
||||
const (
|
||||
minUserID = 0
|
||||
maxUserID = math.MaxInt32
|
||||
minGroupID = 0
|
||||
maxGroupID = math.MaxInt32
|
||||
)
|
||||
|
||||
// IsValidGroupID tests that the argument is a valid Unix GID.
|
||||
func IsValidGroupID(gid int64) []string {
|
||||
if minGroupID <= gid && gid <= maxGroupID {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(minGroupID, maxGroupID)}
|
||||
}
|
||||
|
||||
// IsValidUserID tests that the argument is a valid Unix UID.
|
||||
func IsValidUserID(uid int64) []string {
|
||||
if minUserID <= uid && uid <= maxUserID {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(minUserID, maxUserID)}
|
||||
}
|
||||
|
||||
var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$")
|
||||
var portNameOneLetterRegexp = regexp.MustCompile("[a-z]")
|
||||
|
||||
// IsValidPortName check that the argument is valid syntax. It must be
|
||||
// non-empty and no more than 15 characters long. It may contain only [-a-z0-9]
|
||||
// and must contain at least one letter [a-z]. It must not start or end with a
|
||||
// hyphen, nor contain adjacent hyphens.
|
||||
//
|
||||
// Note: We only allow lower-case characters, even though RFC 6335 is case
|
||||
// insensitive.
|
||||
func IsValidPortName(port string) []string {
|
||||
var errs []string
|
||||
if len(port) > 15 {
|
||||
errs = append(errs, MaxLenError(15))
|
||||
}
|
||||
if !portNameCharsetRegex.MatchString(port) {
|
||||
errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)")
|
||||
}
|
||||
if !portNameOneLetterRegexp.MatchString(port) {
|
||||
errs = append(errs, "must contain at least one letter or number (a-z, 0-9)")
|
||||
}
|
||||
if strings.Contains(port, "--") {
|
||||
errs = append(errs, "must not contain consecutive hyphens")
|
||||
}
|
||||
if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') {
|
||||
errs = append(errs, "must not begin or end with a hyphen")
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsValidIP tests that the argument is a valid IP address.
|
||||
func IsValidIP(value string) []string {
|
||||
if net.ParseIP(value) == nil {
|
||||
return []string{"must be a valid IP address, (e.g. 10.9.8.7)"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidIPv4Address tests that the argument is a valid IPv4 address.
|
||||
func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
ip := net.ParseIP(value)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// IsValidIPv6Address tests that the argument is a valid IPv6 address.
|
||||
func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
ip := net.ParseIP(value)
|
||||
if ip == nil || ip.To4() != nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
const percentFmt string = "[0-9]+%"
|
||||
const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"
|
||||
|
||||
var percentRegexp = regexp.MustCompile("^" + percentFmt + "$")
|
||||
|
||||
// IsValidPercent checks that string is in the form of a percentage
|
||||
func IsValidPercent(percent string) []string {
|
||||
if !percentRegexp.MatchString(percent) {
|
||||
return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const httpHeaderNameFmt string = "[-A-Za-z0-9]+"
|
||||
const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'"
|
||||
|
||||
var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$")
|
||||
|
||||
// IsHTTPHeaderName checks that a string conforms to the Go HTTP library's
|
||||
// definition of a valid header field name (a stricter subset than RFC7230).
|
||||
func IsHTTPHeaderName(value string) []string {
|
||||
if !httpHeaderNameRegexp.MatchString(value) {
|
||||
return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*"
|
||||
const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit"
|
||||
|
||||
var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$")
|
||||
|
||||
// IsEnvVarName tests if a string is a valid environment variable name.
|
||||
func IsEnvVarName(value string) []string {
|
||||
var errs []string
|
||||
if !envVarNameRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1"))
|
||||
}
|
||||
|
||||
errs = append(errs, hasChDirPrefix(value)...)
|
||||
return errs
|
||||
}
|
||||
|
||||
const configMapKeyFmt = `[-._a-zA-Z0-9]+`
|
||||
const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'"
|
||||
|
||||
var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$")
|
||||
|
||||
// IsConfigMapKey tests for a string that is a valid key for a ConfigMap or Secret
|
||||
func IsConfigMapKey(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !configMapKeyRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name"))
|
||||
}
|
||||
errs = append(errs, hasChDirPrefix(value)...)
|
||||
return errs
|
||||
}
|
||||
|
||||
// MaxLenError returns a string explanation of a "string too long" validation
|
||||
// failure.
|
||||
func MaxLenError(length int) string {
|
||||
return fmt.Sprintf("must be no more than %d characters", length)
|
||||
}
|
||||
|
||||
// RegexError returns a string explanation of a regex validation failure.
|
||||
func RegexError(msg string, fmt string, examples ...string) string {
|
||||
if len(examples) == 0 {
|
||||
return msg + " (regex used for validation is '" + fmt + "')"
|
||||
}
|
||||
msg += " (e.g. "
|
||||
for i := range examples {
|
||||
if i > 0 {
|
||||
msg += " or "
|
||||
}
|
||||
msg += "'" + examples[i] + "', "
|
||||
}
|
||||
msg += "regex used for validation is '" + fmt + "')"
|
||||
return msg
|
||||
}
|
||||
|
||||
// EmptyError returns a string explanation of a "must not be empty" validation
|
||||
// failure.
|
||||
func EmptyError() string {
|
||||
return "must be non-empty"
|
||||
}
|
||||
|
||||
func prefixEach(msgs []string, prefix string) []string {
|
||||
for i := range msgs {
|
||||
msgs[i] = prefix + msgs[i]
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// InclusiveRangeError returns a string explanation of a numeric "must be
|
||||
// between" validation failure.
|
||||
func InclusiveRangeError(lo, hi int) string {
|
||||
return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi)
|
||||
}
|
||||
|
||||
func hasChDirPrefix(value string) []string {
|
||||
var errs []string
|
||||
switch {
|
||||
case value == ".":
|
||||
errs = append(errs, `must not be '.'`)
|
||||
case value == "..":
|
||||
errs = append(errs, `must not be '..'`)
|
||||
case strings.HasPrefix(value, ".."):
|
||||
errs = append(errs, `must not start with '..'`)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsValidSocketAddr checks that string represents a valid socket address
|
||||
// as defined in RFC 789. (e.g 0.0.0.0:10254 or [::]:10254))
|
||||
func IsValidSocketAddr(value string) []string {
|
||||
var errs []string
|
||||
ip, port, err := net.SplitHostPort(value)
|
||||
if err != nil {
|
||||
errs = append(errs, "must be a valid socket address format, (e.g. 0.0.0.0:10254 or [::]:10254)")
|
||||
return errs
|
||||
}
|
||||
portInt, _ := strconv.Atoi(port)
|
||||
errs = append(errs, IsValidPortNum(portInt)...)
|
||||
errs = append(errs, IsValidIP(ip)...)
|
||||
return errs
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
// AnnotationClearer removes an annotation at metadata.annotations.
|
||||
// Returns nil if the annotation or field does not exist.
|
||||
type AnnotationClearer struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
}
|
||||
|
||||
func (c AnnotationClearer) Filter(rn *RNode) (*RNode, error) {
|
||||
return rn.Pipe(
|
||||
PathGetter{Path: []string{MetadataField, AnnotationsField}},
|
||||
FieldClearer{Name: c.Key})
|
||||
}
|
||||
|
||||
func ClearAnnotation(key string) AnnotationClearer {
|
||||
return AnnotationClearer{Key: key}
|
||||
}
|
||||
|
||||
// ClearEmptyAnnotations clears the keys, annotations
|
||||
// and metadata if they are empty/null
|
||||
func ClearEmptyAnnotations(rn *RNode) error {
|
||||
_, err := rn.Pipe(Lookup(MetadataField), FieldClearer{
|
||||
Name: AnnotationsField, IfEmpty: true})
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
_, err = rn.Pipe(FieldClearer{Name: MetadataField, IfEmpty: true})
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// k8sMetaSetter sets a name at metadata.{key}.
|
||||
// Creates metadata if does not exist.
|
||||
type k8sMetaSetter struct {
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (s k8sMetaSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
_, err := rn.Pipe(
|
||||
PathGetter{Path: []string{MetadataField}, Create: yaml.MappingNode},
|
||||
FieldSetter{Name: s.Key, Value: NewStringRNode(s.Value)})
|
||||
return rn, err
|
||||
}
|
||||
|
||||
func SetK8sName(value string) k8sMetaSetter {
|
||||
return k8sMetaSetter{Key: NameField, Value: value}
|
||||
}
|
||||
|
||||
func SetK8sNamespace(value string) k8sMetaSetter {
|
||||
return k8sMetaSetter{Key: NamespaceField, Value: value}
|
||||
}
|
||||
|
||||
// AnnotationSetter sets an annotation at metadata.annotations.
|
||||
// Creates metadata.annotations if does not exist.
|
||||
type AnnotationSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (s AnnotationSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
v := NewStringRNode(s.Value)
|
||||
// some tools get confused about the type if annotations are not quoted
|
||||
v.YNode().Style = yaml.SingleQuotedStyle
|
||||
if err := ClearEmptyAnnotations(rn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return addMetadataNode(rn, AnnotationsField, s.Key, v)
|
||||
}
|
||||
|
||||
func SetAnnotation(key, value string) AnnotationSetter {
|
||||
return AnnotationSetter{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// AnnotationGetter gets an annotation at metadata.annotations.
|
||||
// Returns nil if metadata.annotations does not exist.
|
||||
type AnnotationGetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
// AnnotationGetter returns the annotation value.
|
||||
// Returns "", nil if the annotation does not exist.
|
||||
func (g AnnotationGetter) Filter(rn *RNode) (*RNode, error) {
|
||||
v, err := rn.Pipe(
|
||||
PathGetter{Path: []string{MetadataField, AnnotationsField, g.Key}})
|
||||
if v == nil || err != nil {
|
||||
return v, err
|
||||
}
|
||||
if g.Value == "" || v.value.Value == g.Value {
|
||||
return v, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func GetAnnotation(key string) AnnotationGetter {
|
||||
return AnnotationGetter{Key: key}
|
||||
}
|
||||
|
||||
// LabelSetter sets a label at metadata.labels.
|
||||
// Creates metadata.labels if does not exist.
|
||||
type LabelSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (s LabelSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
v := NewStringRNode(s.Value)
|
||||
// some tools get confused about the type if labels are not quoted
|
||||
v.YNode().Style = yaml.SingleQuotedStyle
|
||||
return addMetadataNode(rn, LabelsField, s.Key, v)
|
||||
}
|
||||
|
||||
func addMetadataNode(rn *RNode, field, key string, v *RNode) (*RNode, error) {
|
||||
return rn.Pipe(
|
||||
PathGetter{
|
||||
Path: []string{MetadataField, field}, Create: yaml.MappingNode},
|
||||
FieldSetter{Name: key, Value: v})
|
||||
}
|
||||
|
||||
func SetLabel(key, value string) LabelSetter {
|
||||
return LabelSetter{Key: key, Value: value}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
// MapNode wraps a field key and value.
|
||||
type MapNode struct {
|
||||
Key *RNode
|
||||
Value *RNode
|
||||
}
|
||||
|
||||
// IsNilOrEmpty returns true if the MapNode is nil,
|
||||
// has no value, or has a value that appears empty.
|
||||
func (mn *MapNode) IsNilOrEmpty() bool {
|
||||
return mn == nil || mn.Value.IsNilOrEmpty()
|
||||
}
|
||||
|
||||
type MapNodeSlice []*MapNode
|
||||
|
||||
func (m MapNodeSlice) Keys() []*RNode {
|
||||
var keys []*RNode
|
||||
for i := range m {
|
||||
if m[i] != nil {
|
||||
keys = append(keys, m[i].Key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (m MapNodeSlice) Values() []*RNode {
|
||||
var values []*RNode
|
||||
for i := range m {
|
||||
if m[i] != nil {
|
||||
values = append(values, m[i].Value)
|
||||
} else {
|
||||
values = append(values, nil)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
)
|
||||
|
||||
// PathMatcher returns all RNodes matching the path wrapped in a SequenceNode.
|
||||
// Lists may have multiple elements matching the pafunc cleanPath(path []string) []string {g element
|
||||
// is added to the return result.
|
||||
// If Path points to a SequenceNode, the SequenceNode is wrapped in another SequenceNode
|
||||
// If Path does not contain any lists, the result is still wrapped in a SequenceNode of len == 1
|
||||
type PathMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Path is a slice of parts leading to the RNode to lookup.
|
||||
// Each path part may be one of:
|
||||
// * FieldMatcher -- e.g. "spec"
|
||||
// * Map Key -- e.g. "app.k8s.io/version"
|
||||
// * List Entry -- e.g. "[name=nginx]" or "[=-jar]" or "0"
|
||||
//
|
||||
// Map Keys and Fields are equivalent.
|
||||
// See FieldMatcher for more on Fields and Map Keys.
|
||||
//
|
||||
// List Entries are specified as map entry to match [fieldName=fieldValue].
|
||||
// See Elem for more on List Entries.
|
||||
//
|
||||
// Examples:
|
||||
// * spec.template.spec.container with matching name: [name=nginx] -- match 'name': 'nginx'
|
||||
// * spec.template.spec.container.argument matching a value: [=-jar] -- match '-jar'
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
|
||||
// Matches is set by PathMatch to publish the matched element values for each node.
|
||||
// After running PathMatcher.Filter, each node from the SequenceNode result may be
|
||||
// looked up in Matches to find the field values that were matched.
|
||||
Matches map[*Node][]string
|
||||
|
||||
// StripComments may be set to remove the comments on the matching Nodes.
|
||||
// This is useful for if the nodes are to be printed in FlowStyle.
|
||||
StripComments bool
|
||||
|
||||
// Create will cause missing path parts to be created as they are walked.
|
||||
//
|
||||
// * The leaf Node (final path) will be created with a Kind matching Create
|
||||
// * Intermediary Nodes will be created as either a MappingNodes or
|
||||
// SequenceNodes as appropriate for each's Path location.
|
||||
// * Nodes identified by an index will only be created if the index indicates
|
||||
// an append operation (i.e. index=len(list))
|
||||
Create yaml.Kind `yaml:"create,omitempty"`
|
||||
|
||||
val *RNode
|
||||
field string
|
||||
matchRegex string
|
||||
}
|
||||
|
||||
func (p *PathMatcher) stripComments(n *Node) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if p.StripComments {
|
||||
n.LineComment = ""
|
||||
n.HeadComment = ""
|
||||
n.FootComment = ""
|
||||
for i := range n.Content {
|
||||
p.stripComments(n.Content[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PathMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
val, err := p.filter(rn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.stripComments(val.YNode())
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (p *PathMatcher) filter(rn *RNode) (*RNode, error) {
|
||||
p.Matches = map[*Node][]string{}
|
||||
|
||||
if len(p.Path) == 0 {
|
||||
// return the element wrapped in a SequenceNode
|
||||
p.appendRNode("", rn)
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
if IsIdxNumber(p.Path[0]) {
|
||||
return p.doIndexSeq(rn)
|
||||
}
|
||||
|
||||
if IsListIndex(p.Path[0]) {
|
||||
// match seq elements
|
||||
return p.doSeq(rn)
|
||||
}
|
||||
|
||||
if IsWildcard(p.Path[0]) {
|
||||
// match every elements (*)
|
||||
return p.doMatchEvery(rn)
|
||||
}
|
||||
// match a field
|
||||
return p.doField(rn)
|
||||
}
|
||||
|
||||
func (p *PathMatcher) doMatchEvery(rn *RNode) (*RNode, error) {
|
||||
if err := rn.VisitElements(p.visitEveryElem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) visitEveryElem(elem *RNode) error {
|
||||
fieldName := p.Path[0]
|
||||
// recurse on the matching element
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
add, err := pm.filter(elem)
|
||||
for k, v := range pm.Matches {
|
||||
p.Matches[k] = v
|
||||
}
|
||||
if err != nil || add == nil {
|
||||
return err
|
||||
}
|
||||
p.append(fieldName, add.Content()...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) doField(rn *RNode) (*RNode, error) {
|
||||
// lookup the field
|
||||
field, err := rn.Pipe(Get(p.Path[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !IsCreate(p.Create) && field == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if IsCreate(p.Create) && field == nil {
|
||||
var nextPart string
|
||||
if len(p.Path) > 1 {
|
||||
nextPart = p.Path[1]
|
||||
}
|
||||
nextPartKind := getPathPartKind(nextPart, p.Create)
|
||||
field = &RNode{value: &yaml.Node{Kind: nextPartKind}}
|
||||
err := rn.PipeE(SetField(p.Path[0], field))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the field is a scalar and there are remaining path segments
|
||||
if field != nil && field.YNode().Kind == yaml.ScalarNode && len(p.Path) > 1 {
|
||||
return p.handleStructuredDataInScalar(field)
|
||||
}
|
||||
|
||||
// recurse on the field, removing the first element of the path
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
p.val, err = pm.filter(field)
|
||||
p.Matches = pm.Matches
|
||||
return p.val, err
|
||||
}
|
||||
|
||||
// doIndexSeq iterates over a sequence and appends elements matching the index p.Val
|
||||
func (p *PathMatcher) doIndexSeq(rn *RNode) (*RNode, error) {
|
||||
// parse to index number
|
||||
idx, err := strconv.Atoi(p.Path[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
elements, err := rn.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(elements) == idx && IsCreate(p.Create) {
|
||||
var nextPart string
|
||||
if len(p.Path) > 1 {
|
||||
nextPart = p.Path[1]
|
||||
}
|
||||
elem := &yaml.Node{Kind: getPathPartKind(nextPart, p.Create)}
|
||||
err = rn.PipeE(Append(elem))
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "failed to append element for %q", p.Path[0])
|
||||
}
|
||||
elements = append(elements, NewRNode(elem))
|
||||
}
|
||||
|
||||
if len(elements) < idx+1 {
|
||||
return nil, fmt.Errorf("index %d specified but only %d elements found", idx, len(elements))
|
||||
}
|
||||
// get target element
|
||||
element := elements[idx]
|
||||
|
||||
// recurse on the matching element
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
add, err := pm.filter(element)
|
||||
for k, v := range pm.Matches {
|
||||
p.Matches[k] = v
|
||||
}
|
||||
if err != nil || add == nil {
|
||||
return nil, err
|
||||
}
|
||||
p.append("", add.Content()...)
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
// doSeq iterates over a sequence and appends elements matching the path regex to p.Val
|
||||
func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) {
|
||||
// parse the field + match pair
|
||||
var err error
|
||||
p.field, p.matchRegex, err = SplitIndexNameValue(p.Path[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primitiveElement := len(p.field) == 0
|
||||
if primitiveElement {
|
||||
err = rn.VisitElements(p.visitPrimitiveElem)
|
||||
} else {
|
||||
err = rn.VisitElements(p.visitElem)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !p.val.IsNil() && len(p.val.YNode().Content) == 0 {
|
||||
p.val = nil
|
||||
}
|
||||
|
||||
if !IsCreate(p.Create) || p.val != nil {
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
var elem *yaml.Node
|
||||
valueNode := NewScalarRNode(p.matchRegex).YNode()
|
||||
if primitiveElement {
|
||||
elem = valueNode
|
||||
} else {
|
||||
elem = &yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: p.field}, valueNode},
|
||||
}
|
||||
}
|
||||
err = rn.PipeE(Append(elem))
|
||||
if err != nil {
|
||||
return nil, errors.WrapPrefixf(err, "failed to create element for %q", p.Path[0])
|
||||
}
|
||||
// re-do the sequence search; this time we'll find the element we just created
|
||||
return p.doSeq(rn)
|
||||
}
|
||||
|
||||
func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
str, err := elem.String()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.appendRNode("", elem)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) visitElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// check if this elements field matches the regex
|
||||
val := elem.Field(p.field)
|
||||
if val == nil || val.Value == nil {
|
||||
return nil
|
||||
}
|
||||
str, err := val.Value.String()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// recurse on the matching element
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
add, err := pm.filter(elem)
|
||||
for k, v := range pm.Matches {
|
||||
p.Matches[k] = v
|
||||
}
|
||||
if err != nil || add == nil {
|
||||
return err
|
||||
}
|
||||
p.append(str, add.Content()...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) appendRNode(path string, node *RNode) {
|
||||
p.append(path, node.YNode())
|
||||
}
|
||||
|
||||
func (p *PathMatcher) append(path string, nodes ...*Node) {
|
||||
if p.val == nil {
|
||||
p.val = NewRNode(&Node{Kind: SequenceNode})
|
||||
}
|
||||
for i := range nodes {
|
||||
node := nodes[i]
|
||||
p.val.YNode().Content = append(p.val.YNode().Content, node)
|
||||
// record the path if specified
|
||||
if path != "" {
|
||||
p.Matches[node] = append(p.Matches[node], path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanPath(path []string) []string {
|
||||
var p []string
|
||||
for _, elem := range path {
|
||||
elem = strings.TrimSpace(elem)
|
||||
if len(elem) == 0 {
|
||||
continue
|
||||
}
|
||||
p = append(p, elem)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// handleStructuredDataInScalar processes a scalar field that contains structured data (JSON/YAML)
|
||||
// and allows path navigation within that structured data
|
||||
func (p *PathMatcher) handleStructuredDataInScalar(scalarField *RNode) (*RNode, error) {
|
||||
scalarValue := scalarField.YNode().Value
|
||||
var parsedNode yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err != nil {
|
||||
return nil, fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// Create a structured field from the parsed data
|
||||
structuredField := NewRNode(&parsedNode)
|
||||
|
||||
// Process the remaining path on the structured data
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
result, err := pm.filter(structuredField)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Matches = pm.Matches
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package merge2 contains libraries for merging fields from one RNode to another
|
||||
// RNode
|
||||
package merge2
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
|
||||
)
|
||||
|
||||
// Merge merges fields from src into dest.
|
||||
func Merge(src, dest *yaml.RNode, mergeOptions yaml.MergeOptions) (*yaml.RNode, error) {
|
||||
return walk.Walker{
|
||||
Sources: []*yaml.RNode{dest, src},
|
||||
Visitor: Merger{},
|
||||
MergeOptions: mergeOptions,
|
||||
}.Walk()
|
||||
}
|
||||
|
||||
// MergeStrings parses the arguments, and merges fields from srcStr into destStr.
|
||||
func MergeStrings(srcStr, destStr string, infer bool, mergeOptions yaml.MergeOptions) (string, error) {
|
||||
src, err := yaml.Parse(srcStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dest, err := yaml.Parse(destStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := walk.Walker{
|
||||
Sources: []*yaml.RNode{dest, src},
|
||||
Visitor: Merger{},
|
||||
InferAssociativeLists: infer,
|
||||
MergeOptions: mergeOptions,
|
||||
}.Walk()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
type Merger struct {
|
||||
}
|
||||
|
||||
// for forwards compatibility when new functions are added to the interface
|
||||
var _ walk.Visitor = Merger{}
|
||||
|
||||
func (m Merger) VisitMap(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.SetStyle(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if yaml.IsMissingOrNull(nodes.Dest()) {
|
||||
// Add
|
||||
ps, _ := determineSmpDirective(nodes.Origin())
|
||||
if ps == smpDelete {
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
|
||||
// If Origin is missing, preserve explicitly set null in Dest ("null", "~", etc)
|
||||
if nodes.Origin().IsNil() && !nodes.Dest().IsNil() && len(nodes.Dest().YNode().Value) > 0 {
|
||||
return yaml.MakePersistentNullNode(nodes.Dest().YNode().Value), nil
|
||||
}
|
||||
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
if nodes.Origin().IsTaggedNull() {
|
||||
// clear the value
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
|
||||
ps, err := determineSmpDirective(nodes.Origin())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch ps {
|
||||
case smpDelete:
|
||||
return walk.ClearNode, nil
|
||||
case smpReplace:
|
||||
return nodes.Origin(), nil
|
||||
default:
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Merger) VisitScalar(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.SetStyle(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Override value
|
||||
if nodes.Origin() != nil {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Keep
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Merger) VisitList(nodes walk.Sources, s *openapi.ResourceSchema, kind walk.ListKind) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.SetStyle(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind == walk.NonAssociateList {
|
||||
// Override value
|
||||
if nodes.Origin() != nil {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Keep
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
// Add
|
||||
if yaml.IsMissingOrNull(nodes.Dest()) {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Clear
|
||||
if nodes.Origin().IsTaggedNull() {
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
|
||||
ps, err := determineSmpDirective(nodes.Origin())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch ps {
|
||||
case smpDelete:
|
||||
return walk.ClearNode, nil
|
||||
case smpReplace:
|
||||
return nodes.Origin(), nil
|
||||
default:
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Merger) SetStyle(sources walk.Sources) error {
|
||||
source := sources.Origin()
|
||||
dest := sources.Dest()
|
||||
if dest == nil || dest.YNode() == nil || source == nil || source.YNode() == nil {
|
||||
// avoid panic
|
||||
return nil
|
||||
}
|
||||
|
||||
// copy the style from the source.
|
||||
// special case: if the dest was an empty map or seq, then it probably had
|
||||
// folded style applied, but we actually want to keep the style of the origin
|
||||
// in this case (even if it was the default). otherwise the merged elements
|
||||
// will get folded even though this probably isn't what is desired.
|
||||
if dest.YNode().Kind != yaml.ScalarNode && len(dest.YNode().Content) == 0 {
|
||||
dest.YNode().Style = source.YNode().Style
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetComments copies the dest comments to the source comments if they are present
|
||||
// on the source.
|
||||
func (m Merger) SetComments(sources walk.Sources) error {
|
||||
source := sources.Origin()
|
||||
dest := sources.Dest()
|
||||
if dest == nil || dest.YNode() == nil || source == nil || source.YNode() == nil {
|
||||
// avoid panic
|
||||
return nil
|
||||
}
|
||||
if source.YNode().FootComment != "" {
|
||||
dest.YNode().FootComment = source.YNode().FootComment
|
||||
}
|
||||
if source.YNode().HeadComment != "" {
|
||||
dest.YNode().HeadComment = source.YNode().HeadComment
|
||||
}
|
||||
if source.YNode().LineComment != "" {
|
||||
dest.YNode().LineComment = source.YNode().LineComment
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// A strategic merge patch directive.
|
||||
// See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md
|
||||
//
|
||||
//go:generate stringer -type=smpDirective -linecomment
|
||||
type smpDirective int
|
||||
|
||||
const (
|
||||
smpUnknown smpDirective = iota // unknown
|
||||
smpReplace // replace
|
||||
smpDelete // delete
|
||||
smpMerge // merge
|
||||
)
|
||||
|
||||
const strategicMergePatchDirectiveKey = "$patch"
|
||||
|
||||
// Examine patch for a strategic merge patch directive.
|
||||
// If found, return it, and remove the directive from the patch.
|
||||
func determineSmpDirective(patch *yaml.RNode) (smpDirective, error) {
|
||||
if patch == nil {
|
||||
return smpMerge, nil
|
||||
}
|
||||
switch patch.YNode().Kind {
|
||||
case yaml.SequenceNode:
|
||||
return determineSequenceNodePatchStrategy(patch)
|
||||
case yaml.MappingNode:
|
||||
return determineMappingNodePatchStrategy(patch)
|
||||
default:
|
||||
return smpUnknown, fmt.Errorf(
|
||||
"no implemented strategic merge patch strategy for '%s' ('%s')",
|
||||
patch.YNode().ShortTag(), patch.MustString())
|
||||
}
|
||||
}
|
||||
|
||||
func determineSequenceNodePatchStrategy(patch *yaml.RNode) (smpDirective, error) {
|
||||
// get the $patch element
|
||||
node, err := patch.Pipe(yaml.GetElementByKey(strategicMergePatchDirectiveKey))
|
||||
// if there are more than 1 key/value pair in the map, then this $patch
|
||||
// is not for the sequence
|
||||
if err != nil || node == nil || node.YNode() == nil || len(node.Content()) > 2 {
|
||||
return smpMerge, nil
|
||||
}
|
||||
// get the value
|
||||
value, err := node.Pipe(yaml.Get(strategicMergePatchDirectiveKey))
|
||||
if err != nil || value == nil || value.YNode() == nil {
|
||||
return smpMerge, nil
|
||||
}
|
||||
v := value.YNode().Value
|
||||
if v == smpDelete.String() {
|
||||
return smpDelete, elideSequencePatchDirective(patch, v)
|
||||
}
|
||||
if v == smpReplace.String() {
|
||||
return smpReplace, elideSequencePatchDirective(patch, v)
|
||||
}
|
||||
if v == smpMerge.String() {
|
||||
return smpMerge, elideSequencePatchDirective(patch, v)
|
||||
}
|
||||
return smpUnknown, fmt.Errorf(
|
||||
"unknown patch strategy '%s'", v)
|
||||
}
|
||||
|
||||
func determineMappingNodePatchStrategy(patch *yaml.RNode) (smpDirective, error) {
|
||||
node, err := patch.Pipe(yaml.Get(strategicMergePatchDirectiveKey))
|
||||
if err != nil || node == nil || node.YNode() == nil {
|
||||
return smpMerge, nil
|
||||
}
|
||||
v := node.YNode().Value
|
||||
if v == smpDelete.String() {
|
||||
return smpDelete, elideMappingPatchDirective(patch)
|
||||
}
|
||||
if v == smpReplace.String() {
|
||||
return smpReplace, elideMappingPatchDirective(patch)
|
||||
}
|
||||
if v == smpMerge.String() {
|
||||
return smpMerge, elideMappingPatchDirective(patch)
|
||||
}
|
||||
return smpUnknown, fmt.Errorf(
|
||||
"unknown patch strategy '%s'", v)
|
||||
}
|
||||
|
||||
func elideMappingPatchDirective(patch *yaml.RNode) error {
|
||||
return patch.PipeE(yaml.Clear(strategicMergePatchDirectiveKey))
|
||||
}
|
||||
|
||||
func elideSequencePatchDirective(patch *yaml.RNode, value string) error {
|
||||
return patch.PipeE(yaml.ElementSetter{
|
||||
Element: nil,
|
||||
Keys: []string{strategicMergePatchDirectiveKey},
|
||||
Values: []string{value},
|
||||
})
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Code generated by "stringer -type=smpDirective -linecomment"; DO NOT EDIT.
|
||||
|
||||
package merge2
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[smpUnknown-0]
|
||||
_ = x[smpReplace-1]
|
||||
_ = x[smpDelete-2]
|
||||
_ = x[smpMerge-3]
|
||||
}
|
||||
|
||||
const _smpDirective_name = "unknownreplacedeletemerge"
|
||||
|
||||
var _smpDirective_index = [...]uint8{0, 7, 14, 20, 25}
|
||||
|
||||
func (i smpDirective) String() string {
|
||||
if i < 0 || i >= smpDirective(len(_smpDirective_index)-1) {
|
||||
return "smpDirective(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _smpDirective_name[_smpDirective_index[i]:_smpDirective_index[i+1]]
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
// fieldSortOrder contains the relative ordering of fields when formatting an
|
||||
// object.
|
||||
var fieldSortOrder = []string{
|
||||
// top-level metadata
|
||||
"name", "generateName", "namespace", "clusterName",
|
||||
"apiVersion", "kind", "metadata", "type",
|
||||
"labels", "annotations",
|
||||
"spec", "status",
|
||||
|
||||
// secret and configmap
|
||||
"stringData", "data", "binaryData",
|
||||
|
||||
// cronjobspec, daemonsetspec, deploymentspec, statefulsetspec,
|
||||
// jobspec fields
|
||||
"parallelism", "completions", "activeDeadlineSeconds", "backoffLimit",
|
||||
"replicas", "selector", "manualSelector", "template",
|
||||
"ttlSecondsAfterFinished", "volumeClaimTemplates", "service", "serviceName",
|
||||
"podManagementPolicy", "updateStrategy", "strategy", "minReadySeconds",
|
||||
"revision", "revisionHistoryLimit", "paused", "progressDeadlineSeconds",
|
||||
|
||||
// podspec
|
||||
// podspec scalars
|
||||
"restartPolicy", "terminationGracePeriodSeconds",
|
||||
"activeDeadlineSeconds", "dnsPolicy", "serviceAccountName",
|
||||
"serviceAccount", "automountServiceAccountToken", "nodeName",
|
||||
"hostNetwork", "hostPID", "hostIPC", "shareProcessNamespace", "hostname",
|
||||
"subdomain", "schedulerName", "priorityClassName", "priority",
|
||||
"runtimeClassName", "enableServiceLinks",
|
||||
|
||||
// podspec lists and maps
|
||||
"nodeSelector", "hostAliases",
|
||||
|
||||
// podspec objects
|
||||
"initContainers", "containers", "volumes", "securityContext",
|
||||
"imagePullSecrets", "affinity", "tolerations", "dnsConfig",
|
||||
"readinessGates",
|
||||
|
||||
// containers
|
||||
"image", "command", "args", "workingDir", "ports", "envFrom", "env",
|
||||
"resources", "volumeMounts", "volumeDevices", "livenessProbe",
|
||||
"readinessProbe", "lifecycle", "terminationMessagePath",
|
||||
"terminationMessagePolicy", "imagePullPolicy", "securityContext",
|
||||
"stdin", "stdinOnce", "tty",
|
||||
|
||||
// service
|
||||
"clusterIP", "externalIPs", "loadBalancerIP", "loadBalancerSourceRanges",
|
||||
"externalName", "externalTrafficPolicy", "sessionAffinity",
|
||||
|
||||
// ports
|
||||
"protocol", "port", "targetPort", "hostPort", "containerPort", "hostIP",
|
||||
|
||||
// volumemount
|
||||
"readOnly", "mountPath", "subPath", "subPathExpr", "mountPropagation",
|
||||
|
||||
// envvar + envvarsource
|
||||
"value", "valueFrom", "fieldRef", "resourceFieldRef", "configMapKeyRef",
|
||||
"secretKeyRef", "prefix", "configMapRef", "secretRef",
|
||||
}
|
||||
|
||||
type set map[string]interface{}
|
||||
|
||||
func newSet(values ...string) set {
|
||||
m := map[string]interface{}{}
|
||||
for _, value := range values {
|
||||
m[value] = nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s set) Has(key string) bool {
|
||||
_, found := s[key]
|
||||
return found
|
||||
}
|
||||
|
||||
// WhitelistedListSortKinds contains the set of kinds that are whitelisted
|
||||
// for sorting list field elements
|
||||
var WhitelistedListSortKinds = newSet(
|
||||
"CronJob", "DaemonSet", "Deployment", "Job", "ReplicaSet", "StatefulSet",
|
||||
"ValidatingWebhookConfiguration")
|
||||
|
||||
// WhitelistedListSortApis contains the set of apis that are whitelisted for
|
||||
// sorting list field elements
|
||||
var WhitelistedListSortApis = newSet(
|
||||
"apps/v1", "apps/v1beta1", "apps/v1beta2", "batch/v1", "batch/v1beta1",
|
||||
"extensions/v1beta1", "v1", "admissionregistration.k8s.io/v1")
|
||||
|
||||
// WhitelistedListSortFields contains json paths to list fields that should
|
||||
// be sorted, and the field they should be sorted by
|
||||
var WhitelistedListSortFields = map[string]string{
|
||||
".spec.template.spec.containers": "name",
|
||||
".webhooks.rules.operations": "",
|
||||
}
|
||||
|
||||
// FieldOrder indexes fields and maps them to relative precedence
|
||||
var FieldOrder = func() map[string]int {
|
||||
// create an index of field orderings
|
||||
fo := map[string]int{}
|
||||
for i, f := range fieldSortOrder {
|
||||
fo[f] = i + 1
|
||||
}
|
||||
return fo
|
||||
}()
|
||||
+1404
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package schema contains libraries for working with the yaml and openapi packages.
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// IsAssociative returns true if all elements in the list contain an
|
||||
// AssociativeSequenceKey as a field.
|
||||
func IsAssociative(schema *openapi.ResourceSchema, nodes []*yaml.RNode, infer bool) bool {
|
||||
if schema != nil {
|
||||
return schemaHasMergeStrategy(schema)
|
||||
}
|
||||
if !infer {
|
||||
return false
|
||||
}
|
||||
for i := range nodes {
|
||||
node := nodes[i]
|
||||
if yaml.IsMissingOrNull(node) {
|
||||
continue
|
||||
}
|
||||
if node.IsAssociative() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func schemaHasMergeStrategy(schema *openapi.ResourceSchema) bool {
|
||||
tmp, _ := schema.PatchStrategyAndKey()
|
||||
strategies := strings.Split(tmp, ",")
|
||||
for _, s := range strategies {
|
||||
if s == "merge" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
)
|
||||
|
||||
// CopyYNode returns a distinct copy of its argument.
|
||||
// Use https://github.com/jinzhu/copier instead?
|
||||
func CopyYNode(n *yaml.Node) *yaml.Node {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
c := *n
|
||||
if len(n.Content) > 0 {
|
||||
// Using Go 'copy' here doesn't yield independent slices.
|
||||
c.Content = make([]*Node, len(n.Content))
|
||||
for i, item := range n.Content {
|
||||
c.Content[i] = CopyYNode(item)
|
||||
}
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// IsYNodeTaggedNull returns true if the node is explicitly tagged Null.
|
||||
func IsYNodeTaggedNull(n *yaml.Node) bool {
|
||||
return n != nil && n.Tag == NodeTagNull
|
||||
}
|
||||
|
||||
// IsYNodeEmptyMap is true if the Node is a non-nil empty map.
|
||||
func IsYNodeEmptyMap(n *yaml.Node) bool {
|
||||
return n != nil && n.Kind == yaml.MappingNode && len(n.Content) == 0
|
||||
}
|
||||
|
||||
// IsYNodeEmptySeq is true if the Node is a non-nil empty sequence.
|
||||
func IsYNodeEmptySeq(n *yaml.Node) bool {
|
||||
return n != nil && n.Kind == yaml.SequenceNode && len(n.Content) == 0
|
||||
}
|
||||
|
||||
// IsYNodeNilOrEmpty is true if the Node is nil or appears empty.
|
||||
func IsYNodeNilOrEmpty(n *yaml.Node) bool {
|
||||
return n == nil ||
|
||||
IsYNodeTaggedNull(n) ||
|
||||
IsYNodeEmptyMap(n) ||
|
||||
IsYNodeEmptySeq(n) ||
|
||||
IsYNodeZero(n)
|
||||
}
|
||||
|
||||
// IsYNodeEmptyDoc is true if the node is a Document with no content.
|
||||
// E.g.: "---\n---"
|
||||
func IsYNodeEmptyDoc(n *yaml.Node) bool {
|
||||
return n.Kind == yaml.DocumentNode && n.Content[0].Tag == NodeTagNull
|
||||
}
|
||||
|
||||
func IsYNodeString(n *yaml.Node) bool {
|
||||
return n.Kind == yaml.ScalarNode &&
|
||||
(n.Tag == NodeTagString || n.Tag == NodeTagEmpty)
|
||||
}
|
||||
|
||||
// IsYNodeZero is true if all the public fields in the Node are empty.
|
||||
// Which means it's not initialized and should be omitted when marshal.
|
||||
// The Node itself has a method IsZero but it is not released
|
||||
// in yaml.v3. https://pkg.go.dev/gopkg.in/yaml.v3#Node.IsZero
|
||||
func IsYNodeZero(n *yaml.Node) bool {
|
||||
// TODO: Change this to use IsZero when it's avaialable.
|
||||
return n != nil && n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" &&
|
||||
n.Anchor == "" && n.Alias == nil && n.Content == nil &&
|
||||
n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" &&
|
||||
n.Line == 0 && n.Column == 0
|
||||
}
|
||||
|
||||
// Parser parses values into configuration.
|
||||
type Parser struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (p Parser) Filter(_ *RNode) (*RNode, error) {
|
||||
d := yaml.NewDecoder(bytes.NewBuffer([]byte(p.Value)))
|
||||
o := &RNode{value: &yaml.Node{}}
|
||||
return o, d.Decode(o.value)
|
||||
}
|
||||
|
||||
// TODO(pwittrock): test this
|
||||
func GetStyle(styles ...string) Style {
|
||||
var style Style
|
||||
for _, s := range styles {
|
||||
switch s {
|
||||
case "TaggedStyle":
|
||||
style |= TaggedStyle
|
||||
case "DoubleQuotedStyle":
|
||||
style |= DoubleQuotedStyle
|
||||
case "SingleQuotedStyle":
|
||||
style |= SingleQuotedStyle
|
||||
case "LiteralStyle":
|
||||
style |= LiteralStyle
|
||||
case "FoldedStyle":
|
||||
style |= FoldedStyle
|
||||
case "FlowStyle":
|
||||
style |= FlowStyle
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// Filter defines a function to manipulate an individual RNode such as by changing
|
||||
// its values, or returning a field.
|
||||
//
|
||||
// When possible, Filters should be serializable to yaml so that they can be described
|
||||
// declaratively as data.
|
||||
//
|
||||
// Analogous to http://www.linfo.org/filters.html
|
||||
type Filter interface {
|
||||
Filter(object *RNode) (*RNode, error)
|
||||
}
|
||||
|
||||
type FilterFunc func(object *RNode) (*RNode, error)
|
||||
|
||||
func (f FilterFunc) Filter(object *RNode) (*RNode, error) {
|
||||
return f(object)
|
||||
}
|
||||
|
||||
// TypeMeta partially copies apimachinery/pkg/apis/meta/v1.TypeMeta
|
||||
// No need for a direct dependence; the fields are stable.
|
||||
type TypeMeta struct {
|
||||
// APIVersion is the apiVersion field of a Resource
|
||||
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
|
||||
// Kind is the kind field of a Resource
|
||||
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
|
||||
}
|
||||
|
||||
// NameMeta contains name information.
|
||||
type NameMeta struct {
|
||||
// Name is the metadata.name field of a Resource
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
// Namespace is the metadata.namespace field of a Resource
|
||||
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceMeta contains the metadata for a both Resource Type and Resource.
|
||||
type ResourceMeta struct {
|
||||
TypeMeta `json:",inline" yaml:",inline"`
|
||||
// ObjectMeta is the metadata field of a Resource
|
||||
ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectMeta contains metadata about a Resource
|
||||
type ObjectMeta struct {
|
||||
NameMeta `json:",inline" yaml:",inline"`
|
||||
// Labels is the metadata.labels field of a Resource
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
// Annotations is the metadata.annotations field of a Resource.
|
||||
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// GetIdentifier returns a ResourceIdentifier that includes
|
||||
// the information needed to uniquely identify a resource in a cluster.
|
||||
func (m *ResourceMeta) GetIdentifier() ResourceIdentifier {
|
||||
return ResourceIdentifier{
|
||||
TypeMeta: m.TypeMeta,
|
||||
NameMeta: m.NameMeta,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceIdentifier contains the information needed to uniquely
|
||||
// identify a resource in a cluster.
|
||||
type ResourceIdentifier struct {
|
||||
TypeMeta `json:",inline" yaml:",inline"`
|
||||
NameMeta `json:",inline" yaml:",inline"`
|
||||
}
|
||||
|
||||
// Comments struct is comment yaml comment types
|
||||
type Comments struct {
|
||||
LineComment string `yaml:"lineComment,omitempty"`
|
||||
HeadComment string `yaml:"headComment,omitempty"`
|
||||
FootComment string `yaml:"footComment,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ResourceIdentifier) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *ResourceIdentifier) GetNamespace() string {
|
||||
return r.Namespace
|
||||
}
|
||||
|
||||
func (r *ResourceIdentifier) GetAPIVersion() string {
|
||||
return r.APIVersion
|
||||
}
|
||||
|
||||
func (r *ResourceIdentifier) GetKind() string {
|
||||
return r.Kind
|
||||
}
|
||||
|
||||
const (
|
||||
Trim = "Trim"
|
||||
Flow = "Flow"
|
||||
)
|
||||
|
||||
// String returns a string value for a Node, applying the supplied formatting options
|
||||
func String(node *yaml.Node, opts ...string) (string, error) {
|
||||
if node == nil {
|
||||
return "", nil
|
||||
}
|
||||
optsSet := sets.String{}
|
||||
optsSet.Insert(opts...)
|
||||
if optsSet.Has(Flow) {
|
||||
oldStyle := node.Style
|
||||
defer func() {
|
||||
node.Style = oldStyle
|
||||
}()
|
||||
node.Style = yaml.FlowStyle
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
e := NewEncoder(b)
|
||||
err := e.Encode(node)
|
||||
errClose := e.Close()
|
||||
if err == nil {
|
||||
err = errClose
|
||||
}
|
||||
val := b.String()
|
||||
if optsSet.Has(Trim) {
|
||||
val = strings.TrimSpace(val)
|
||||
}
|
||||
return val, errors.Wrap(err)
|
||||
}
|
||||
|
||||
// MergeOptionsListIncreaseDirection is the type of list growth in merge
|
||||
type MergeOptionsListIncreaseDirection int
|
||||
|
||||
const (
|
||||
MergeOptionsListAppend MergeOptionsListIncreaseDirection = iota
|
||||
MergeOptionsListPrepend
|
||||
)
|
||||
|
||||
// MergeOptions is a struct which contains the options for merge
|
||||
type MergeOptions struct {
|
||||
// ListIncreaseDirection indicates should merge function prepend the items from
|
||||
// source list to destination or append.
|
||||
ListIncreaseDirection MergeOptionsListIncreaseDirection
|
||||
}
|
||||
|
||||
// Since ObjectMeta and TypeMeta are stable, we manually create DeepCopy funcs for ResourceMeta and ObjectMeta.
|
||||
// For TypeMeta and NameMeta no DeepCopy funcs are required, as they only contain basic types.
|
||||
|
||||
// DeepCopyInto copies the receiver, writing into out. in must be non-nil.
|
||||
func (in *ObjectMeta) DeepCopyInto(out *ObjectMeta) {
|
||||
*out = *in
|
||||
out.NameMeta = in.NameMeta
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy copies the receiver, creating a new ObjectMeta.
|
||||
func (in *ObjectMeta) DeepCopy() *ObjectMeta {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ObjectMeta)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceMeta) DeepCopyInto(out *ResourceMeta) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
}
|
||||
|
||||
// DeepCopy copies the receiver, creating a new ResourceMeta.
|
||||
func (in *ResourceMeta) DeepCopy() *ResourceMeta {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceMeta)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright 2021 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DeriveSeqIndentStyle derives the sequence indentation annotation value for the resource,
|
||||
// originalYAML is the input yaml string,
|
||||
// the style is decided by deriving the existing sequence indentation of first sequence node
|
||||
func DeriveSeqIndentStyle(originalYAML string) string {
|
||||
lines := strings.Split(originalYAML, "\n")
|
||||
for i, line := range lines {
|
||||
elems := strings.SplitN(line, "- ", 2)
|
||||
if len(elems) != 2 {
|
||||
continue
|
||||
}
|
||||
// prefix of "- " must be sequence of spaces
|
||||
if strings.Trim(elems[0], " ") != "" {
|
||||
continue
|
||||
}
|
||||
numSpacesBeforeSeqElem := len(elems[0])
|
||||
|
||||
// keyLine is the line before the first sequence element
|
||||
keyLine := keyLineBeforeSeqElem(lines, i)
|
||||
if keyLine == "" {
|
||||
// there is no keyLine for this sequence node
|
||||
// all of those lines are comments
|
||||
continue
|
||||
}
|
||||
numSpacesBeforeKeyElem := len(keyLine) - len(strings.TrimLeft(keyLine, " "))
|
||||
trimmedKeyLine := strings.Trim(keyLine, " ")
|
||||
if strings.Count(trimmedKeyLine, ":") != 1 || !strings.HasSuffix(trimmedKeyLine, ":") {
|
||||
// if the key line doesn't contain only one : that too at the end,
|
||||
// this is not a sequence node, it is a wrapped sequence node string
|
||||
// ignore it
|
||||
continue
|
||||
}
|
||||
|
||||
if numSpacesBeforeSeqElem == numSpacesBeforeKeyElem {
|
||||
return string(CompactSequenceStyle)
|
||||
}
|
||||
|
||||
if numSpacesBeforeSeqElem-numSpacesBeforeKeyElem == 2 {
|
||||
return string(WideSequenceStyle)
|
||||
}
|
||||
}
|
||||
|
||||
return string(CompactSequenceStyle)
|
||||
}
|
||||
|
||||
// keyLineBeforeSeqElem iterates through the lines before the first seqElement
|
||||
// and tries to find the non-comment key line for the sequence node
|
||||
func keyLineBeforeSeqElem(lines []string, seqElemIndex int) string {
|
||||
// start with the previous line of sequence element
|
||||
i := seqElemIndex - 1
|
||||
for ; i >= 0; i-- {
|
||||
line := lines[i]
|
||||
trimmedLine := strings.Trim(line, " ")
|
||||
if strings.HasPrefix(trimmedLine, "#") { // commented line
|
||||
continue
|
||||
}
|
||||
// we have a non-commented line which can have a trailing comment
|
||||
parts := strings.SplitN(line, "#", 2)
|
||||
return parts[0] // throw away the trailing comment part
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// appendListNode will append the nodes from src to dst and return dst.
|
||||
// src and dst should be both sequence node. key is used to call ElementSetter.
|
||||
// ElementSetter will use key-value pair to find and set the element in sequence
|
||||
// node.
|
||||
func appendListNode(dst, src *yaml.RNode, keys []string) (*yaml.RNode, error) {
|
||||
var err error
|
||||
for _, elem := range src.Content() {
|
||||
// If key is empty, we know this is a scalar value and we can directly set the
|
||||
// node
|
||||
if keys[0] == "" {
|
||||
_, err = dst.Pipe(yaml.ElementSetter{
|
||||
Element: elem,
|
||||
Keys: []string{""},
|
||||
Values: []string{elem.Value},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// we need to get the value for key so that we can find the element to set
|
||||
// in sequence.
|
||||
v := []string{}
|
||||
for _, key := range keys {
|
||||
tmpNode := yaml.NewRNode(elem)
|
||||
valueNode, err := tmpNode.Pipe(yaml.Get(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if valueNode.IsNil() {
|
||||
// no key found, directly append to dst
|
||||
err = dst.PipeE(yaml.Append(elem))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
v = append(v, valueNode.YNode().Value)
|
||||
}
|
||||
|
||||
// When there are multiple keys, ElementSetter appends the node to dst
|
||||
// even if the output is already in dst. We remove the node from dst to
|
||||
// prevent duplicates.
|
||||
if len(keys) > 1 {
|
||||
_, err = dst.Pipe(yaml.ElementSetter{
|
||||
Keys: keys,
|
||||
Values: v,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// We use the key and value from elem to find the corresponding element in dst.
|
||||
// Then we will use ElementSetter to replace the element with elem. If we cannot
|
||||
// find the item, the element will be appended.
|
||||
_, err = dst.Pipe(yaml.ElementSetter{
|
||||
Element: elem,
|
||||
Keys: keys,
|
||||
Values: v,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// validateKeys returns a list of valid key-value pairs
|
||||
// if secondary merge key values are missing, use only the available merge keys
|
||||
func validateKeys(valuesList [][]string, values []string, keys []string) ([]string, []string) {
|
||||
validKeys := make([]string, 0)
|
||||
validValues := make([]string, 0)
|
||||
validKeySet := sets.String{}
|
||||
for _, values := range valuesList {
|
||||
for i, v := range values {
|
||||
if v != "" {
|
||||
validKeySet.Insert(keys[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if validKeySet.Len() == 0 { // if values missing, fall back to primary keys
|
||||
return keys, values
|
||||
}
|
||||
for _, k := range keys {
|
||||
if validKeySet.Has(k) {
|
||||
validKeys = append(validKeys, k)
|
||||
}
|
||||
}
|
||||
for i, v := range values {
|
||||
if v != "" || validKeySet.Has(keys[i]) {
|
||||
validValues = append(validValues, v)
|
||||
}
|
||||
}
|
||||
return validKeys, validValues
|
||||
}
|
||||
|
||||
// mergeValues merges values together - e.g. if two containerPorts
|
||||
// have the same port and targetPort but one has an empty protocol
|
||||
// and the other doesn't, they are treated as the same containerPort
|
||||
func mergeValues(valuesList [][]string) [][]string {
|
||||
for i, values1 := range valuesList {
|
||||
for j, values2 := range valuesList {
|
||||
if matched, values := match(values1, values2); matched {
|
||||
valuesList[i] = values
|
||||
valuesList[j] = values
|
||||
}
|
||||
}
|
||||
}
|
||||
return valuesList
|
||||
}
|
||||
|
||||
// two values match if they have at least one common element and
|
||||
// corresponding elements only differ if one is an empty string
|
||||
func match(values1 []string, values2 []string) (bool, []string) {
|
||||
if len(values1) != len(values2) {
|
||||
return false, nil
|
||||
}
|
||||
var commonElement bool
|
||||
var res []string
|
||||
for i := range values1 {
|
||||
if values1[i] == values2[i] {
|
||||
commonElement = true
|
||||
res = append(res, values1[i])
|
||||
continue
|
||||
}
|
||||
if values1[i] != "" && values2[i] != "" {
|
||||
return false, nil
|
||||
}
|
||||
if values1[i] != "" {
|
||||
res = append(res, values1[i])
|
||||
} else {
|
||||
res = append(res, values2[i])
|
||||
}
|
||||
}
|
||||
return commonElement, res
|
||||
}
|
||||
|
||||
// setAssociativeSequenceElements recursively set the elements in the list
|
||||
func (l *Walker) setAssociativeSequenceElements(valuesList [][]string, keys []string, dest *yaml.RNode) (*yaml.RNode, error) {
|
||||
// itemsToBeAdded contains the items that will be added to dest
|
||||
itemsToBeAdded := yaml.NewListRNode()
|
||||
var schema *openapi.ResourceSchema
|
||||
if l.Schema != nil {
|
||||
schema = l.Schema.Elements()
|
||||
}
|
||||
if len(keys) > 1 {
|
||||
valuesList = mergeValues(valuesList)
|
||||
}
|
||||
|
||||
// each element in valuesList is a list of values corresponding to the keys
|
||||
// for example, for the following yaml:
|
||||
// - containerPort: 8080
|
||||
// protocol: UDP
|
||||
// - containerPort: 8080
|
||||
// protocol: TCP
|
||||
// `keys` would be [containerPort, protocol]
|
||||
// and `valuesList` would be [ [8080, UDP], [8080, TCP] ]
|
||||
var validKeys []string
|
||||
var validValues []string
|
||||
for _, values := range valuesList {
|
||||
if len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
validKeys, validValues = validateKeys(valuesList, values, keys)
|
||||
val, err := Walker{
|
||||
VisitKeysAsScalars: l.VisitKeysAsScalars,
|
||||
InferAssociativeLists: l.InferAssociativeLists,
|
||||
Visitor: l,
|
||||
Schema: schema,
|
||||
Sources: l.elementValueList(validKeys, validValues),
|
||||
MergeOptions: l.MergeOptions,
|
||||
}.Walk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exit := false
|
||||
for i, key := range validKeys {
|
||||
// delete the node from **dest** if it's null or empty
|
||||
if yaml.IsMissingOrNull(val) || yaml.IsEmptyMap(val) {
|
||||
_, err = dest.Pipe(yaml.ElementSetter{
|
||||
Keys: validKeys,
|
||||
Values: validValues,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exit = true
|
||||
} else if val.Field(key) == nil && validValues[i] != "" {
|
||||
// make sure the key is set on the field
|
||||
_, err = val.Pipe(yaml.SetField(key, yaml.NewScalarRNode(validValues[i])))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if exit {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the val to the sequence. val will replace the item in the sequence if
|
||||
// there is an item that matches all key-value pairs. Otherwise val will be appended
|
||||
// the sequence.
|
||||
_, err = itemsToBeAdded.Pipe(yaml.ElementSetter{
|
||||
Element: val.YNode(),
|
||||
Keys: validKeys,
|
||||
Values: validValues,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(valuesList) > 0 {
|
||||
if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend {
|
||||
// items from patches are needed to be prepended. so we append the
|
||||
// dest to itemsToBeAdded
|
||||
dest, err = appendListNode(itemsToBeAdded, dest, validKeys)
|
||||
} else {
|
||||
// append the items
|
||||
dest, err = appendListNode(dest, itemsToBeAdded, validKeys)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// sequence is empty
|
||||
if yaml.IsMissingOrNull(dest) {
|
||||
return nil, nil
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func (l *Walker) walkAssociativeSequence() (*yaml.RNode, error) {
|
||||
// may require initializing the dest node
|
||||
dest, err := l.Sources.setDestNode(l.VisitList(l.Sources, l.Schema, AssociativeList))
|
||||
if dest == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the merge key(s) from schema
|
||||
var strategy string
|
||||
var keys []string
|
||||
if l.Schema != nil {
|
||||
strategy, keys = l.Schema.PatchStrategyAndKeyList()
|
||||
}
|
||||
if strategy == "" && len(keys) == 0 { // neither strategy nor keys present in the schema -- infer the key
|
||||
// find the list of elements we need to recursively walk
|
||||
key, err := l.elementKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key != "" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// non-primitive associative list -- merge the elements
|
||||
values := l.elementValues(keys)
|
||||
if len(values) != 0 || len(keys) > 0 {
|
||||
return l.setAssociativeSequenceElements(values, keys, dest)
|
||||
}
|
||||
|
||||
// primitive associative list -- merge the values
|
||||
return l.setAssociativeSequenceElements(l.elementPrimitiveValues(), []string{""}, dest)
|
||||
}
|
||||
|
||||
// elementKey returns the merge key to use for the associative list
|
||||
func (l Walker) elementKey() (string, error) {
|
||||
var key string
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] != nil && len(l.Sources[i].Content()) > 0 {
|
||||
newKey := l.Sources[i].GetAssociativeKey()
|
||||
if key != "" && key != newKey {
|
||||
return "", errors.Errorf(
|
||||
"conflicting merge keys [%s,%s] for field %s",
|
||||
key, newKey, strings.Join(l.Path, "."))
|
||||
}
|
||||
key = newKey
|
||||
}
|
||||
}
|
||||
if key == "" {
|
||||
return "", errors.Errorf("no merge key found for field %s",
|
||||
strings.Join(l.Path, "."))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// elementValues returns a slice containing all values for the field across all elements
|
||||
// from all sources.
|
||||
// Return value slice is ordered using the original ordering from the elements, where
|
||||
// elements missing from earlier sources appear later.
|
||||
func (l Walker) elementValues(keys []string) [][]string {
|
||||
// use slice to to keep elements in the original order
|
||||
var returnValues [][]string
|
||||
var seen sets.StringList
|
||||
|
||||
// if we are doing append, dest node should be the first.
|
||||
// otherwise dest node should be the last.
|
||||
beginIdx := 0
|
||||
if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend {
|
||||
beginIdx = 1
|
||||
}
|
||||
for i := range l.Sources {
|
||||
src := l.Sources[(i+beginIdx)%len(l.Sources)]
|
||||
if src == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// add the value of the field for each element
|
||||
// don't check error, we know this is a list node
|
||||
values, _ := src.ElementValuesList(keys)
|
||||
for _, s := range values {
|
||||
if len(s) == 0 || seen.Has(s) {
|
||||
continue
|
||||
}
|
||||
returnValues = append(returnValues, s)
|
||||
seen = seen.Insert(s)
|
||||
}
|
||||
}
|
||||
return returnValues
|
||||
}
|
||||
|
||||
// elementPrimitiveValues returns the primitive values in an associative list -- eg. finalizers
|
||||
func (l Walker) elementPrimitiveValues() [][]string {
|
||||
// use slice to to keep elements in the original order
|
||||
var returnValues [][]string
|
||||
seen := sets.String{}
|
||||
// if we are doing append, dest node should be the first.
|
||||
// otherwise dest node should be the last.
|
||||
beginIdx := 0
|
||||
if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend {
|
||||
beginIdx = 1
|
||||
}
|
||||
for i := range l.Sources {
|
||||
src := l.Sources[(i+beginIdx)%len(l.Sources)]
|
||||
if src == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// add the value of the field for each element
|
||||
// don't check error, we know this is a list node
|
||||
for _, item := range src.YNode().Content {
|
||||
if seen.Has(item.Value) {
|
||||
continue
|
||||
}
|
||||
returnValues = append(returnValues, []string{item.Value})
|
||||
seen.Insert(item.Value)
|
||||
}
|
||||
}
|
||||
return returnValues
|
||||
}
|
||||
|
||||
// fieldValue returns a slice containing each source's value for fieldName
|
||||
func (l Walker) elementValueList(keys []string, values []string) []*yaml.RNode {
|
||||
keys, values = validateKeys([][]string{values}, values, keys)
|
||||
var fields []*yaml.RNode
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] == nil {
|
||||
fields = append(fields, nil)
|
||||
continue
|
||||
}
|
||||
fields = append(fields, l.Sources[i].ElementList(keys, values))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// walkMap returns the value of VisitMap
|
||||
//
|
||||
// - call VisitMap
|
||||
// - set the return value on l.Dest
|
||||
// - walk each source field
|
||||
// - set each source field value on l.Dest
|
||||
func (l Walker) walkMap() (*yaml.RNode, error) {
|
||||
// get the new map value
|
||||
dest, err := l.Sources.setDestNode(l.VisitMap(l.Sources, l.Schema))
|
||||
if dest == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// recursively set the field values on the map
|
||||
for _, key := range l.fieldNames() {
|
||||
var res *yaml.RNode
|
||||
var keys []*yaml.RNode
|
||||
if l.VisitKeysAsScalars {
|
||||
// visit the map keys as if they were scalars,
|
||||
// this is necessary if doing things such as copying
|
||||
// comments
|
||||
for i := range l.Sources {
|
||||
// construct the sources from the keys
|
||||
if l.Sources[i] == nil {
|
||||
keys = append(keys, nil)
|
||||
continue
|
||||
}
|
||||
field := l.Sources[i].Field(key)
|
||||
if field == nil || yaml.IsMissingOrNull(field.Key) {
|
||||
keys = append(keys, nil)
|
||||
continue
|
||||
}
|
||||
keys = append(keys, field.Key)
|
||||
}
|
||||
// visit the sources as a scalar
|
||||
// keys don't have any schema --pass in nil
|
||||
res, err = l.Visitor.VisitScalar(keys, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var s *openapi.ResourceSchema
|
||||
if l.Schema != nil {
|
||||
s = l.Schema.Field(key)
|
||||
}
|
||||
fv, commentSch, keyStyles := l.fieldValue(key)
|
||||
if commentSch != nil {
|
||||
s = commentSch
|
||||
}
|
||||
val, err := Walker{
|
||||
VisitKeysAsScalars: l.VisitKeysAsScalars,
|
||||
InferAssociativeLists: l.InferAssociativeLists,
|
||||
Visitor: l,
|
||||
Schema: s,
|
||||
Sources: fv,
|
||||
MergeOptions: l.MergeOptions,
|
||||
Path: append(l.Path, key)}.Walk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// transfer the comments of res to dest node
|
||||
var comments yaml.Comments
|
||||
if !yaml.IsMissingOrNull(res) {
|
||||
comments = yaml.Comments{
|
||||
LineComment: res.YNode().LineComment,
|
||||
HeadComment: res.YNode().HeadComment,
|
||||
FootComment: res.YNode().FootComment,
|
||||
}
|
||||
if len(keys) > 0 && !yaml.IsMissingOrNull(keys[DestIndex]) {
|
||||
keys[DestIndex].YNode().HeadComment = res.YNode().HeadComment
|
||||
keys[DestIndex].YNode().LineComment = res.YNode().LineComment
|
||||
keys[DestIndex].YNode().FootComment = res.YNode().FootComment
|
||||
}
|
||||
}
|
||||
|
||||
// this handles empty and non-empty values
|
||||
fieldSetter := yaml.FieldSetter{
|
||||
Name: key,
|
||||
Comments: comments,
|
||||
AppendKeyStyle: keyStyles[val],
|
||||
Value: val,
|
||||
}
|
||||
_, err = dest.Pipe(fieldSetter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// valueIfPresent returns node.Value if node is non-nil, otherwise returns nil
|
||||
func (l Walker) valueIfPresent(node *yaml.MapNode) (*yaml.RNode, *openapi.ResourceSchema) {
|
||||
if node == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// parse the schema for the field if present
|
||||
var s *openapi.ResourceSchema
|
||||
fm := fieldmeta.FieldMeta{}
|
||||
var err error
|
||||
// check the value for a schema
|
||||
if err = fm.Read(node.Value); err == nil {
|
||||
s = &openapi.ResourceSchema{Schema: &fm.Schema}
|
||||
if fm.Schema.Ref.String() != "" {
|
||||
r, err := openapi.Resolve(&fm.Schema.Ref, openapi.Schema())
|
||||
if err == nil && r != nil {
|
||||
s.Schema = r
|
||||
}
|
||||
}
|
||||
}
|
||||
// check the key for a schema -- this will be used
|
||||
// when the value is a Sequence (comments are attached)
|
||||
// to the key
|
||||
if fm.IsEmpty() {
|
||||
if err = fm.Read(node.Key); err == nil {
|
||||
s = &openapi.ResourceSchema{Schema: &fm.Schema}
|
||||
}
|
||||
if fm.Schema.Ref.String() != "" {
|
||||
r, err := openapi.Resolve(&fm.Schema.Ref, openapi.Schema())
|
||||
if err == nil && r != nil {
|
||||
s.Schema = r
|
||||
}
|
||||
}
|
||||
}
|
||||
return node.Value, s
|
||||
}
|
||||
|
||||
// fieldNames returns a sorted slice containing the names of all fields that appear in any of
|
||||
// the sources
|
||||
func (l Walker) fieldNames() []string {
|
||||
fields := sets.String{}
|
||||
for _, s := range l.Sources {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
// don't check error, we know this is a mapping node
|
||||
sFields, _ := s.Fields()
|
||||
fields.Insert(sFields...)
|
||||
}
|
||||
result := fields.List()
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// fieldValue returns a slice containing each source's value for fieldName, the
|
||||
// schema, and a map of each source's value to the style for the source's key.
|
||||
func (l Walker) fieldValue(fieldName string) ([]*yaml.RNode, *openapi.ResourceSchema, map[*yaml.RNode]yaml.Style) {
|
||||
var fields []*yaml.RNode
|
||||
var sch *openapi.ResourceSchema
|
||||
keyStyles := make(map[*yaml.RNode]yaml.Style, len(l.Sources))
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] == nil {
|
||||
fields = append(fields, nil)
|
||||
continue
|
||||
}
|
||||
field := l.Sources[i].Field(fieldName)
|
||||
f, s := l.valueIfPresent(field)
|
||||
fields = append(fields, f)
|
||||
if field != nil && field.Key != nil && field.Key.YNode() != nil {
|
||||
keyStyles[f] = field.Key.YNode().Style
|
||||
}
|
||||
if sch == nil && !s.IsMissingOrNull() {
|
||||
sch = s
|
||||
}
|
||||
}
|
||||
return fields, sch, keyStyles
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// walkNonAssociativeSequence returns the value of VisitList
|
||||
func (l Walker) walkNonAssociativeSequence() (*yaml.RNode, error) {
|
||||
return l.VisitList(l.Sources, l.Schema, NonAssociateList)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
|
||||
// walkScalar returns the value of VisitScalar
|
||||
func (l Walker) walkScalar() (*yaml.RNode, error) {
|
||||
return l.VisitScalar(l.Sources, l.Schema)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type ListKind int32
|
||||
|
||||
const (
|
||||
AssociativeList ListKind = 1 + iota
|
||||
NonAssociateList
|
||||
)
|
||||
|
||||
// Visitor is invoked by walk with source and destination node pairs
|
||||
type Visitor interface {
|
||||
VisitMap(Sources, *openapi.ResourceSchema) (*yaml.RNode, error)
|
||||
|
||||
VisitScalar(Sources, *openapi.ResourceSchema) (*yaml.RNode, error)
|
||||
|
||||
VisitList(Sources, *openapi.ResourceSchema, ListKind) (*yaml.RNode, error)
|
||||
}
|
||||
|
||||
// ClearNode is returned if GrepFilter should do nothing after calling Set
|
||||
var ClearNode *yaml.RNode
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/schema"
|
||||
)
|
||||
|
||||
// Walker walks the Source RNode and modifies the RNode provided to GrepFilter.
|
||||
type Walker struct {
|
||||
// Visitor is invoked by GrepFilter
|
||||
Visitor
|
||||
|
||||
Schema *openapi.ResourceSchema
|
||||
|
||||
// Source is the RNode to walk. All Source fields and associative list elements
|
||||
// will be visited.
|
||||
Sources Sources
|
||||
|
||||
// Path is the field path to the current Source Node.
|
||||
Path []string
|
||||
|
||||
// InferAssociativeLists if set to true will infer merge strategies for
|
||||
// fields which it doesn't have the schema based on the fields in the
|
||||
// list elements.
|
||||
InferAssociativeLists bool
|
||||
|
||||
// VisitKeysAsScalars if true will call VisitScalar on map entry keys,
|
||||
// providing nil as the OpenAPI schema.
|
||||
VisitKeysAsScalars bool
|
||||
|
||||
// MergeOptions is a struct to store options for merge
|
||||
MergeOptions yaml.MergeOptions
|
||||
}
|
||||
|
||||
// Kind returns the kind of the first non-null node in Sources.
|
||||
func (l Walker) Kind() yaml.Kind {
|
||||
for _, s := range l.Sources {
|
||||
if !yaml.IsMissingOrNull(s) {
|
||||
return s.YNode().Kind
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Walk will recursively traverse every item in the Sources and perform corresponding
|
||||
// actions on them
|
||||
func (l Walker) Walk() (*yaml.RNode, error) {
|
||||
l.Schema = l.GetSchema()
|
||||
|
||||
// invoke the handler for the corresponding node type
|
||||
switch l.Kind() {
|
||||
case yaml.MappingNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.MappingNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.walkMap()
|
||||
case yaml.SequenceNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.SequenceNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// AssociativeSequence means the items in the sequence are associative. They can be merged
|
||||
// according to merge key.
|
||||
if schema.IsAssociative(l.Schema, l.Sources, l.InferAssociativeLists) {
|
||||
return l.walkAssociativeSequence()
|
||||
}
|
||||
return l.walkNonAssociativeSequence()
|
||||
|
||||
case yaml.ScalarNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.ScalarNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.walkScalar()
|
||||
case 0:
|
||||
// walk empty nodes as maps
|
||||
return l.walkMap()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l Walker) GetSchema() *openapi.ResourceSchema {
|
||||
for i := range l.Sources {
|
||||
r := l.Sources[i]
|
||||
if yaml.IsMissingOrNull(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
fm := fieldmeta.FieldMeta{}
|
||||
if err := fm.Read(r); err == nil && !fm.IsEmpty() {
|
||||
// per-field schema, this is fine
|
||||
if fm.Schema.Ref.String() != "" {
|
||||
// resolve the reference
|
||||
s, err := openapi.Resolve(&fm.Schema.Ref, openapi.Schema())
|
||||
if err == nil && s != nil {
|
||||
fm.Schema = *s
|
||||
}
|
||||
}
|
||||
return &openapi.ResourceSchema{Schema: &fm.Schema}
|
||||
}
|
||||
}
|
||||
|
||||
if l.Schema != nil {
|
||||
return l.Schema
|
||||
}
|
||||
for i := range l.Sources {
|
||||
r := l.Sources[i]
|
||||
if yaml.IsMissingOrNull(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
m, _ := r.GetMeta()
|
||||
if m.Kind == "" || m.APIVersion == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
s := openapi.SchemaForResourceType(yaml.TypeMeta{Kind: m.Kind, APIVersion: m.APIVersion})
|
||||
if s != nil {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
DestIndex = iota
|
||||
OriginIndex
|
||||
UpdatedIndex
|
||||
)
|
||||
|
||||
// Sources are a list of RNodes. First item is the dest node, followed by
|
||||
// multiple source nodes.
|
||||
type Sources []*yaml.RNode
|
||||
|
||||
// Dest returns the destination node
|
||||
func (s Sources) Dest() *yaml.RNode {
|
||||
if len(s) <= DestIndex {
|
||||
return nil
|
||||
}
|
||||
return s[DestIndex]
|
||||
}
|
||||
|
||||
// Origin returns the origin node
|
||||
func (s Sources) Origin() *yaml.RNode {
|
||||
if len(s) <= OriginIndex {
|
||||
return nil
|
||||
}
|
||||
return s[OriginIndex]
|
||||
}
|
||||
|
||||
// Updated returns the updated node
|
||||
func (s Sources) Updated() *yaml.RNode {
|
||||
if len(s) <= UpdatedIndex {
|
||||
return nil
|
||||
}
|
||||
return s[UpdatedIndex]
|
||||
}
|
||||
|
||||
func (s Sources) String() string {
|
||||
var values []string
|
||||
for i := range s {
|
||||
str, err := s[i].String()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
}
|
||||
values = append(values, str)
|
||||
}
|
||||
return strings.Join(values, "\n")
|
||||
}
|
||||
|
||||
// setDestNode sets the destination source node
|
||||
func (s Sources) setDestNode(node *yaml.RNode, err error) (*yaml.RNode, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s[0] = node
|
||||
return node, nil
|
||||
}
|
||||
Reference in New Issue
Block a user