-
Notifications
You must be signed in to change notification settings - Fork 428
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #119 from DirectXMan12/feature/object-scaffold
✨ Object scaffolding logic from KubeBuilder
- Loading branch information
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
}) | ||
} | ||
} |