diff --git a/cmd/type-scaffold/main.go b/cmd/type-scaffold/main.go new file mode 100644 index 000000000..a01dd37e4 --- /dev/null +++ b/cmd/type-scaffold/main.go @@ -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) + } +} diff --git a/pkg/typescaffold/resource.go b/pkg/typescaffold/resource.go new file mode 100644 index 000000000..cd982e007 --- /dev/null +++ b/pkg/typescaffold/resource.go @@ -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 +} diff --git a/pkg/typescaffold/scaffold.go b/pkg/typescaffold/scaffold.go new file mode 100644 index 000000000..361452fe0 --- /dev/null +++ b/pkg/typescaffold/scaffold.go @@ -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) +} diff --git a/pkg/typescaffold/scaffold_test.go b/pkg/typescaffold/scaffold_test.go new file mode 100644 index 000000000..2ac850481 --- /dev/null +++ b/pkg/typescaffold/scaffold_test.go @@ -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") + } + }) + } +}