From c7ad227b6a2835369f771debb6d80e90702f5f41 Mon Sep 17 00:00:00 2001 From: Tamal Saha Date: Sat, 31 Mar 2018 13:40:18 -0700 Subject: [PATCH] Add CRD Validation (#962) ref: https://kubernetes.io/docs/tasks/access-kubernetes-api/extend-api-custom-resource-definitions/#validation --- apis/voyager/v1beta1/crd.go | 2 - apis/voyager/v1beta1/crds.yaml | 6 +- glide.lock | 8 +- .../kutil/apiextensions/v1beta1/cli-utils.go | 3 - .../apiextensions/v1beta1/convert_types.go | 63 ++- vendor/github.com/pkg/errors/stack.go | 51 ++- vendor/gopkg.in/ini.v1/LICENSE | 2 +- vendor/gopkg.in/ini.v1/file.go | 403 ------------------ vendor/gopkg.in/ini.v1/ini.go | 384 ++++++++++++++++- vendor/gopkg.in/ini.v1/key.go | 64 +-- vendor/gopkg.in/ini.v1/parser.go | 50 +-- vendor/gopkg.in/ini.v1/section.go | 9 - vendor/gopkg.in/ini.v1/struct.go | 16 +- 13 files changed, 467 insertions(+), 594 deletions(-) delete mode 100644 vendor/gopkg.in/ini.v1/file.go diff --git a/apis/voyager/v1beta1/crd.go b/apis/voyager/v1beta1/crd.go index 59c264aa0..556b9f7b5 100644 --- a/apis/voyager/v1beta1/crd.go +++ b/apis/voyager/v1beta1/crd.go @@ -12,7 +12,6 @@ func (r Ingress) CustomResourceDefinition() *apiextensions.CustomResourceDefinit Plural: ResourcePluralIngress, Singular: ResourceSingularIngress, Kind: ResourceKindIngress, - ListKind: ResourceKindIngress + "List", ShortNames: []string{"ing"}, ResourceScope: string(apiextensions.NamespaceScoped), Labels: crdutils.Labels{ @@ -31,7 +30,6 @@ func (c Certificate) CustomResourceDefinition() *apiextensions.CustomResourceDef Plural: ResourcePluralCertificate, Singular: ResourceSingularCertificate, Kind: ResourceKindCertificate, - ListKind: ResourceKindCertificate + "List", ShortNames: []string{"cert"}, ResourceScope: string(apiextensions.NamespaceScoped), Labels: crdutils.Labels{ diff --git a/apis/voyager/v1beta1/crds.yaml b/apis/voyager/v1beta1/crds.yaml index 0b618052f..b2c994a7c 100644 --- a/apis/voyager/v1beta1/crds.yaml +++ b/apis/voyager/v1beta1/crds.yaml @@ -10,7 +10,6 @@ spec: group: voyager.appscode.com names: kind: Ingress - listKind: IngressList plural: ingresses shortNames: - ing @@ -109,6 +108,8 @@ spec: errorPage: type: string headers: + additionalProperties: + type: string type: object secretName: type: string @@ -139,6 +140,8 @@ spec: type: string type: array nodeSelector: + additionalProperties: + type: string description: 'NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node''s labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' @@ -376,7 +379,6 @@ spec: group: voyager.appscode.com names: kind: Certificate - listKind: CertificateList plural: certificates shortNames: - cert diff --git a/glide.lock b/glide.lock index bf9cce498..7055200bd 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ hash: 1596963029d94145703f740f040faad597b7cc93be4e9933cb70a743c717b579 -updated: 2018-03-31T13:35:04.621496894-07:00 +updated: 2018-03-31T23:59:42.913219771-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -40,7 +40,7 @@ imports: - admission/v1beta1 - registry/admissionreview/v1beta1 - name: github.com/appscode/kutil - version: d05ce0e9bbbec24d6f6f1fdce4a0a7c65ff221e5 + version: eeda8988aeb7981d4c6bd685538411b4eb87db1d subpackages: - apiextensions/v1beta1 - apps/v1beta1 @@ -375,7 +375,7 @@ imports: - name: github.com/pires/go-proxyproto version: 2f38359974ffed92842a86ffcc17001b3f525e34 - name: github.com/pkg/errors - version: 816c9085562cd7ee03e7f8188a1cfd942858cded + version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/prometheus/client_golang version: c5b7fccd204277076155f10851dad72b76a49317 subpackages: @@ -581,7 +581,7 @@ imports: - name: gopkg.in/inf.v0 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 - name: gopkg.in/ini.v1 - version: 6333e38ac20b8949a8dd68baa3650f4dee8f39f0 + version: 20b96f641a5ea98f2f8619ff4f3e061cff4833bd - name: gopkg.in/natefinch/lumberjack.v2 version: 20b71e5b60d756d3d2f80def009790325acc2b23 - name: gopkg.in/square/go-jose.v1 diff --git a/vendor/github.com/appscode/kutil/apiextensions/v1beta1/cli-utils.go b/vendor/github.com/appscode/kutil/apiextensions/v1beta1/cli-utils.go index c7b109843..cc636c02c 100644 --- a/vendor/github.com/appscode/kutil/apiextensions/v1beta1/cli-utils.go +++ b/vendor/github.com/appscode/kutil/apiextensions/v1beta1/cli-utils.go @@ -40,7 +40,6 @@ type Config struct { Plural string Singular string ShortNames []string - ListKind string GetOpenAPIDefinitions GetAPIDefinitions } @@ -102,7 +101,6 @@ func NewCustomResourceDefinition(config Config) *extensionsobj.CustomResourceDef Singular: config.Singular, Kind: config.Kind, ShortNames: config.ShortNames, - ListKind: config.ListKind, }, }, } @@ -146,6 +144,5 @@ func InitFlags(cfg *Config, fs *pflag.FlagSet) *pflag.FlagSet { fs.StringVar(&cfg.Plural, "plural", "", "CRD plural name") fs.StringVar(&cfg.Singular, "singular", "", "CRD singular name") fs.StringSliceVar(&cfg.ShortNames, "short-names", nil, "CRD short names") - fs.StringVar(&cfg.ListKind, "list-kind", "", "CRD list kind") return fs } diff --git a/vendor/github.com/appscode/kutil/apiextensions/v1beta1/convert_types.go b/vendor/github.com/appscode/kutil/apiextensions/v1beta1/convert_types.go index 4f5106c79..25d411e63 100644 --- a/vendor/github.com/appscode/kutil/apiextensions/v1beta1/convert_types.go +++ b/vendor/github.com/appscode/kutil/apiextensions/v1beta1/convert_types.go @@ -90,38 +90,37 @@ func SchemaPropsToJSONProps(schema *spec.Schema, openapiSpec map[string]common.O } props = &extensionsobj.JSONSchemaProps{ - Ref: ref, - ID: schemaProps.ID, - Schema: extensionsobj.JSONSchemaURL(string(schema.Schema)), - Description: schemaProps.Description, - Type: StringOrArrayToString(schemaProps.Type), - Format: schemaProps.Format, - Title: schemaProps.Title, - Maximum: schemaProps.Maximum, - ExclusiveMaximum: schemaProps.ExclusiveMaximum, - Minimum: schemaProps.Minimum, - ExclusiveMinimum: schemaProps.ExclusiveMinimum, - MaxLength: schemaProps.MaxLength, - MinLength: schemaProps.MinLength, - Pattern: schemaProps.Pattern, - MaxItems: schemaProps.MaxItems, - MinItems: schemaProps.MinItems, - UniqueItems: schemaProps.UniqueItems, - MultipleOf: schemaProps.MultipleOf, - Enum: EnumJSON(schemaProps.Enum), - MaxProperties: schemaProps.MaxProperties, - MinProperties: schemaProps.MinProperties, - Required: schemaProps.Required, - Items: SchemaOrArrayToJSONItems(schemaProps.Items, openapiSpec, nested), - AllOf: SchemaPropsToJSONPropsArray(schemaProps.AllOf, openapiSpec, nested), - OneOf: SchemaPropsToJSONPropsArray(schemaProps.OneOf, openapiSpec, nested), - AnyOf: SchemaPropsToJSONPropsArray(schemaProps.AnyOf, openapiSpec, nested), - Not: SchemaPropsToJSONProps(schemaProps.Not, openapiSpec, nested), - Properties: SchemPropsMapToJSONMap(schemaProps.Properties, openapiSpec, nested), - // @TODO(01-25-2018) Field not accepted by the current CRD Validation Spec - // AdditionalProperties: SchemaOrBoolToJSONProps(schemaProps.AdditionalProperties, openapiSpec, nested), - PatternProperties: SchemPropsMapToJSONMap(schemaProps.PatternProperties, openapiSpec, nested), - AdditionalItems: SchemaOrBoolToJSONProps(schemaProps.AdditionalItems, openapiSpec, nested), + Ref: ref, + ID: schemaProps.ID, + Schema: extensionsobj.JSONSchemaURL(string(schema.Schema)), + Description: schemaProps.Description, + Type: StringOrArrayToString(schemaProps.Type), + Format: schemaProps.Format, + Title: schemaProps.Title, + Maximum: schemaProps.Maximum, + ExclusiveMaximum: schemaProps.ExclusiveMaximum, + Minimum: schemaProps.Minimum, + ExclusiveMinimum: schemaProps.ExclusiveMinimum, + MaxLength: schemaProps.MaxLength, + MinLength: schemaProps.MinLength, + Pattern: schemaProps.Pattern, + MaxItems: schemaProps.MaxItems, + MinItems: schemaProps.MinItems, + UniqueItems: schemaProps.UniqueItems, + MultipleOf: schemaProps.MultipleOf, + Enum: EnumJSON(schemaProps.Enum), + MaxProperties: schemaProps.MaxProperties, + MinProperties: schemaProps.MinProperties, + Required: schemaProps.Required, + Items: SchemaOrArrayToJSONItems(schemaProps.Items, openapiSpec, nested), + AllOf: SchemaPropsToJSONPropsArray(schemaProps.AllOf, openapiSpec, nested), + OneOf: SchemaPropsToJSONPropsArray(schemaProps.OneOf, openapiSpec, nested), + AnyOf: SchemaPropsToJSONPropsArray(schemaProps.AnyOf, openapiSpec, nested), + Not: SchemaPropsToJSONProps(schemaProps.Not, openapiSpec, nested), + Properties: SchemPropsMapToJSONMap(schemaProps.Properties, openapiSpec, nested), + AdditionalProperties: SchemaOrBoolToJSONProps(schemaProps.AdditionalProperties, openapiSpec, nested), + PatternProperties: SchemPropsMapToJSONMap(schemaProps.PatternProperties, openapiSpec, nested), + AdditionalItems: SchemaOrBoolToJSONProps(schemaProps.AdditionalItems, openapiSpec, nested), } return props } diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go index 2874a048c..6b1f2891a 100644 --- a/vendor/github.com/pkg/errors/stack.go +++ b/vendor/github.com/pkg/errors/stack.go @@ -46,8 +46,7 @@ func (f Frame) line() int { // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) +// %+s path of source file relative to the compile time GOPATH // %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { switch verb { @@ -80,14 +79,6 @@ func (f Frame) Format(s fmt.State, verb rune) { // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). type StackTrace []Frame -// Format formats the stack of Frames according to the fmt.Formatter interface. -// -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack -// -// Format accepts flags that alter the printing of some verbs, as follows: -// -// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -145,3 +136,43 @@ func funcname(name string) string { i = strings.Index(name, ".") return name[i+1:] } + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/gopkg.in/ini.v1/LICENSE b/vendor/gopkg.in/ini.v1/LICENSE index d361bbcdf..37ec93a14 100644 --- a/vendor/gopkg.in/ini.v1/LICENSE +++ b/vendor/gopkg.in/ini.v1/LICENSE @@ -176,7 +176,7 @@ 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 2014 Unknwon + 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. diff --git a/vendor/gopkg.in/ini.v1/file.go b/vendor/gopkg.in/ini.v1/file.go deleted file mode 100644 index ae6264acf..000000000 --- a/vendor/gopkg.in/ini.v1/file.go +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright 2017 Unknwon -// -// 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 ini - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "strings" - "sync" -) - -// File represents a combination of a or more INI file(s) in memory. -type File struct { - options LoadOptions - dataSources []dataSource - - // Should make things safe, but sometimes doesn't matter. - BlockMode bool - lock sync.RWMutex - - // To keep data in order. - sectionList []string - // Actual data is stored here. - sections map[string]*Section - - NameMapper - ValueMapper -} - -// newFile initializes File object with given data sources. -func newFile(dataSources []dataSource, opts LoadOptions) *File { - return &File{ - BlockMode: true, - dataSources: dataSources, - sections: make(map[string]*Section), - sectionList: make([]string, 0, 10), - options: opts, - } -} - -// Empty returns an empty file object. -func Empty() *File { - // Ignore error here, we sure our data is good. - f, _ := Load([]byte("")) - return f -} - -// NewSection creates a new section. -func (f *File) NewSection(name string) (*Section, error) { - if len(name) == 0 { - return nil, errors.New("error creating new section: empty section name") - } else if f.options.Insensitive && name != DEFAULT_SECTION { - name = strings.ToLower(name) - } - - if f.BlockMode { - f.lock.Lock() - defer f.lock.Unlock() - } - - if inSlice(name, f.sectionList) { - return f.sections[name], nil - } - - f.sectionList = append(f.sectionList, name) - f.sections[name] = newSection(f, name) - return f.sections[name], nil -} - -// NewRawSection creates a new section with an unparseable body. -func (f *File) NewRawSection(name, body string) (*Section, error) { - section, err := f.NewSection(name) - if err != nil { - return nil, err - } - - section.isRawSection = true - section.rawBody = body - return section, nil -} - -// NewSections creates a list of sections. -func (f *File) NewSections(names ...string) (err error) { - for _, name := range names { - if _, err = f.NewSection(name); err != nil { - return err - } - } - return nil -} - -// GetSection returns section by given name. -func (f *File) GetSection(name string) (*Section, error) { - if len(name) == 0 { - name = DEFAULT_SECTION - } - if f.options.Insensitive { - name = strings.ToLower(name) - } - - if f.BlockMode { - f.lock.RLock() - defer f.lock.RUnlock() - } - - sec := f.sections[name] - if sec == nil { - return nil, fmt.Errorf("section '%s' does not exist", name) - } - return sec, nil -} - -// Section assumes named section exists and returns a zero-value when not. -func (f *File) Section(name string) *Section { - sec, err := f.GetSection(name) - if err != nil { - // Note: It's OK here because the only possible error is empty section name, - // but if it's empty, this piece of code won't be executed. - sec, _ = f.NewSection(name) - return sec - } - return sec -} - -// Section returns list of Section. -func (f *File) Sections() []*Section { - if f.BlockMode { - f.lock.RLock() - defer f.lock.RUnlock() - } - - sections := make([]*Section, len(f.sectionList)) - for i, name := range f.sectionList { - sections[i] = f.sections[name] - } - return sections -} - -// ChildSections returns a list of child sections of given section name. -func (f *File) ChildSections(name string) []*Section { - return f.Section(name).ChildSections() -} - -// SectionStrings returns list of section names. -func (f *File) SectionStrings() []string { - list := make([]string, len(f.sectionList)) - copy(list, f.sectionList) - return list -} - -// DeleteSection deletes a section. -func (f *File) DeleteSection(name string) { - if f.BlockMode { - f.lock.Lock() - defer f.lock.Unlock() - } - - if len(name) == 0 { - name = DEFAULT_SECTION - } - - for i, s := range f.sectionList { - if s == name { - f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) - delete(f.sections, name) - return - } - } -} - -func (f *File) reload(s dataSource) error { - r, err := s.ReadCloser() - if err != nil { - return err - } - defer r.Close() - - return f.parse(r) -} - -// Reload reloads and parses all data sources. -func (f *File) Reload() (err error) { - for _, s := range f.dataSources { - if err = f.reload(s); err != nil { - // In loose mode, we create an empty default section for nonexistent files. - if os.IsNotExist(err) && f.options.Loose { - f.parse(bytes.NewBuffer(nil)) - continue - } - return err - } - } - return nil -} - -// Append appends one or more data sources and reloads automatically. -func (f *File) Append(source interface{}, others ...interface{}) error { - ds, err := parseDataSource(source) - if err != nil { - return err - } - f.dataSources = append(f.dataSources, ds) - for _, s := range others { - ds, err = parseDataSource(s) - if err != nil { - return err - } - f.dataSources = append(f.dataSources, ds) - } - return f.Reload() -} - -func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { - equalSign := "=" - if PrettyFormat { - equalSign = " = " - } - - // Use buffer to make sure target is safe until finish encoding. - buf := bytes.NewBuffer(nil) - for i, sname := range f.sectionList { - sec := f.Section(sname) - if len(sec.Comment) > 0 { - if sec.Comment[0] != '#' && sec.Comment[0] != ';' { - sec.Comment = "; " + sec.Comment - } else { - sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:]) - } - if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil { - return nil, err - } - } - - if i > 0 || DefaultHeader { - if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { - return nil, err - } - } else { - // Write nothing if default section is empty - if len(sec.keyList) == 0 { - continue - } - } - - if sec.isRawSection { - if _, err := buf.WriteString(sec.rawBody); err != nil { - return nil, err - } - - if PrettySection { - // Put a line between sections - if _, err := buf.WriteString(LineBreak); err != nil { - return nil, err - } - } - continue - } - - // Count and generate alignment length and buffer spaces using the - // longest key. Keys may be modifed if they contain certain characters so - // we need to take that into account in our calculation. - alignLength := 0 - if PrettyFormat { - for _, kname := range sec.keyList { - keyLength := len(kname) - // First case will surround key by ` and second by """ - if strings.ContainsAny(kname, "\"=:") { - keyLength += 2 - } else if strings.Contains(kname, "`") { - keyLength += 6 - } - - if keyLength > alignLength { - alignLength = keyLength - } - } - } - alignSpaces := bytes.Repeat([]byte(" "), alignLength) - - KEY_LIST: - for _, kname := range sec.keyList { - key := sec.Key(kname) - if len(key.Comment) > 0 { - if len(indent) > 0 && sname != DEFAULT_SECTION { - buf.WriteString(indent) - } - if key.Comment[0] != '#' && key.Comment[0] != ';' { - key.Comment = "; " + key.Comment - } else { - key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:]) - } - if _, err := buf.WriteString(key.Comment + LineBreak); err != nil { - return nil, err - } - } - - if len(indent) > 0 && sname != DEFAULT_SECTION { - buf.WriteString(indent) - } - - switch { - case key.isAutoIncrement: - kname = "-" - case strings.ContainsAny(kname, "\"=:"): - kname = "`" + kname + "`" - case strings.Contains(kname, "`"): - kname = `"""` + kname + `"""` - } - - for _, val := range key.ValueWithShadows() { - if _, err := buf.WriteString(kname); err != nil { - return nil, err - } - - if key.isBooleanType { - if kname != sec.keyList[len(sec.keyList)-1] { - buf.WriteString(LineBreak) - } - continue KEY_LIST - } - - // Write out alignment spaces before "=" sign - if PrettyFormat { - buf.Write(alignSpaces[:alignLength-len(kname)]) - } - - // In case key value contains "\n", "`", "\"", "#" or ";" - if strings.ContainsAny(val, "\n`") { - val = `"""` + val + `"""` - } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { - val = "`" + val + "`" - } - if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { - return nil, err - } - } - - for _, val := range key.nestedValues { - if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil { - return nil, err - } - } - } - - if PrettySection { - // Put a line between sections - if _, err := buf.WriteString(LineBreak); err != nil { - return nil, err - } - } - } - - return buf, nil -} - -// WriteToIndent writes content into io.Writer with given indention. -// If PrettyFormat has been set to be true, -// it will align "=" sign with spaces under each section. -func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) { - buf, err := f.writeToBuffer(indent) - if err != nil { - return 0, err - } - return buf.WriteTo(w) -} - -// WriteTo writes file content into io.Writer. -func (f *File) WriteTo(w io.Writer) (int64, error) { - return f.WriteToIndent(w, "") -} - -// SaveToIndent writes content to file system with given value indention. -func (f *File) SaveToIndent(filename, indent string) error { - // Note: Because we are truncating with os.Create, - // so it's safer to save to a temporary file location and rename afte done. - buf, err := f.writeToBuffer(indent) - if err != nil { - return err - } - - return ioutil.WriteFile(filename, buf.Bytes(), 0666) -} - -// SaveTo writes content to file system. -func (f *File) SaveTo(filename string) error { - return f.SaveToIndent(filename, "") -} diff --git a/vendor/gopkg.in/ini.v1/ini.go b/vendor/gopkg.in/ini.v1/ini.go index 9f6ea3b41..7f3c4d1ed 100644 --- a/vendor/gopkg.in/ini.v1/ini.go +++ b/vendor/gopkg.in/ini.v1/ini.go @@ -17,12 +17,15 @@ package ini import ( "bytes" + "errors" "fmt" "io" "io/ioutil" "os" "regexp" "runtime" + "strings" + "sync" ) const ( @@ -32,7 +35,7 @@ const ( // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 - _VERSION = "1.33.0" + _VERSION = "1.28.2" ) // Version returns current package version literal. @@ -89,6 +92,18 @@ func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) { return os.Open(s.name) } +type bytesReadCloser struct { + reader io.Reader +} + +func (rc *bytesReadCloser) Read(p []byte) (n int, err error) { + return rc.reader.Read(p) +} + +func (rc *bytesReadCloser) Close() error { + return nil +} + // sourceData represents an object that contains content in memory. type sourceData struct { data []byte @@ -107,6 +122,38 @@ func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) { return s.reader, nil } +// File represents a combination of a or more INI file(s) in memory. +type File struct { + // Should make things safe, but sometimes doesn't matter. + BlockMode bool + // Make sure data is safe in multiple goroutines. + lock sync.RWMutex + + // Allow combination of multiple data sources. + dataSources []dataSource + // Actual data is stored here. + sections map[string]*Section + + // To keep data in order. + sectionList []string + + options LoadOptions + + NameMapper + ValueMapper +} + +// newFile initializes File object with given data sources. +func newFile(dataSources []dataSource, opts LoadOptions) *File { + return &File{ + BlockMode: true, + dataSources: dataSources, + sections: make(map[string]*Section), + sectionList: make([]string, 0, 10), + options: opts, + } +} + func parseDataSource(source interface{}) (dataSource, error) { switch s := source.(type) { case string: @@ -134,16 +181,6 @@ type LoadOptions struct { AllowBooleanKeys bool // AllowShadows indicates whether to keep track of keys with same name under same section. AllowShadows bool - // AllowNestedValues indicates whether to allow AWS-like nested values. - // Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values - AllowNestedValues bool - // UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format - // when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value" - UnescapeValueDoubleQuotes bool - // UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format - // when value is NOT surrounded by any quotes. - // Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all. - UnescapeValueCommentSymbols bool // Some INI formats allow group blocks that store a block of raw content that doesn't otherwise // conform to key/value pairs. Specify the names of those blocks here. UnparseableSections []string @@ -192,3 +229,328 @@ func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{AllowShadows: true}, source, others...) } + +// Empty returns an empty file object. +func Empty() *File { + // Ignore error here, we sure our data is good. + f, _ := Load([]byte("")) + return f +} + +// NewSection creates a new section. +func (f *File) NewSection(name string) (*Section, error) { + if len(name) == 0 { + return nil, errors.New("error creating new section: empty section name") + } else if f.options.Insensitive && name != DEFAULT_SECTION { + name = strings.ToLower(name) + } + + if f.BlockMode { + f.lock.Lock() + defer f.lock.Unlock() + } + + if inSlice(name, f.sectionList) { + return f.sections[name], nil + } + + f.sectionList = append(f.sectionList, name) + f.sections[name] = newSection(f, name) + return f.sections[name], nil +} + +// NewRawSection creates a new section with an unparseable body. +func (f *File) NewRawSection(name, body string) (*Section, error) { + section, err := f.NewSection(name) + if err != nil { + return nil, err + } + + section.isRawSection = true + section.rawBody = body + return section, nil +} + +// NewSections creates a list of sections. +func (f *File) NewSections(names ...string) (err error) { + for _, name := range names { + if _, err = f.NewSection(name); err != nil { + return err + } + } + return nil +} + +// GetSection returns section by given name. +func (f *File) GetSection(name string) (*Section, error) { + if len(name) == 0 { + name = DEFAULT_SECTION + } else if f.options.Insensitive { + name = strings.ToLower(name) + } + + if f.BlockMode { + f.lock.RLock() + defer f.lock.RUnlock() + } + + sec := f.sections[name] + if sec == nil { + return nil, fmt.Errorf("section '%s' does not exist", name) + } + return sec, nil +} + +// Section assumes named section exists and returns a zero-value when not. +func (f *File) Section(name string) *Section { + sec, err := f.GetSection(name) + if err != nil { + // Note: It's OK here because the only possible error is empty section name, + // but if it's empty, this piece of code won't be executed. + sec, _ = f.NewSection(name) + return sec + } + return sec +} + +// Section returns list of Section. +func (f *File) Sections() []*Section { + sections := make([]*Section, len(f.sectionList)) + for i := range f.sectionList { + sections[i] = f.Section(f.sectionList[i]) + } + return sections +} + +// ChildSections returns a list of child sections of given section name. +func (f *File) ChildSections(name string) []*Section { + return f.Section(name).ChildSections() +} + +// SectionStrings returns list of section names. +func (f *File) SectionStrings() []string { + list := make([]string, len(f.sectionList)) + copy(list, f.sectionList) + return list +} + +// DeleteSection deletes a section. +func (f *File) DeleteSection(name string) { + if f.BlockMode { + f.lock.Lock() + defer f.lock.Unlock() + } + + if len(name) == 0 { + name = DEFAULT_SECTION + } + + for i, s := range f.sectionList { + if s == name { + f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...) + delete(f.sections, name) + return + } + } +} + +func (f *File) reload(s dataSource) error { + r, err := s.ReadCloser() + if err != nil { + return err + } + defer r.Close() + + return f.parse(r) +} + +// Reload reloads and parses all data sources. +func (f *File) Reload() (err error) { + for _, s := range f.dataSources { + if err = f.reload(s); err != nil { + // In loose mode, we create an empty default section for nonexistent files. + if os.IsNotExist(err) && f.options.Loose { + f.parse(bytes.NewBuffer(nil)) + continue + } + return err + } + } + return nil +} + +// Append appends one or more data sources and reloads automatically. +func (f *File) Append(source interface{}, others ...interface{}) error { + ds, err := parseDataSource(source) + if err != nil { + return err + } + f.dataSources = append(f.dataSources, ds) + for _, s := range others { + ds, err = parseDataSource(s) + if err != nil { + return err + } + f.dataSources = append(f.dataSources, ds) + } + return f.Reload() +} + +func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) { + equalSign := "=" + if PrettyFormat { + equalSign = " = " + } + + // Use buffer to make sure target is safe until finish encoding. + buf := bytes.NewBuffer(nil) + for i, sname := range f.sectionList { + sec := f.Section(sname) + if len(sec.Comment) > 0 { + if sec.Comment[0] != '#' && sec.Comment[0] != ';' { + sec.Comment = "; " + sec.Comment + } + if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil { + return nil, err + } + } + + if i > 0 || DefaultHeader { + if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil { + return nil, err + } + } else { + // Write nothing if default section is empty + if len(sec.keyList) == 0 { + continue + } + } + + if sec.isRawSection { + if _, err := buf.WriteString(sec.rawBody); err != nil { + return nil, err + } + continue + } + + // Count and generate alignment length and buffer spaces using the + // longest key. Keys may be modifed if they contain certain characters so + // we need to take that into account in our calculation. + alignLength := 0 + if PrettyFormat { + for _, kname := range sec.keyList { + keyLength := len(kname) + // First case will surround key by ` and second by """ + if strings.ContainsAny(kname, "\"=:") { + keyLength += 2 + } else if strings.Contains(kname, "`") { + keyLength += 6 + } + + if keyLength > alignLength { + alignLength = keyLength + } + } + } + alignSpaces := bytes.Repeat([]byte(" "), alignLength) + + KEY_LIST: + for _, kname := range sec.keyList { + key := sec.Key(kname) + if len(key.Comment) > 0 { + if len(indent) > 0 && sname != DEFAULT_SECTION { + buf.WriteString(indent) + } + if key.Comment[0] != '#' && key.Comment[0] != ';' { + key.Comment = "; " + key.Comment + } + if _, err := buf.WriteString(key.Comment + LineBreak); err != nil { + return nil, err + } + } + + if len(indent) > 0 && sname != DEFAULT_SECTION { + buf.WriteString(indent) + } + + switch { + case key.isAutoIncrement: + kname = "-" + case strings.ContainsAny(kname, "\"=:"): + kname = "`" + kname + "`" + case strings.Contains(kname, "`"): + kname = `"""` + kname + `"""` + } + + for _, val := range key.ValueWithShadows() { + if _, err := buf.WriteString(kname); err != nil { + return nil, err + } + + if key.isBooleanType { + if kname != sec.keyList[len(sec.keyList)-1] { + buf.WriteString(LineBreak) + } + continue KEY_LIST + } + + // Write out alignment spaces before "=" sign + if PrettyFormat { + buf.Write(alignSpaces[:alignLength-len(kname)]) + } + + // In case key value contains "\n", "`", "\"", "#" or ";" + if strings.ContainsAny(val, "\n`") { + val = `"""` + val + `"""` + } else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") { + val = "`" + val + "`" + } + if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil { + return nil, err + } + } + } + + if PrettySection { + // Put a line between sections + if _, err := buf.WriteString(LineBreak); err != nil { + return nil, err + } + } + } + + return buf, nil +} + +// WriteToIndent writes content into io.Writer with given indention. +// If PrettyFormat has been set to be true, +// it will align "=" sign with spaces under each section. +func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) { + buf, err := f.writeToBuffer(indent) + if err != nil { + return 0, err + } + return buf.WriteTo(w) +} + +// WriteTo writes file content into io.Writer. +func (f *File) WriteTo(w io.Writer) (int64, error) { + return f.WriteToIndent(w, "") +} + +// SaveToIndent writes content to file system with given value indention. +func (f *File) SaveToIndent(filename, indent string) error { + // Note: Because we are truncating with os.Create, + // so it's safer to save to a temporary file location and rename afte done. + buf, err := f.writeToBuffer(indent) + if err != nil { + return err + } + + return ioutil.WriteFile(filename, buf.Bytes(), 0666) +} + +// SaveTo writes content to file system. +func (f *File) SaveTo(filename string) error { + return f.SaveToIndent(filename, "") +} diff --git a/vendor/gopkg.in/ini.v1/key.go b/vendor/gopkg.in/ini.v1/key.go index 7c8566a1b..838356af0 100644 --- a/vendor/gopkg.in/ini.v1/key.go +++ b/vendor/gopkg.in/ini.v1/key.go @@ -15,7 +15,6 @@ package ini import ( - "bytes" "errors" "fmt" "strconv" @@ -26,7 +25,6 @@ import ( // Key represents a key under a section. type Key struct { s *Section - Comment string name string value string isAutoIncrement bool @@ -35,7 +33,7 @@ type Key struct { isShadow bool shadows []*Key - nestedValues []string + Comment string } // newKey simply return a key object with given values. @@ -68,22 +66,6 @@ func (k *Key) AddShadow(val string) error { return k.addShadow(val) } -func (k *Key) addNestedValue(val string) error { - if k.isAutoIncrement || k.isBooleanType { - return errors.New("cannot add nested value to auto-increment or boolean key") - } - - k.nestedValues = append(k.nestedValues, val) - return nil -} - -func (k *Key) AddNestedValue(val string) error { - if !k.s.f.options.AllowNestedValues { - return errors.New("nested value is not allowed") - } - return k.addNestedValue(val) -} - // ValueMapper represents a mapping function for values, e.g. os.ExpandEnv type ValueMapper func(string) string @@ -110,12 +92,6 @@ func (k *Key) ValueWithShadows() []string { return vals } -// NestedValues returns nested values stored in the key. -// It is possible returned value is nil if no nested values stored in the key. -func (k *Key) NestedValues() []string { - return k.nestedValues -} - // transformValue takes a raw value and transforms to its final string. func (k *Key) transformValue(val string) string { if k.s.f.ValueMapper != nil { @@ -138,7 +114,7 @@ func (k *Key) transformValue(val string) string { // Search in the same section. nk, err := k.s.GetKey(noption) - if err != nil || k == nk { + if err != nil { // Search again in default section. nk, _ = k.s.f.Section("").GetKey(noption) } @@ -468,39 +444,11 @@ func (k *Key) Strings(delim string) []string { return []string{} } - runes := []rune(str) - vals := make([]string, 0, 2) - var buf bytes.Buffer - escape := false - idx := 0 - for { - if escape { - escape = false - if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) { - buf.WriteRune('\\') - } - buf.WriteRune(runes[idx]) - } else { - if runes[idx] == '\\' { - escape = true - } else if strings.HasPrefix(string(runes[idx:]), delim) { - idx += len(delim) - 1 - vals = append(vals, strings.TrimSpace(buf.String())) - buf.Reset() - } else { - buf.WriteRune(runes[idx]) - } - } - idx += 1 - if idx == len(runes) { - break - } - } - - if buf.Len() > 0 { - vals = append(vals, strings.TrimSpace(buf.String())) + vals := strings.Split(str, delim) + for i := range vals { + // vals[i] = k.transformValue(strings.TrimSpace(vals[i])) + vals[i] = strings.TrimSpace(vals[i]) } - return vals } diff --git a/vendor/gopkg.in/ini.v1/parser.go b/vendor/gopkg.in/ini.v1/parser.go index db3af8f00..69d547627 100644 --- a/vendor/gopkg.in/ini.v1/parser.go +++ b/vendor/gopkg.in/ini.v1/parser.go @@ -193,9 +193,7 @@ func hasSurroundedQuote(in string, quote byte) bool { strings.IndexByte(in[1:], quote) == len(in)-2 } -func (p *parser) readValue(in []byte, - ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) { - +func (p *parser) readValue(in []byte, ignoreContinuation, ignoreInlineComment bool) (string, error) { line := strings.TrimLeftFunc(string(in), unicode.IsSpace) if len(line) == 0 { return "", nil @@ -206,8 +204,6 @@ func (p *parser) readValue(in []byte, valQuote = `"""` } else if line[0] == '`' { valQuote = "`" - } else if unescapeValueDoubleQuotes && line[0] == '"' { - valQuote = `"` } if len(valQuote) > 0 { @@ -218,9 +214,6 @@ func (p *parser) readValue(in []byte, return p.readMultilines(line, line[startIdx:], valQuote) } - if unescapeValueDoubleQuotes && valQuote == `"` { - return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil - } return line[startIdx : pos+startIdx], nil } @@ -241,17 +234,10 @@ func (p *parser) readValue(in []byte, } } - // Trim single and double quotes + // Trim single quotes if hasSurroundedQuote(line, '\'') || hasSurroundedQuote(line, '"') { line = line[1 : len(line)-1] - } else if len(valQuote) == 0 && unescapeValueCommentSymbols { - if strings.Contains(line, `\;`) { - line = strings.Replace(line, `\;`, ";", -1) - } - if strings.Contains(line, `\#`) { - line = strings.Replace(line, `\#`, "#", -1) - } } return line, nil } @@ -264,15 +250,7 @@ func (f *File) parse(reader io.Reader) (err error) { } // Ignore error because default section name is never empty string. - name := DEFAULT_SECTION - if f.options.Insensitive { - name = strings.ToLower(DEFAULT_SECTION) - } - section, _ := f.NewSection(name) - - // This "last" is not strictly equivalent to "previous one" if current key is not the first nested key - var isLastValueEmpty bool - var lastRegularKey *Key + section, _ := f.NewSection(DEFAULT_SECTION) var line []byte var inUnparseableSection bool @@ -282,14 +260,6 @@ func (f *File) parse(reader io.Reader) (err error) { return err } - if f.options.AllowNestedValues && - isLastValueEmpty && len(line) > 0 { - if line[0] == ' ' || line[0] == '\t' { - lastRegularKey.addNestedValue(string(bytes.TrimSpace(line))) - continue - } - } - line = bytes.TrimLeftFunc(line, unicode.IsSpace) if len(line) == 0 { continue @@ -351,11 +321,7 @@ func (f *File) parse(reader io.Reader) (err error) { if err != nil { // Treat as boolean key when desired, and whole line is key name. if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys { - kname, err := p.readValue(line, - f.options.IgnoreContinuation, - f.options.IgnoreInlineComment, - f.options.UnescapeValueDoubleQuotes, - f.options.UnescapeValueCommentSymbols) + kname, err := p.readValue(line, f.options.IgnoreContinuation, f.options.IgnoreInlineComment) if err != nil { return err } @@ -378,15 +344,10 @@ func (f *File) parse(reader io.Reader) (err error) { p.count++ } - value, err := p.readValue(line[offset:], - f.options.IgnoreContinuation, - f.options.IgnoreInlineComment, - f.options.UnescapeValueDoubleQuotes, - f.options.UnescapeValueCommentSymbols) + value, err := p.readValue(line[offset:], f.options.IgnoreContinuation, f.options.IgnoreInlineComment) if err != nil { return err } - isLastValueEmpty = len(value) == 0 key, err := section.NewKey(kname, value) if err != nil { @@ -395,7 +356,6 @@ func (f *File) parse(reader io.Reader) (err error) { key.isAutoIncrement = isAutoIncr key.Comment = strings.TrimSpace(p.comment.String()) p.comment.Reset() - lastRegularKey = key } return nil } diff --git a/vendor/gopkg.in/ini.v1/section.go b/vendor/gopkg.in/ini.v1/section.go index d8a402619..94f7375ed 100644 --- a/vendor/gopkg.in/ini.v1/section.go +++ b/vendor/gopkg.in/ini.v1/section.go @@ -54,14 +54,6 @@ func (s *Section) Body() string { return strings.TrimSpace(s.rawBody) } -// SetBody updates body content only if section is raw. -func (s *Section) SetBody(body string) { - if !s.isRawSection { - return - } - s.rawBody = body -} - // NewKey creates a new key to given section. func (s *Section) NewKey(name, val string) (*Key, error) { if len(name) == 0 { @@ -144,7 +136,6 @@ func (s *Section) HasKey(name string) bool { } // Haskey is a backwards-compatible name for HasKey. -// TODO: delete me in v2 func (s *Section) Haskey(name string) bool { return s.HasKey(name) } diff --git a/vendor/gopkg.in/ini.v1/struct.go b/vendor/gopkg.in/ini.v1/struct.go index 9719dc698..eeb8dabaa 100644 --- a/vendor/gopkg.in/ini.v1/struct.go +++ b/vendor/gopkg.in/ini.v1/struct.go @@ -113,7 +113,7 @@ func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowSh default: return fmt.Errorf("unsupported type '[]%s'", sliceOf) } - if err != nil && isStrict { + if isStrict { return err } @@ -166,7 +166,7 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: durationVal, err := key.Duration() // Skip zero value - if err == nil && int64(durationVal) > 0 { + if err == nil && int(durationVal) > 0 { field.Set(reflect.ValueOf(durationVal)) return nil } @@ -450,12 +450,6 @@ func (s *Section) reflectFrom(val reflect.Value) error { // Note: fieldName can never be empty here, ignore error. sec, _ = s.f.NewSection(fieldName) } - - // Add comment from comment tag - if len(sec.Comment) == 0 { - sec.Comment = tpField.Tag.Get("comment") - } - if err = sec.reflectFrom(field); err != nil { return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) } @@ -467,12 +461,6 @@ func (s *Section) reflectFrom(val reflect.Value) error { if err != nil { key, _ = s.NewKey(fieldName, "") } - - // Add comment from comment tag - if len(key.Comment) == 0 { - key.Comment = tpField.Tag.Get("comment") - } - if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil { return fmt.Errorf("error reflecting field (%s): %v", fieldName, err) }