Skip to content

Commit

Permalink
(go/v4): Add Hub and Spoke for conversion webhooks
Browse files Browse the repository at this point in the history
This PR introduces the initial implementation of the hub-and-spoke model for handling conversion webhooks. The goal is to streamline the conversion process by utilizing a central hub to speak a specific version of the same Group and Kind.

- **Single Spoke Support (A to B, Same Kind and Group):**
  The system will allow only one spoke version for conversions (i.e., converting from Version A to Version B within the same kind and group).

- **Future Expansion (List of GKV Spokes):**
  In the future, based on user feedback or demand, we can expand to support a list of **GKV spokes**, allowing for greater flexibility in conversions across different versions, kinds, and groups.

- **Advanced Case Handling (Manual Steps):**
  For more advanced cases, users can proceed without specifying a spoke. In this scenario, the conversion process will still occur without the spoke, enabling users to continue. However, they will be required to complete specific advanced steps manually.

Closes; #2589
  • Loading branch information
camilamacedo86 committed Nov 10, 2024
1 parent cc242e6 commit e039767
Show file tree
Hide file tree
Showing 12 changed files with 34 additions and 95 deletions.
4 changes: 1 addition & 3 deletions docs/book/src/multiversion-tutorial/testdata/project/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ resources:
webhooks:
conversion: true
defaulting: true
spoke:
- v2
- v2
spoke: v2
validation: true
webhookVersion: v1
- api:
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ func getWebhookResourceFlags(resource resource.Resource) []string {
}
if resource.HasConversionWebhook() {
args = append(args, "--conversion")
for _, spoke := range resource.Webhooks.Spoke {
args = append(args, "--spoke", spoke)
if resource.Webhooks.Spoke != "" {
args = append(args, "--spoke", resource.Webhooks.Spoke)
}
}
return args
Expand Down
24 changes: 2 additions & 22 deletions pkg/model/resource/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Webhooks struct {
// Conversion specifies if a conversion webhook is associated to the resource.
Conversion bool `json:"conversion,omitempty"`

Spoke []string `json:"spoke,omitempty"`
Spoke string `json:"spoke,omitempty"`
}

// Validate checks that the Webhooks is valid.
Expand Down Expand Up @@ -80,7 +80,7 @@ func (webhooks *Webhooks) Update(other *Webhooks) error {
webhooks.Conversion = webhooks.Conversion || other.Conversion

// Update Spoke.
webhooks.Spoke = append(webhooks.Spoke, other.Spoke...)
webhooks.Spoke = other.Spoke

return nil
}
Expand All @@ -89,23 +89,3 @@ func (webhooks *Webhooks) Update(other *Webhooks) error {
func (webhooks Webhooks) IsEmpty() bool {
return webhooks.WebhookVersion == "" && !webhooks.Defaulting && !webhooks.Validation && !webhooks.Conversion
}

// HasSpokeVersion returns true if the spoke version is present in the list of spoke versions.
func (webhooks Webhooks) HasSpokeVersion(version string) bool {
for _, v := range webhooks.Spoke {
if v == version {
return true
}
}
return false
}

// HasAnySpokeVersionFrom returns true if any spoke versions are present in the list of spoke versions.
func (webhooks Webhooks) HasAnySpokeVersionFrom(values []string) bool {
for _, v := range values {
if webhooks.HasSpokeVersion(v) {
return true
}
}
return false
}
4 changes: 2 additions & 2 deletions pkg/model/resource/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ var _ = Describe("Webhooks", func() {
Defaulting: true,
Validation: true,
Conversion: true,
Spoke: []string{"v2"},
Spoke: "v2",
}
Expect(webhook.Update(nil)).To(Succeed())
Expect(webhook.WebhookVersion).To(Equal(v1))
Expect(webhook.Defaulting).To(BeTrue())
Expect(webhook.Validation).To(BeTrue())
Expect(webhook.Conversion).To(BeTrue())
Expect(webhook.Spoke).To(Equal([]string{"v2"}))
Expect(webhook.Spoke).To(Equal("v2"))
})

Context("webhooks version", func() {
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugins/golang/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type Options struct {
DoConversion bool

// Spoke version for conversion webhook
Spoke []string
Spoke string
}

// UpdateResource updates the provided resource with the options
Expand Down
17 changes: 8 additions & 9 deletions pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,16 @@ type Spoke struct {
machinery.BoilerplateMixin
machinery.ResourceMixin

Force bool
SpokeVersion string
Force bool
}

// SetTemplateDefaults implements file.Template
func (f *Spoke) SetTemplateDefaults() error {
if f.Path == "" {
if f.MultiGroup && f.Resource.Group != "" {
f.Path = filepath.Join("api", "%[group]", f.SpokeVersion, "%[kind]_conversion.go")
f.Path = filepath.Join("api", "%[group]", f.Resource.Webhooks.Spoke, "%[kind]_conversion.go")
} else {
f.Path = filepath.Join("api", f.SpokeVersion, "%[kind]_conversion.go")
f.Path = filepath.Join("api", f.Resource.Webhooks.Spoke, "%[kind]_conversion.go")
}
}

Expand All @@ -63,7 +62,7 @@ func (f *Spoke) SetTemplateDefaults() error {
// nolint:lll
const spokeTemplate = `{{ .Boilerplate }}
package {{ .SpokeVersion }}
package {{ .Resource.Webhooks.Spoke }}
import (
"log"
Expand All @@ -78,18 +77,18 @@ func (src *{{ .Resource.Kind }}) ConvertTo(dstRaw conversion.Hub) error {
log.Printf("ConvertTo: converts this {{ .Resource.Kind }} to the Hub version ({{ .Resource.Version }});" +
"source: %s/%s and target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
// TODO(user): Implement conversion logic from {{ .SpokeVersion }} to {{ .Resource.Version }}
// TODO(user): Implement conversion logic from {{ .Resource.Webhooks.Spoke }} to {{ .Resource.Version }}
return nil
}
// ConvertFrom converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .SpokeVersion}}).
// ConvertFrom converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .Resource.Webhooks.Spoke }}).
func (dst *{{ .Resource.Kind }}) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
log.Printf("ConvertFrom: converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .SpokeVersion}});" +
log.Printf("ConvertFrom: converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .Resource.Webhooks.Spoke }});" +
"source: %s/%s and target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
// TODO(user): Implement conversion logic from {{ .Resource.Version }} to {{ .SpokeVersion }}
// TODO(user): Implement conversion logic from {{ .Resource.Version }} to {{ .Resource.Webhooks.Spoke }}
return nil
}
Expand Down
10 changes: 1 addition & 9 deletions pkg/plugins/golang/v4/scaffolds/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,10 @@ func (s *webhookScaffolder) Scaffold() error {

if err := scaffold.Execute(
&api.Hub{Force: s.force},
&api.Spoke{Force: s.force},
); err != nil {
return err
}

// Create the spoke version conversion file for each spoke version
for _, spokeVersion := range s.resource.Webhooks.Spoke {
if err := scaffold.Execute(
&api.Spoke{Force: s.force, SpokeVersion: spokeVersion},
); err != nil {
return err
}
}
log.Println(`Webhook server has been set up for you.
You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`)
}
Expand Down
48 changes: 12 additions & 36 deletions pkg/plugins/golang/v4/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"errors"
"fmt"

log "github.com/sirupsen/logrus"
"github.com/spf13/pflag"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
Expand Down Expand Up @@ -66,7 +65,7 @@ validating and/or conversion webhooks.
# Create conversion webhook for Group: ship, Version: v1beta1
# and Kind: Frigate
%[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1,v2,v3
%[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1
`, cliMeta.CommandName)
}

Expand All @@ -84,9 +83,9 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
fs.BoolVar(&p.options.DoConversion, "conversion", false,
"if set, scaffold the conversion webhook")

fs.StringSliceVar(&p.options.Spoke, "spoke",
[]string{},
"comma-separated list of spoke versions for conversion webhook (i.e. --spoke v1,v2,v3)")
fs.StringVar(&p.options.Spoke, "spoke",
"",
"if set, scaffold the spoke implementation (i.e. --spoke v1)")

// TODO: remove for go/v5
fs.BoolVar(&p.isLegacyPath, "legacy", false,
Expand Down Expand Up @@ -114,19 +113,8 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
p.resource = res

if len(p.options.ExternalAPIPath) != 0 && len(p.options.ExternalAPIDomain) != 0 && p.isLegacyPath {
return errors.New("you cannot scaffold webhooks for external types using the legacy path")
}

if p.options.DoConversion {
if len(p.options.Spoke) == 0 {
log.Warnf("The Spoke will not be implemebted." +
"Inform the Spoke version via the flag --spoke")
}
}

if !p.options.DoConversion && !p.options.DoDefaulting && !p.options.DoValidation {
return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
" --programmatic-validation and --conversion to be true", p.commandName)
return errors.New("You cannot scaffold webhooks for external types " +
"using the legacy path")
}

p.options.UpdateResource(p.resource, p.config)
Expand All @@ -135,6 +123,12 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
return err
}

if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
" --programmatic-validation and --conversion to be true", p.commandName)
}

// check if resource exist to create webhook
resValue, err := p.config.GetResource(p.resource.GVK)
res = &resValue
if err != nil {
Expand All @@ -148,24 +142,6 @@ func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
return fmt.Errorf("webhook resource already exists")
}

if p.options.DoConversion {
if res.HasConversionWebhook() &&
len(p.options.Spoke) > 0 &&
res.Webhooks.HasAnySpokeVersionFrom(p.options.Spoke) {
return fmt.Errorf("conversion webhook already exists with one or more spoke versions informed")
}

// Check if all spoke versions informed are api versions which exist in the resource
for _, spokeVersion := range p.options.Spoke {
spokeGKV := res.GVK
spokeGKV.Version = spokeVersion
_, err := p.config.GetResource(spokeGKV)
if err != nil {
return fmt.Errorf("resource does not have a version %s",
spokeVersion)
}
}
}
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/v4/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,12 @@ func scaffoldConversionWebhook(kbc *utils.TestContext) {

err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "api/v2/conversiontest_conversion.go"),
"// TODO(user): Implement conversion logic from v1 to v2",
`dst.Spec.Size = src.Spec.Replicas`)
`src.Spec.Size = dst.Spec.Replicas`)
Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v1 to v2")

err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "api/v2/conversiontest_conversion.go"),
"// TODO(user): Implement conversion logic from v2 to v1",
`dst.Spec.Replicas = src.Spec.Size`)
`src.Spec.Replicas = dst.Spec.Size`)
Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v2 to v1")
}

Expand Down
4 changes: 1 addition & 3 deletions testdata/project-v4-multigroup/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ resources:
version: v1
webhooks:
conversion: true
spoke:
- v2
- v2
spoke: v2
webhookVersion: v1
- api:
crdVersion: v1
Expand Down
4 changes: 1 addition & 3 deletions testdata/project-v4-with-plugins/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ resources:
version: v1
webhooks:
conversion: true
spoke:
- v2
- v2
spoke: v2
webhookVersion: v1
- api:
crdVersion: v1
Expand Down
4 changes: 1 addition & 3 deletions testdata/project-v4/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ resources:
version: v1
webhooks:
conversion: true
spoke:
- v2
- v2
spoke: v2
webhookVersion: v1
- api:
crdVersion: v1
Expand Down

0 comments on commit e039767

Please sign in to comment.