Replace pkger with embed.FS compatibility

This commit is contained in:
Katrina Verey
2021-05-17 09:26:30 -07:00
parent 53c87a32e9
commit 3f3d3b17a4
59 changed files with 888 additions and 357 deletions

View File

@@ -0,0 +1,43 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package parser contains implementations of the framework.TemplateParser and framework.SchemaParser interfaces.
// Typically, you would use these in a framework.TemplateProcessor.
//
// Example:
//
// processor := framework.TemplateProcessor{
// ResourceTemplates: []framework.ResourceTemplate{{
// Templates: parser.TemplateFiles("path/to/templates"),
// }},
// PatchTemplates: []framework.PatchTemplate{
// &framework.ResourcePatchTemplate{
// Templates: parser.TemplateFiles("path/to/patches/ingress.template.yaml"),
// },
// },
// AdditionalSchemas: parser.SchemaFiles("path/to/crd-schemas"),
// }
//
//
// All the parser in this file are compatible with embed.FS filesystems. To load from an embed.FS
// instead of local disk, use `.FromFS`. For example, if you embed filesystems as follows:
//
// //go:embed resources/* patches/*
// var templateFS embed.FS
// //go:embed schemas/*
// var schemaFS embed.FS
//
// Then you can use them like so:
//
// processor := framework.TemplateProcessor{
// ResourceTemplates: []framework.ResourceTemplate{{
// Templates: parser.TemplateFiles("resources").FromFS(templateFS),
// }},
// PatchTemplates: []framework.PatchTemplate{
// &framework.ResourcePatchTemplate{
// Templates: parser.TemplateFiles("patches/ingress.template.yaml").FromFS(templateFS),
// },
// },
// AdditionalSchemas: parser.SchemaFiles("schemas").FromFS(schemaFS),
// }
package parser

View File

@@ -0,0 +1,95 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package parser
import (
iofs "io/fs"
"io/ioutil"
"path"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
)
type parser struct {
fs iofs.FS
paths []string
extension string
}
type contentProcessor func(content []byte, name string) error
func (l parser) parse(processContent contentProcessor) error {
for _, path := range l.paths {
if err := l.readPath(path, processContent); err != nil {
return err
}
}
return nil
}
func (l parser) readPath(path string, processContent contentProcessor) error {
f, err := l.fs.Open(path)
if err != nil {
return err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
// File is directory -- read templates among its immediate children
if info.IsDir() {
dir, ok := f.(iofs.ReadDirFile)
if !ok {
return errors.Errorf("%s is a directory but could not be opened as one", path)
}
return l.readDir(dir, path, processContent)
}
// Path is a file -- check extension and read it
if !strings.HasSuffix(path, l.extension) {
return errors.Errorf("file %s did not have required extension %s", path, l.extension)
}
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
return processContent(b, path)
}
func (l parser) readDir(dir iofs.ReadDirFile, dirname string, processContent contentProcessor) error {
entries, err := dir.ReadDir(0)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), l.extension) {
continue
}
// Note: using filepath.Join will break Windows, because io/fs.FS implementations require slashes on all OS.
// See https://golang.org/pkg/io/fs/#ValidPath
b, err := l.readFile(path.Join(dirname, entry.Name()))
if err != nil {
return err
}
if err := processContent(b, entry.Name()); err != nil {
return err
}
}
return nil
}
func (l parser) readFile(path string) ([]byte, error) {
f, err := l.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}

View File

@@ -0,0 +1,95 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package parser
import (
iofs "io/fs"
"os"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
)
const (
// SchemaExtension is the file extension this package requires schema files to have
SchemaExtension = ".json"
)
// SchemaStrings returns a SchemaParser that will parse the schemas in the given strings.
//
// This is a helper for use with framework.TemplateProcessor#AdditionalSchemas. Example:
//
// processor := framework.TemplateProcessor{
// //...
// AdditionalSchemas: parser.SchemaStrings(`
// {
// "definitions": {
// "com.example.v1.Foo": {
// ...
// }
// }
// }
// `),
//
func SchemaStrings(data ...string) framework.SchemaParser {
return framework.SchemaParserFunc(func() ([]*spec.Definitions, error) {
var defs []*spec.Definitions
for _, content := range data {
var schema spec.Schema
if err := schema.UnmarshalJSON([]byte(content)); err != nil {
return nil, err
} else if schema.Definitions == nil {
return nil, errors.Errorf("inline schema did not contain any definitions")
}
defs = append(defs, &schema.Definitions)
}
return defs, nil
})
}
// SchemaFiles returns a SchemaParser that will parse the schemas in the given files.
// This is a helper for use with framework.TemplateProcessor#AdditionalSchemas.
// processor := framework.TemplateProcessor{
// //...
// AdditionalSchemas: parser.SchemaFiles("path/to/crd-schemas", "path/to/special-schema.json),
// }
func SchemaFiles(paths ...string) SchemaParser {
return SchemaParser{parser{paths: paths, extension: SchemaExtension}}
}
// SchemaParser is a framework.SchemaParser that can parse files or directories containing openapi schemas.
type SchemaParser struct {
parser
}
// Parse implements framework.SchemaParser
func (l SchemaParser) Parse() ([]*spec.Definitions, error) {
if l.fs == nil {
l.fs = os.DirFS(".")
}
var defs []*spec.Definitions
err := l.parse(func(content []byte, name string) error {
var schema spec.Schema
if err := schema.UnmarshalJSON(content); err != nil {
return err
} else if schema.Definitions == nil {
return errors.Errorf("schema %s did not contain any definitions", name)
}
defs = append(defs, &schema.Definitions)
return nil
})
if err != nil {
return nil, err
}
return defs, nil
}
// FromFS allows you to replace the filesystem in which the parser will look up the given paths.
// For example, you can use an embed.FS.
func (l SchemaParser) FromFS(fs iofs.FS) SchemaParser {
l.parser.fs = fs
return l
}

View File

@@ -0,0 +1,100 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package parser_test
import (
_ "embed"
iofs "io/fs"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
)
//go:embed testdata/schema1.json
var schema1String string
//go:embed testdata/schema2.json
var schema2String string
func TestSchemaFiles(t *testing.T) {
tests := []struct {
name string
paths []string
fs iofs.FS
expectedCount int
wantErr string
}{
{
name: "parses schema from file",
paths: []string{"testdata/schema1.json"},
expectedCount: 1,
},
{
name: "accepts multiple inputs",
paths: []string{"testdata/schema1.json", "testdata/schema2.json"},
expectedCount: 2,
},
{
name: "parses templates from directory",
paths: []string{"testdata"},
expectedCount: 2,
},
{
name: "can be configured with an alternative FS",
fs: os.DirFS("testdata"), // changes the root of the input paths
paths: []string{"schema1.json"},
expectedCount: 1,
},
{
name: "rejects non-.template.yaml files",
paths: []string{"testdata/ignore.yaml"},
wantErr: "file testdata/ignore.yaml did not have required extension .json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := parser.SchemaFiles(tt.paths...)
if tt.fs != nil {
p = p.FromFS(tt.fs)
}
schemas, err := p.Parse()
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expectedCount, len(schemas))
})
}
}
func TestSchemaStrings(t *testing.T) {
tests := []struct {
name string
data []string
expectedCount int
}{
{
name: "parses templates from strings",
data: []string{schema1String},
expectedCount: 1,
},
{
name: "accepts multiple inputs",
data: []string{schema1String, schema2String},
expectedCount: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := parser.SchemaStrings(tt.data...)
schemas, err := p.Parse()
require.NoError(t, err)
assert.Equal(t, tt.expectedCount, len(schemas))
})
}
}

View File

@@ -0,0 +1,97 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package parser
import (
"fmt"
iofs "io/fs"
"os"
"path/filepath"
"text/template"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
)
const (
// TemplateExtension is the file extension this package requires template files to have
TemplateExtension = ".template.yaml"
)
// TemplateStrings returns a TemplateParser that will parse the templates from the given strings.
//
// This is a helper for use with framework.TemplateProcessor's template subfields. Example:
//
// processor := framework.TemplateProcessor{
// ResourceTemplates: []framework.ResourceTemplate{{
// Templates: parser.TemplateStrings(`
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// {{ .Key }}: {{ .Value }}
// `)
// }},
// }
func TemplateStrings(data ...string) framework.TemplateParser {
return framework.TemplateParserFunc(func() ([]*template.Template, error) {
var templates []*template.Template
for i := range data {
t, err := template.New(fmt.Sprintf("inline%d", i)).Parse(data[i])
if err != nil {
return nil, err
}
templates = append(templates, t)
}
return templates, nil
})
}
// TemplateFiles returns a TemplateParser that will parse the templates from the given files or directories.
// Only immediate children of any given directories will be parsed.
// All files must end in .template.yaml.
//
// This is a helper for use with framework.TemplateProcessor's template subfields. Example:
//
// processor := framework.TemplateProcessor{
// ResourceTemplates: []framework.ResourceTemplate{{
// Templates: parser.TemplateFiles("path/to/templates", "path/to/special.template.yaml")
// }},
// }
func TemplateFiles(paths ...string) TemplateParser {
return TemplateParser{parser{paths: paths, extension: TemplateExtension}}
}
// TemplateParser is a framework.TemplateParser that can parse files or directories containing Go templated YAML.
type TemplateParser struct {
parser
}
// Parse implements framework.TemplateParser
func (l TemplateParser) Parse() ([]*template.Template, error) {
if l.fs == nil {
l.fs = os.DirFS(".")
}
var templates []*template.Template
err := l.parse(func(content []byte, file string) error {
t, err := template.New(filepath.Base(file)).Parse(string(content))
if err == nil {
templates = append(templates, t)
}
return err
})
if err != nil {
return nil, err
}
return templates, nil
}
// FromFS allows you to replace the filesystem in which the parser will look up the given paths.
// For example, you can use an embed.FS.
func (l TemplateParser) FromFS(fs iofs.FS) TemplateParser {
l.parser.fs = fs
return l
}

View File

@@ -0,0 +1,155 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package parser_test
import (
"bytes"
_ "embed"
iofs "io/fs"
"os"
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
)
//go:embed testdata/cm1.template.yaml
var cm1String string
//go:embed testdata/cm2.template.yaml
var cm2String string
var templateData = struct {
Name string `yaml:"name"`
}{Name: "tester"}
var cm1Success = strings.TrimSpace(`
apiVersion: v1
kind: ConfigMap
metadata:
name: appconfig
labels:
app: tester
data:
app: tester
`)
var cm2Success = strings.TrimSpace(`
apiVersion: v1
kind: ConfigMap
metadata:
name: env
labels:
app: tester
data:
env: production
`)
func TestTemplateFiles(t *testing.T) {
tests := []struct {
name string
paths []string
fs iofs.FS
expected []string
wantErr string
}{
{
name: "parses templates from file",
paths: []string{"testdata/cm1.template.yaml"},
expected: []string{cm1Success},
},
{
name: "accepts multiple inputs",
paths: []string{"testdata/cm1.template.yaml", "testdata/cm2.template.yaml"},
expected: []string{cm1Success, cm2Success},
},
{
name: "parses templates from directory",
paths: []string{"testdata"},
expected: []string{cm1Success, cm2Success},
},
{
name: "can be configured with an alternative FS",
fs: os.DirFS("testdata"), // changes the root of the input paths
paths: []string{"cm1.template.yaml"},
expected: []string{cm1Success},
},
{
name: "rejects non-.template.yaml files",
paths: []string{"testdata/ignore.yaml"},
wantErr: "file testdata/ignore.yaml did not have required extension .template.yaml",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := parser.TemplateFiles(tt.paths...)
if tt.fs != nil {
p = p.FromFS(tt.fs)
}
templates, err := p.Parse()
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
result := []string{}
for _, template := range templates {
w := bytes.NewBuffer([]byte{})
err := template.Execute(w, templateData)
require.NoError(t, err)
result = append(result, strings.TrimSpace(w.String()))
}
sort.Strings(tt.expected)
sort.Strings(result)
assert.Equal(t, len(result), len(tt.expected))
for i := range tt.expected {
assert.YAMLEq(t, tt.expected[i], result[i])
}
})
}
}
func TestTemplateStrings(t *testing.T) {
tests := []struct {
name string
data []string
expected []string
}{
{
name: "parses templates from strings",
data: []string{cm1String},
expected: []string{cm1Success},
},
{
name: "accepts multiple inputs",
data: []string{cm1String, cm2String},
expected: []string{cm1Success, cm2Success},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := parser.TemplateStrings(tt.data...)
templates, err := p.Parse()
require.NoError(t, err)
result := []string{}
for _, template := range templates {
w := bytes.NewBuffer([]byte{})
err := template.Execute(w, templateData)
require.NoError(t, err)
result = append(result, strings.TrimSpace(w.String()))
}
sort.Strings(tt.expected)
sort.Strings(result)
assert.Equal(t, len(result), len(tt.expected))
for i := range tt.expected {
assert.YAMLEq(t, tt.expected[i], result[i])
}
})
}
}

View File

@@ -0,0 +1,11 @@
# Copyright 2021 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
apiVersion: v1
kind: ConfigMap
metadata:
name: appconfig
labels:
app: {{ .Name }}
data:
app: {{ .Name }}

View File

@@ -0,0 +1,11 @@
# Copyright 2021 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
apiVersion: v1
kind: ConfigMap
metadata:
name: env
labels:
app: {{ .Name }}
data:
env: production

View File

@@ -0,0 +1,4 @@
# Copyright 2021 The Kubernetes Authors.
# SPDX-License-Identifier: Apache-2.0
not: a_resource

View File

@@ -0,0 +1,58 @@
{
"definitions": {
"com.example.v1.Foo": {
"type": "object",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
},
"spec": {
"type": "object",
"required": [
"targets"
],
"properties": {
"targets": {
"type": "array",
"x-kubernetes-patch-merge-key": "app",
"x-kubernetes-patch-strategy": "merge",
"items": {
"type": "object",
"required": [
"app"
],
"properties": {
"app": {
"type": "string"
},
"size": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
}
}
}
},
"x-kubernetes-group-version-kind": [
{
"group": "example.com",
"kind": "Foo",
"version": "v1"
}
]
}
}
}

View File

@@ -0,0 +1,58 @@
{
"definitions": {
"com.example.v1.Bar": {
"type": "object",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
},
"spec": {
"type": "object",
"required": [
"targets"
],
"properties": {
"targets": {
"type": "array",
"x-kubernetes-patch-merge-key": "app",
"x-kubernetes-patch-strategy": "merge",
"items": {
"type": "object",
"required": [
"app"
],
"properties": {
"app": {
"type": "string"
},
"size": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
}
}
}
},
"x-kubernetes-group-version-kind": [
{
"group": "example.com",
"kind": "Bar",
"version": "v1"
}
]
}
}
}