Skip to content

Commit

Permalink
Support for empty group in kubebuilder v3 api creation
Browse files Browse the repository at this point in the history
  • Loading branch information
prafull01 committed Sep 14, 2020
1 parent 471fe66 commit caae428
Show file tree
Hide file tree
Showing 25 changed files with 696 additions and 14 deletions.
4 changes: 4 additions & 0 deletions generate_testdata.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ scaffold_test_project() {
$kb create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true --make=false
$kb create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true --make=false
$kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false
if [ $project == "project-v3-multigroup" ]; then
$kb create api --version v1 --kind Lakers --controller=true --resource=true --make=false
$kb create webhook --version v1 --kind Lakers --defaulting --programmatic-validation
fi
elif [ $project == "project-v2-addon" ] || [ $project == "project-v3-addon" ]; then
header_text 'enabling --pattern flag ...'
export KUBEBUILDER_ENABLE_PLUGINS=1
Expand Down
20 changes: 10 additions & 10 deletions pkg/model/resource/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
const (
versionPattern = "^v\\d+(alpha\\d+|beta\\d+)?$"

groupRequired = "group cannot be empty"
versionRequired = "version cannot be empty"
kindRequired = "kind cannot be empty"
)
Expand Down Expand Up @@ -91,19 +90,13 @@ func (opts *Options) Validate() error {
// We can safely look for a '-' as the first char as none of the fields accepts it
// NOTE: We must do this for all the required flags first or we may output the wrong
// error as flags may seem to be missing because Cobra assigned them to another flag.
if strings.HasPrefix(opts.Group, "-") {
return fmt.Errorf(groupRequired)
}
if strings.HasPrefix(opts.Version, "-") {
return fmt.Errorf(versionRequired)
}
if strings.HasPrefix(opts.Kind, "-") {
return fmt.Errorf(kindRequired)
}
// Now we can check that all the required flags are not empty
if len(opts.Group) == 0 {
return fmt.Errorf(groupRequired)
}
if len(opts.Version) == 0 {
return fmt.Errorf(versionRequired)
}
Expand All @@ -112,8 +105,10 @@ func (opts *Options) Validate() error {
}

// Check if the Group has a valid DNS1123 subdomain value
if err := validation.IsDNS1123Subdomain(opts.Group); err != nil {
return fmt.Errorf("group name is invalid: (%v)", err)
if len(opts.Group) != 0 {
if err := validation.IsDNS1123Subdomain(opts.Group); err != nil {
return fmt.Errorf("group name is invalid: (%v)", err)
}
}

// Check if the version follows the valid pattern
Expand Down Expand Up @@ -188,8 +183,13 @@ func (opts *Options) NewResource(c *config.Config, doResource bool) *Resource {

res.Package = pkg
res.Domain = opts.Group
if domain != "" {
if domain != "" && opts.Group != "" {
res.Domain += "." + domain
} else if opts.Group == "" && c.IsV3() {
// Empty group overrides the default values provided by newResource(). GroupPackageName and ImportAlias includes domain instead of group name as user provided group is empty.
res.Domain = domain
res.GroupPackageName = opts.safeImport(domain)
res.ImportAlias = opts.safeImport(domain + opts.Version)
}

return res
Expand Down
5 changes: 2 additions & 3 deletions pkg/model/resource/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ var _ = Describe("Resource Options", func() {
Expect(options.Validate()).To(Succeed())
})

It("should fail if the Group is not specified", func() {
It("should succeed if the Group is not specified", func() {
options := &Options{Version: "v1", Kind: "FirstMate"}
Expect(options.Validate()).NotTo(Succeed())
Expect(options.Validate().Error()).To(ContainSubstring("group cannot be empty"))
Expect(options.Validate()).To(Succeed())
})

It("should fail if the Group is not all lowercase", func() {
Expand Down
8 changes: 7 additions & 1 deletion pkg/model/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ func wrapKey(key string) string {
func (r Resource) Replacer() *strings.Replacer {
var replacements []string

replacements = append(replacements, wrapKey("group"), r.Group)
// If group is empty replace the path with core.
group := r.Group
if group == "" {
group = "core"
}

replacements = append(replacements, wrapKey("group"), group)
replacements = append(replacements, wrapKey("group-package-name"), r.GroupPackageName)
replacements = append(replacements, wrapKey("version"), r.Version)
replacements = append(replacements, wrapKey("kind"), strings.ToLower(r.Kind))
Expand Down
23 changes: 23 additions & 0 deletions pkg/model/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ var _ = Describe("Resource", func() {
Expect(options.Validate()).To(Succeed())

resource := options.NewResource(singleGroupConfig, true)

Expect(resource.Group).To(Equal(options.Group))
Expect(resource.GroupPackageName).To(Equal("myproject"))
Expect(resource.ImportAlias).To(Equal("myprojectv1"))
Expand Down Expand Up @@ -234,5 +235,27 @@ var _ = Describe("Resource", func() {
Expect(resource.Package).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version)))
Expect(resource.Domain).To(Equal("authentication.k8s.io"))
})
It("should use domain if the group is empty", func() {
options := &Options{Version: "v1", Kind: "FirstMate"}
Expect(options.Validate()).To(Succeed())

resource := options.NewResource(
&config.Config{
Version: config.Version2,
Domain: "test.io",
Repo: "test",
},
true,
)
Expect(resource.Namespaced).To(Equal(options.Namespaced))
Expect(resource.Group).To(Equal(""))
Expect(resource.GroupPackageName).To(Equal("testio"))
Expect(resource.Version).To(Equal(options.Version))
Expect(resource.Kind).To(Equal(options.Kind))
Expect(resource.Plural).To(Equal("firstmates"))
Expect(resource.ImportAlias).To(Equal("testiov1"))
Expect(resource.Package).To(Equal(path.Join("test", "api", "v1")))
Expect(resource.Domain).To(Equal("test.io"))
})
})
})
8 changes: 8 additions & 0 deletions pkg/plugin/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ func (p *createAPIPlugin) Validate() error {
return err
}

if p.resource.Group == "" && p.config.Domain == "" {
return fmt.Errorf("can not have group and domain both empty")
}

if p.config.IsV2() && p.config.Domain == "" {
fmt.Errorf("group cannot be empty")
}

reader := bufio.NewReader(os.Stdin)
if !p.resourceFlag.Changed {
fmt.Println("Create Resource [y/n]")
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugin/v3/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ func (p *createAPIPlugin) Validate() error {
return err
}

if p.resource.Group == "" && p.config.Domain == "" {
return fmt.Errorf("can not have group and domain both empty")
}

// TODO: re-evaluate whether y/n input still makes sense. We should probably always
// scaffold the resource and controller.
reader := bufio.NewReader(os.Stdin)
Expand Down
5 changes: 5 additions & 0 deletions pkg/plugin/v3/scaffolds/internal/templates/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap {
return fragments
}

// If Group is empty add the core in import path
if f.Resource.Group == "" {
f.Resource.Group = "core"
}

// Generate import code fragments
imports := make([]string, 0)
imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias, f.Resource.Package))
Expand Down
2 changes: 2 additions & 0 deletions testdata/project-v3-multigroup/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ resources:
- group: foo.policy
kind: HealthCheckPolicy
version: v1
- kind: Lakers
version: v1
version: 3-alpha
36 changes: 36 additions & 0 deletions testdata/project-v3-multigroup/apis/core/v1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2020 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 v1 contains API Schema definitions for the v1 API group
// +kubebuilder:object:generate=true
// +groupName=testproject.org
package v1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "testproject.org", Version: "v1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
64 changes: 64 additions & 0 deletions testdata/project-v3-multigroup/apis/core/v1/lakers_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2020 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 v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// LakersSpec defines the desired state of Lakers
type LakersSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file

// Foo is an example field of Lakers. Edit Lakers_types.go to remove/update
Foo string `json:"foo,omitempty"`
}

// LakersStatus defines the observed state of Lakers
type LakersStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Lakers is the Schema for the lakers API
type Lakers struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec LakersSpec `json:"spec,omitempty"`
Status LakersStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// LakersList contains a list of Lakers
type LakersList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Lakers `json:"items"`
}

func init() {
SchemeBuilder.Register(&Lakers{}, &LakersList{})
}
75 changes: 75 additions & 0 deletions testdata/project-v3-multigroup/apis/core/v1/lakers_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2020 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 v1

import (
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var lakerslog = logf.Log.WithName("lakers-resource")

func (r *Lakers) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-testproject-org-v1-lakers,mutating=true,failurePolicy=fail,groups=testproject.org,resources=lakers,verbs=create;update,versions=v1,name=mlakers.kb.io

var _ webhook.Defaulter = &Lakers{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Lakers) Default() {
lakerslog.Info("default", "name", r.Name)

// TODO(user): fill in your defaulting logic.
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-testproject-org-v1-lakers,mutating=false,failurePolicy=fail,groups=testproject.org,resources=lakers,versions=v1,name=vlakers.kb.io

var _ webhook.Validator = &Lakers{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Lakers) ValidateCreate() error {
lakerslog.Info("validate create", "name", r.Name)

// TODO(user): fill in your validation logic upon object creation.
return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Lakers) ValidateUpdate(old runtime.Object) error {
lakerslog.Info("validate update", "name", r.Name)

// TODO(user): fill in your validation logic upon object update.
return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Lakers) ValidateDelete() error {
lakerslog.Info("validate delete", "name", r.Name)

// TODO(user): fill in your validation logic upon object deletion.
return nil
}
Loading

0 comments on commit caae428

Please sign in to comment.