Skip to content

Commit

Permalink
fix: Deterministic ordering of lifecycle hooks (#847)
Browse files Browse the repository at this point in the history
This commit introduces wrapper handlers to allow ordering
of lifecyce hooks to be called via a single registered hook.
This ensures that hooks are called in a defined order, fixing
issues that have arisen that require CNI hook to be applied
before metallb hook.

---------

Co-authored-by: Daniel Lipovetsky <[email protected]>
Co-authored-by: Dimitri Koshkin <[email protected]>
  • Loading branch information
3 people authored Aug 9, 2024
1 parent bd575b7 commit c75b759
Show file tree
Hide file tree
Showing 5 changed files with 347 additions and 3 deletions.
1 change: 1 addition & 0 deletions common/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
Expand Down
26 changes: 26 additions & 0 deletions common/pkg/capi/clustertopology/handlers/lifecycle/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"

runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"

"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers"
)

type BeforeClusterCreate interface {
Expand All @@ -16,31 +18,55 @@ type BeforeClusterCreate interface {
*runtimehooksv1.BeforeClusterCreateResponse,
)
}
type NamedBeforeClusterCreate interface {
handlers.Named
BeforeClusterCreate
}

type AfterControlPlaneInitialized interface {
AfterControlPlaneInitialized(
context.Context,
*runtimehooksv1.AfterControlPlaneInitializedRequest,
*runtimehooksv1.AfterControlPlaneInitializedResponse,
)
}
type NamedAfterControlPlaneInitialized interface {
handlers.Named
AfterControlPlaneInitialized
}

type BeforeClusterUpgrade interface {
BeforeClusterUpgrade(
context.Context,
*runtimehooksv1.BeforeClusterUpgradeRequest,
*runtimehooksv1.BeforeClusterUpgradeResponse,
)
}
type NamedBeforeClusterUpgrade interface {
handlers.Named
BeforeClusterUpgrade
}

type AfterControlPlaneUpgrade interface {
AfterControlPlaneUpgrade(
context.Context,
*runtimehooksv1.AfterControlPlaneUpgradeRequest,
*runtimehooksv1.AfterControlPlaneUpgradeResponse,
)
}
type NamedAfterControlPlaneUpgrade interface {
handlers.Named
AfterControlPlaneUpgrade
}

type BeforeClusterDelete interface {
BeforeClusterDelete(
context.Context,
*runtimehooksv1.BeforeClusterDeleteRequest,
*runtimehooksv1.BeforeClusterDeleteResponse,
)
}
type NamedBeforeClusterDelete interface {
handlers.Named
BeforeClusterDelete
}
236 changes: 236 additions & 0 deletions common/pkg/capi/clustertopology/handlers/lifecycle/ordered.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Copyright 2024 Nutanix. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package lifecycle

import (
"context"
"strings"

runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
"sigs.k8s.io/cluster-api/util"
)

type orderedBCC struct {
name string

hooks []BeforeClusterCreate
}

func (o *orderedBCC) BeforeClusterCreate(
ctx context.Context,
req *runtimehooksv1.BeforeClusterCreateRequest,
resp *runtimehooksv1.BeforeClusterCreateResponse,
) {
responses := make([]runtimehooksv1.ResponseObject, 0, len(o.hooks))
for _, h := range o.hooks {
hookResponse := &runtimehooksv1.BeforeClusterCreateResponse{}
h.BeforeClusterCreate(ctx, req, hookResponse)
if hookResponse.Status == runtimehooksv1.ResponseStatusFailure {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = hookResponse.Message
return
}
responses = append(responses, hookResponse)
}

aggregateSuccessfulResponses(resp, responses)
}

func (o *orderedBCC) Name() string {
return o.name
}

func OrderedBeforeClusterCreateHook(
name string,
hooks ...BeforeClusterCreate,
) NamedBeforeClusterCreate {
return &orderedBCC{
name: name,
hooks: hooks,
}
}

type orderedACPI struct {
name string
hooks []AfterControlPlaneInitialized
}

func (o *orderedACPI) AfterControlPlaneInitialized(
ctx context.Context,
req *runtimehooksv1.AfterControlPlaneInitializedRequest,
resp *runtimehooksv1.AfterControlPlaneInitializedResponse,
) {
responses := make([]runtimehooksv1.ResponseObject, 0, len(o.hooks))
for _, h := range o.hooks {
hookResponse := &runtimehooksv1.AfterControlPlaneInitializedResponse{}
h.AfterControlPlaneInitialized(ctx, req, hookResponse)
if hookResponse.Status == runtimehooksv1.ResponseStatusFailure {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = hookResponse.Message
return
}
responses = append(responses, hookResponse)
}

aggregateSuccessfulResponses(resp, responses)
}

func (o *orderedACPI) Name() string {
return o.name
}

func OrderedAfterControlPlaneInitializedHook(
name string, hooks ...AfterControlPlaneInitialized,
) NamedAfterControlPlaneInitialized {
return &orderedACPI{
name: name,
hooks: hooks,
}
}

type orderedBCU struct {
name string

hooks []BeforeClusterUpgrade
}

func (o *orderedBCU) BeforeClusterUpgrade(
ctx context.Context,
req *runtimehooksv1.BeforeClusterUpgradeRequest,
resp *runtimehooksv1.BeforeClusterUpgradeResponse,
) {
responses := make([]runtimehooksv1.ResponseObject, 0, len(o.hooks))
for _, h := range o.hooks {
hookResponse := &runtimehooksv1.BeforeClusterUpgradeResponse{}
h.BeforeClusterUpgrade(ctx, req, hookResponse)
if hookResponse.Status == runtimehooksv1.ResponseStatusFailure {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = hookResponse.Message
return
}
responses = append(responses, hookResponse)
}

aggregateSuccessfulResponses(resp, responses)
}

func (o *orderedBCU) Name() string {
return o.name
}

func OrderedBeforeClusterUpgradeHook(
name string,
hooks ...BeforeClusterUpgrade,
) NamedBeforeClusterUpgrade {
return &orderedBCU{
name: name,
hooks: hooks,
}
}

type orderedACPU struct {
name string

hooks []AfterControlPlaneUpgrade
}

func (o *orderedACPU) AfterControlPlaneUpgrade(
ctx context.Context,
req *runtimehooksv1.AfterControlPlaneUpgradeRequest,
resp *runtimehooksv1.AfterControlPlaneUpgradeResponse,
) {
responses := make([]runtimehooksv1.ResponseObject, 0, len(o.hooks))
for _, h := range o.hooks {
hookResponse := &runtimehooksv1.AfterControlPlaneUpgradeResponse{}
h.AfterControlPlaneUpgrade(ctx, req, hookResponse)
if hookResponse.Status == runtimehooksv1.ResponseStatusFailure {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = hookResponse.Message
return
}
responses = append(responses, hookResponse)
}

aggregateSuccessfulResponses(resp, responses)
}

func (o *orderedACPU) Name() string {
return o.name
}

func OrderedAfterControlPlaneUpgradeHook(
name string,
hooks ...AfterControlPlaneUpgrade,
) NamedAfterControlPlaneUpgrade {
return &orderedACPU{
name: name,
hooks: hooks,
}
}

type orderedBCD struct {
name string

hooks []BeforeClusterDelete
}

func (o *orderedBCD) BeforeClusterDelete(
ctx context.Context,
req *runtimehooksv1.BeforeClusterDeleteRequest,
resp *runtimehooksv1.BeforeClusterDeleteResponse,
) {
responses := make([]runtimehooksv1.ResponseObject, 0, len(o.hooks))
for _, h := range o.hooks {
hookResponse := &runtimehooksv1.BeforeClusterDeleteResponse{}
h.BeforeClusterDelete(ctx, req, hookResponse)
if hookResponse.Status == runtimehooksv1.ResponseStatusFailure {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = hookResponse.Message
return
}
responses = append(responses, hookResponse)
}

aggregateSuccessfulResponses(resp, responses)
}

func (o *orderedBCD) Name() string {
return o.name
}

func OrderedBeforeClusterDeleteHook(
name string,
hooks ...BeforeClusterDelete,
) NamedBeforeClusterDelete {
return &orderedBCD{
name: name,
hooks: hooks,
}
}

// aggregateSuccessfulResponses aggregates all successful responses into a single response.
func aggregateSuccessfulResponses(
aggregatedResponse runtimehooksv1.ResponseObject,
responses []runtimehooksv1.ResponseObject,
) {
// At this point the Status should always be ResponseStatusSuccess.
aggregatedResponse.SetStatus(runtimehooksv1.ResponseStatusSuccess)

// Note: As all responses have the same type we can assume now that
// they all implement the RetryResponseObject interface.
messages := []string{}
for _, resp := range responses {
aggregatedRetryResponse, ok := aggregatedResponse.(runtimehooksv1.RetryResponseObject)
if ok {
aggregatedRetryResponse.SetRetryAfterSeconds(util.LowestNonZeroInt32(
aggregatedRetryResponse.GetRetryAfterSeconds(),
resp.(runtimehooksv1.RetryResponseObject).GetRetryAfterSeconds(),
))
}
if resp.GetMessage() != "" {
messages = append(messages, resp.GetMessage())
}
}
aggregatedResponse.SetMessage(strings.Join(messages, ", "))
}
Loading

0 comments on commit c75b759

Please sign in to comment.