Unverified Commit cd2592ec authored by Bryan Liles's avatar Bryan Liles Committed by GitHub
Browse files

Merge pull request #333 from bryanl/use-new-ksonnet-lib-generator

Use new ksonnet lib generator
parents 332a9610 e47f5a5b
......@@ -224,12 +224,15 @@
[[projects]]
name = "github.com/ksonnet/ksonnet-lib"
packages = [
"ksonnet-gen/astext",
"ksonnet-gen/jsonnet",
"ksonnet-gen/ksonnet",
"ksonnet-gen/kubespec",
"ksonnet-gen/kubeversion"
"ksonnet-gen/kubeversion",
"ksonnet-gen/nodemaker",
"ksonnet-gen/printer"
]
revision = "e8077023fa84395ad7976fa750136b39d0aa5f08"
revision = "b13dc1c505011ee838ae45324994dac432233000"
[[projects]]
branch = "master"
......@@ -600,6 +603,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "2d8337f0b0870fb8b2c0a041d7b5e3e77b66b414fe9c2d7962ee5c5842bb94e0"
inputs-digest = "f44fb422720ed1d7e1a654a9c4b6f6d9baa90492bb28c459bf30552527255e83"
solver-name = "gps-cdcl"
solver-version = 1
......@@ -54,7 +54,7 @@
[[constraint]]
name = "github.com/ksonnet/ksonnet-lib"
revision = "e8077023fa84395ad7976fa750136b39d0aa5f08"
revision = "b13dc1c505011ee838ae45324994dac432233000"
[[constraint]]
name = "github.com/mattn/go-isatty"
......
package generator
import (
"encoding/json"
"io/ioutil"
"os"
"github.com/davecgh/go-spew/spew"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion"
log "github.com/sirupsen/logrus"
)
var (
// ksonnetEmitter is the function which emits the ksonnet standard library.
ksonnetEmitter = ksonnet.Emit
ksonnetEmitter = ksonnet.GenerateLib
)
// KsonnetLib is the ksonnet standard library for a version of swagger.
......@@ -29,33 +28,34 @@ type KsonnetLib struct {
// Ksonnet generates the ksonnet standard library or returns an error if there was
// a problem.
func Ksonnet(swaggerData []byte) (*KsonnetLib, error) {
// Deserialize the API object.
s := kubespec.APISpec{}
if err := json.Unmarshal(swaggerData, &s); err != nil {
f, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
s.Text = swaggerData
defer os.Remove(f.Name())
// Emit Jsonnet code.
extensionsLibData, k8sLibData, err := ksonnetEmitter(&s, nil, nil)
_, err = f.Write(swaggerData)
if err != nil {
return nil, err
}
// Warn where the Kubernetes version is currently only supported as Beta.
if kubeversion.Beta(s.Info.Version) {
log.Warnf(`!
============================================================================================
Kubernetes version %s is currently supported as Beta; you may encounter unexpected behavior
============================================================================================`, s.Info.Version)
if err = f.Close(); err != nil {
return nil, err
}
spew.Dump("---", f.Name(), ksonnetEmitter)
lib, err := ksonnetEmitter(f.Name())
if err != nil {
return nil, err
}
kl := &KsonnetLib{
K: extensionsLibData,
K8s: k8sLibData,
K: lib.Extensions,
K8s: lib.K8s,
Swagger: swaggerData,
Version: s.Info.Version,
Version: lib.Version,
}
return kl, nil
......
......@@ -4,7 +4,7 @@ import (
"errors"
"testing"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
)
func TestKsonnet(t *testing.T) {
......@@ -16,19 +16,22 @@ func TestKsonnet(t *testing.T) {
var (
ext = []byte("k")
lib = []byte("k8s")
successfulEmit = func(*kubespec.APISpec, *string, *string) ([]byte, []byte, error) {
return ext, lib, nil
successfulEmit = func(string) (*ksonnet.Lib, error) {
return &ksonnet.Lib{
Version: "v1.7.0",
K8s: lib,
Extensions: ext,
}, nil
}
failureEmit = func(*kubespec.APISpec, *string, *string) ([]byte, []byte, error) {
return nil, nil, errors.New("failure")
failureEmit = func(string) (*ksonnet.Lib, error) {
return nil, errors.New("failure")
}
v170swagger = []byte(`{"info":{"version":"v1.7.0"}}`)
v180swagger = []byte(`{"info":{"version":"v1.8.0"}}`)
)
cases := []struct {
name string
emitter func(*kubespec.APISpec, *string, *string) ([]byte, []byte, error)
emitter func(string) (*ksonnet.Lib, error)
swaggerData []byte
version string
isErr bool
......@@ -41,6 +44,7 @@ func TestKsonnet(t *testing.T) {
},
{
name: "invalid swagger",
emitter: failureEmit,
swaggerData: []byte(`{`),
isErr: true,
},
......@@ -50,16 +54,12 @@ func TestKsonnet(t *testing.T) {
swaggerData: v170swagger,
isErr: true,
},
{
name: "valid beta swagger",
emitter: successfulEmit,
swaggerData: v180swagger,
version: "v1.8.0",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ogKSEmitter := ksonnetEmitter
defer func() { ksonnetEmitter = ogKSEmitter }()
ksonnetEmitter = tc.emitter
kl, err := Ksonnet(tc.swaggerData)
......@@ -70,7 +70,7 @@ func TestKsonnet(t *testing.T) {
}
} else {
if err != nil {
t.Fatal("Ksonnet() returned unexpected error")
t.Fatalf("Ksonnet() returned unexpected error: %#v", err)
}
if got, expected := string(kl.K), string(ext); got != expected {
......
......@@ -16,6 +16,7 @@ package lib
import (
"fmt"
"io/ioutil"
"os"
"testing"
......@@ -37,14 +38,6 @@ const (
"definitions": {
}
}`
blankK8sLib = `// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.
// Kubernetes version: v1.7.0
{
local hidden = {
},
}
`
)
var testFS = afero.NewMemMapFs()
......@@ -74,7 +67,9 @@ func TestGenerateLibData(t *testing.T) {
bytes, err := afero.ReadFile(testFS, string(schemaPath))
if err != nil {
t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err)
} else if actualSwagger := string(bytes); actualSwagger != blankSwaggerData {
}
if actualSwagger := string(bytes); actualSwagger != blankSwaggerData {
t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger)
}
......@@ -82,7 +77,14 @@ func TestGenerateLibData(t *testing.T) {
k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath))
if err != nil {
t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err)
} else if actualK8sLib := string(k8sLibBytes); actualK8sLib != blankK8sLib {
}
blankK8sLib, err := ioutil.ReadFile("testdata/k8s.libsonnet")
if err != nil {
t.Fatalf("Failed to read testdata/k8s.libsonnet: %#v", err)
}
if actualK8sLib := string(k8sLibBytes); actualK8sLib != string(blankK8sLib) {
t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib)
}
}
{
"__ksonnet": {
checksum: "f942f2f2b70d842504ffa21b502ad044be92110af159342a352bf1ed4c6221e2",
kubernetesVersion: "1.7.0",
},
local hidden = {
},
}
\ No newline at end of file
package astext
import "github.com/google/go-jsonnet/ast"
// ObjectFields is a slice of ObjectField.
type ObjectFields []ObjectField
// ObjectField wraps ast.ObjectField and adds commenting and the ability to
// be printed on one line.
type ObjectField struct {
ast.ObjectField
// Comment is a comment for the object field.
Comment *Comment
// Oneline prints this field on a single line.
Oneline bool
}
// Object wraps ast.Object and adds the ability to be printed on one line.
type Object struct {
ast.Object
Fields []ObjectField
// Oneline prints this field on a single line.
Oneline bool
}
package astext
// extensions for ast that could live upstream
// Comment is a comment.
type Comment struct {
Text string // represents a single line comment
}
package ksonnet
import (
nm "github.com/ksonnet/ksonnet-lib/ksonnet-gen/nodemaker"
"github.com/pkg/errors"
)
// APIObject is an API object.
type APIObject struct {
resource Object
renderFieldsFn renderFieldsFn
}
// NewAPIObject creates an instance of APIObject.
func NewAPIObject(resource Object) *APIObject {
ao := &APIObject{
resource: resource,
renderFieldsFn: renderFields,
}
return ao
}
// Kind is the kind of api object this is.
func (a *APIObject) Kind() string {
return FormatKind(a.resource.Kind())
}
// Description is the description of this API object.
func (a *APIObject) Description() string {
return a.resource.Description()
}
// Node returns an AST node for this api object.
func (a *APIObject) Node(catalog *Catalog) (*nm.Object, error) {
return apiObjectNode(catalog, a)
}
func (a *APIObject) initNode(catalog *Catalog) (*nm.Object, error) {
o := nm.NewObject()
if a.resource.IsType() {
kindObject := nm.OnelineObject()
kind := a.resource.Kind()
kindObject.Set(nm.InheritedKey("kind"), nm.NewStringDouble(kind))
o.Set(nm.LocalKey("kind"), kindObject)
ctorBase := []nm.Noder{
nm.NewVar("apiVersion"),
nm.NewVar("kind"),
}
a.setConstructors(o, ctorBase, objectConstructor())
} else {
a.setConstructors(o, nil, nm.OnelineObject())
}
return o, nil
}
func (a *APIObject) setConstructors(parent *nm.Object, ctorBase []nm.Noder, defaultCtorBody nm.Noder) error {
desc := makeDescriptor(a.resource.Codebase(), a.resource.Group(), a.resource.Kind())
ctors := locateConstructors(desc)
if len(ctors) > 0 {
for _, ctor := range ctors {
key, err := ctor.Key()
if err != nil {
return errors.Wrap(err, "generate constructor key")
}
parent.Set(key, ctor.Body(ctorBase...))
}
return nil
}
parent.Set(nm.FunctionKey("new", []string{}), defaultCtorBody)
return nil
}
func objectConstructor() *nm.Binary {
return nm.NewBinary(nm.NewVar("apiVersion"), nm.NewVar("kind"), nm.BopPlus)
}
func apiObjectNode(catalog *Catalog, a *APIObject) (*nm.Object, error) {
if catalog == nil {
return nil, errors.New("catalog is nil")
}
o, err := a.initNode(catalog)
if err != nil {
return nil, err
}
if err := a.renderFieldsFn(catalog, o, "", a.resource.Properties()); err != nil {
return nil, err
}
return o, nil
}
package ksonnet
import (
"strings"
"github.com/blang/semver"
"github.com/go-openapi/spec"
"github.com/pkg/errors"
)
var (
blockedReferences = []string{
"io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta",
"io.k8s.apimachinery.pkg.apis.meta.v1.Status",
}
blockedPropertyNames = []string{
"status",
"apiVersion",
"kind",
}
)
// ExtractFn is a function which extracts properties from a schema.
type ExtractFn func(*Catalog, map[string]spec.Schema, []string) (map[string]Property, error)
// CatalogOpt is an option for configuring Catalog.
type CatalogOpt func(*Catalog)
// CatalogOptExtractProperties is a Catalog option for setting the property
// extractor.
func CatalogOptExtractProperties(fn ExtractFn) CatalogOpt {
return func(c *Catalog) {
c.extractFn = fn
}
}
// CatalogOptChecksum is a Catalog option for setting the checksum of the swagger schema.
func CatalogOptChecksum(checksum string) CatalogOpt {
return func(c *Catalog) {
c.checksum = checksum
}
}
// Catalog is a catalog definitions
type Catalog struct {
apiSpec *spec.Swagger
extractFn ExtractFn
apiVersion semver.Version
paths map[string]Component
checksum string
// memos
typesCache []Type
fieldsCache []Field
}
// NewCatalog creates an instance of Catalog.
func NewCatalog(apiSpec *spec.Swagger, opts ...CatalogOpt) (*Catalog, error) {
if apiSpec == nil {
return nil, errors.New("apiSpec is nil")
}
if apiSpec.Info == nil {
return nil, errors.New("apiSpec Info is nil")
}
parts := strings.SplitN(apiSpec.Info.Version, ".", 3)
parts[0] = strings.TrimPrefix(parts[0], "v")
vers := strings.Join(parts, ".")
apiVersion, err := semver.Parse(vers)
if err != nil {
return nil, errors.Wrap(err, "invalid apiSpec version")
}
paths, err := parsePaths(apiSpec)
if err != nil {
return nil, errors.Wrap(err, "parse apiSpec paths")
}
c := &Catalog{
apiSpec: apiSpec,
extractFn: extractProperties,
apiVersion: apiVersion,
paths: paths,
}
for _, opt := range opts {
opt(c)
}
return c, nil
}
// Checksum returns the checksum of the swagger schema.
func (c *Catalog) Checksum() string {
return c.checksum
}
// Version returns the Kubernetes API version represented by this Catalog.
func (c *Catalog) Version() string {
return c.apiVersion.String()
}
// Types returns a slice of all types.
func (c *Catalog) Types() ([]Type, error) {
if c.typesCache != nil {
return c.typesCache, nil
}
var resources []Type
for name, schema := range c.definitions() {
desc, err := ParseDescription(name)
if err != nil {
return nil, errors.Wrapf(err, "parse description for %s", name)
}
// If there is a path, we can update it as a first class object
// in the API. This makes this schema a type.
component, ok := c.paths[name]
if !ok {
continue
}
props, err := c.extractFn(c, schema.Properties, schema.Required)
if err != nil {
return nil, errors.Wrapf(err, "extract propererties from %s", name)
}
kind := NewType(name, schema.Description, desc.Codebase, desc.Group, component, props)
resources = append(resources, kind)
}
c.typesCache = resources
return resources, nil
}
// Fields returns a slice of all fields.
func (c *Catalog) Fields() ([]Field, error) {
if c.fieldsCache != nil {
return c.fieldsCache, nil
}
var types []Field
for name, schema := range c.definitions() {
desc, err := ParseDescription(name)
if err != nil {
return nil, errors.Wrapf(err, "parse description for %s", name)
}
// If there is a path, this should ot be a hidden object. This
// makes this schema a field.
if _, ok := c.paths[name]; ok {
continue
}
props, err := c.extractFn(c, schema.Properties, schema.Required)
if err != nil {
return nil, errors.Wrapf(err, "extract propererties from %s", name)
}
t := NewField(name, schema.Description, desc.Codebase, desc.Group, desc.Version, desc.Kind, props)
types = append(types, *t)
}
c.fieldsCache = types
return types, nil
}
func (c *Catalog) isFormatRef(name string) (bool, error) {
schema, ok := c.apiSpec.Definitions[name]
if !ok {
return false, errors.Errorf("%s was not found", name)
}
if schema.Format != "" {
return true, nil
}
return false, nil
}
// Field returns a field by definition id. If the type cannot be found, it returns an error.
func (c *Catalog) Field(name string) (*Field, error) {
types, err := c.Fields()
if err != nil {
return nil, err
}
for _, ty := range types {
if ty.Identifier() == name {
return &ty, nil
}
}
return nil, errors.Errorf("%s was not found", name)
}
// Resource returns a resource by group, version, kind. If the field cannot be found,
// it returns an error
func (c *Catalog) Resource(group, version, kind string) (*Type, error) {
resources, err := c.Types()
if err != nil {
return nil, err
}
for _, resource := range resources {
if group == resource.Group() &&
version == resource.Version() &&
kind == resource.Kind() {
return &resource, nil
}
}