Skip to content

Commit

Permalink
Object scaffolding logic from KubeBuilder
Browse files Browse the repository at this point in the history
This extracts KubeBuilder's object scaffolding logic into a separate
command.  Creating the basic structure for Kubernetes objects is a
non-opinionated task, so it can be reused for other purposes
(incrementally adding types to files, non-KubeBuilder scaffolding
projects, etc).
  • Loading branch information
DirectXMan12 committed Jan 10, 2019
1 parent 950a0e8 commit 3cb46cf
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
72 changes: 72 additions & 0 deletions cmd/type-scaffold/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2018 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 main

import (
"fmt"
"os"

"github.com/spf13/cobra"
"sigs.k8s.io/controller-tools/pkg/typescaffold"
)

func main() {
opts := &typescaffold.ScaffoldOptions{
Resource: typescaffold.Resource{
Namespaced: true,
},
}
scaffoldCmd := &cobra.Command{
Use: "type-scaffold",
Short: "Quickly scaffold out basic bits of a Kubernetes type.",
Long: `Quickly scaffold out the structure of a type for a Kubernetes kind and associated types.
Produces:
- a root type with approparite metadata fields
- Spec and Status types
- a list type
Also applies the appropriate comments to generate the code required to conform to runtime.Object.`,
Example: ` # Generate types for a Kind called Foo with a resource called foos
type-scaffold --kind Foo
# Generate types for a Kind called Bar with a resource of foobars
type-scaffold --kind Bar --resource foobars`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.Validate(); err != nil {
return err
}

return opts.Scaffold(os.Stdout)
},
}

scaffoldCmd.Flags().StringVar(&opts.Resource.Kind, "kind", opts.Resource.Kind, "The kind of the typescaffold being scaffolded.")
scaffoldCmd.Flags().StringVar(&opts.Resource.Resource, "resource", opts.Resource.Resource, "The resource of the typescaffold being scaffolded (defaults to a lower-case, plural version of kind).")
scaffoldCmd.Flags().BoolVar(&opts.Resource.Namespaced, "namespaced", opts.Resource.Namespaced, "Whether or not the given resource is namespaced.")

if err := cobra.MarkFlagRequired(scaffoldCmd.Flags(), "kind"); err != nil {
panic("unable to mark --kind as required")
}

if err := scaffoldCmd.Execute(); err != nil {
if _, err := fmt.Fprintln(os.Stderr, err); err != nil {
// this would be exceedingly bizarre if we ever got here
panic("unable to write to error details to standard error")
}
os.Exit(1)
}
}
54 changes: 54 additions & 0 deletions pkg/typescaffold/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copyright 2018 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 typescaffold

import (
"fmt"
"strings"

"github.com/markbates/inflect"
)

// Resource contains the information required to scaffold files for a resource.
type Resource struct {
// Namespaced is true if the resource is namespaced
Namespaced bool

// Kind is the API Kind.
Kind string

// Resource is the API Resource.
Resource string
}

// Validate checks the Resource values to make sure they are valid.
func (r *Resource) Validate() error {
if len(r.Kind) == 0 {
return fmt.Errorf("kind cannot be empty")
}

rs := inflect.NewDefaultRuleset()
if len(r.Resource) == 0 {
r.Resource = rs.Pluralize(strings.ToLower(r.Kind))
}

if r.Kind != inflect.Camelize(r.Kind) {
return fmt.Errorf("Kind must be camelcase (expected %s was %s)", inflect.Camelize(r.Kind), r.Kind)
}

return nil
}
105 changes: 105 additions & 0 deletions pkg/typescaffold/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2018 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 typescaffold

import (
"io"
"strings"
"text/template"
)

var (
typesTemplateRaw = `// {{.Resource.Kind}}Spec defines the desired state of {{.Resource.Kind}}
type {{.Resource.Kind}}Spec struct {
// INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster
{{- if .AdditionalHelp }}
{{- range .AdditionalHelp | SplitLines }}
// {{.}}
{{- end }}
{{- end }}
}
// {{.Resource.Kind}}Status defines the observed state of {{.Resource.Kind}}.
// It should always be reconstructable from the state of the cluster and/or outside world.
type {{.Resource.Kind}}Status struct {
// INSERT ADDITIONAL STATUS FIELDS -- observed state of cluster
{{- if .AdditionalHelp }}
{{- range .AdditionalHelp | SplitLines }}
// {{.}}
{{- end }}
{{- end }}
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
{{- if .GenerateClients }}
// +genclient
{{- if not .Resource.Namespaced }}
// +genclient:nonNamespaced
{{- end }}
{{- end }}
// {{.Resource.Kind}} is the Schema for the {{ .Resource.Resource }} API
// +k8s:openapi-gen=true
type {{.Resource.Kind}} struct {
metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
Spec {{.Resource.Kind}}Spec ` + "`" + `json:"spec,omitempty"` + "`" + `
Status {{.Resource.Kind}}Status ` + "`" + `json:"status,omitempty"` + "`" + `
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
{{- if and (.GenerateClients) (not .Resource.Namespaced) }}
// +genclient:nonNamespaced
{{- end }}
// {{.Resource.Kind}}List contains a list of {{.Resource.Kind}}
type {{.Resource.Kind}}List struct {
metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + `
}
`
typesTemplateHelpers = template.FuncMap{
"SplitLines": func(raw string) []string { return strings.Split(raw, "\n") },
}

typesTemplate = template.Must(template.New("object-scaffolding").Funcs(typesTemplateHelpers).Parse(typesTemplateRaw))
)

// ScaffoldOptions describes how to scaffold out a Kubernetes object
// with the basic metadata and comment annotations required to generate code
// for and conform to runtime.Object and metav1.Object.
type ScaffoldOptions struct {
Resource Resource
AdditionalHelp string
GenerateClients bool
}

// Validate validates the options, returning an error if anything is invalid.
func (o *ScaffoldOptions) Validate() error {
if err := o.Resource.Validate(); err != nil {
return err
}

return nil
}

// Scaffold prints the Kubernetes object scaffolding to the given output.
func (o *ScaffoldOptions) Scaffold(out io.Writer) error {
return typesTemplate.Execute(out, o)
}
104 changes: 104 additions & 0 deletions pkg/typescaffold/scaffold_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
Copyright 2018 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 typescaffold_test

import (
"bytes"
"testing"

"sigs.k8s.io/controller-tools/pkg/typescaffold"
)

func TestScaffold(t *testing.T) {
tests := []struct {
name string
opts typescaffold.ScaffoldOptions
}{
{
name: "kind only",
opts: typescaffold.ScaffoldOptions{
Resource: typescaffold.Resource{
Kind: "Foo",
},
},
},
{
name: "kind and resource",
opts: typescaffold.ScaffoldOptions{
Resource: typescaffold.Resource{
Kind: "Foo",
Resource: "foos",
},
},
},
{
name: "namespaced",
opts: typescaffold.ScaffoldOptions{
Resource: typescaffold.Resource{
Kind: "Foo",
Resource: "foos",
Namespaced: true,
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.opts.Validate()
if err != nil {
t.Fatalf("unable to validate scaffold opts: %v", err)
}
var out bytes.Buffer
if err := test.opts.Scaffold(&out); err != nil {
t.Fatalf("unable to scaffold types: %v", err)
}

// TODO(directxman12): testing the direct output seems fragile
// there must be a better way.
})
}
}

func TestInvalidScaffoldOpts(t *testing.T) {
tests := []struct {
name string
opts typescaffold.ScaffoldOptions
}{
{
name: "bad kind",
opts: typescaffold.ScaffoldOptions{
Resource: typescaffold.Resource{
Kind: "Foo_bats",
},
},
},
{
name: "no kind",
opts: typescaffold.ScaffoldOptions{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.opts.Validate()
if err == nil {
t.Fatalf("expected error -- those options were invalid")
}
})
}
}

0 comments on commit 3cb46cf

Please sign in to comment.