Skip to content

Commit

Permalink
provisioner: Set SupportedVersion condition on GatewayClass (#6147)
Browse files Browse the repository at this point in the history
Based on Gateway API CRD bundle-version annotation

We will still reconcile the GatewayClass etc. but will set a
status condition on the GatewayClass saying this support is best effort.

Signed-off-by: Sunjay Bhatia <sunjayb@vmware.com>
  • Loading branch information
sunjayBhatia authored Jan 31, 2024
1 parent 29201bb commit ac49aee
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 122 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/6147-sunjayBhatia-small.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Gateway API provisioner now checks `gateway.networking.k8s.io/bundle-version` annotation on Gateway CRDs and sets SupportedVersion status condition on GatewayClass if annotation value matches supported Gateway API version. Best-effort support is provided if version does not match.
8 changes: 8 additions & 0 deletions examples/gateway-provisioner/01-roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ rules:
- list
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour-gateway-provisioner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19830,6 +19830,14 @@ rules:
- list
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
Expand Down
139 changes: 92 additions & 47 deletions internal/provisioner/controller/gatewayclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apiextensions_v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -37,6 +38,11 @@ import (
gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

const (
gatewayAPIBundleVersionAnnotation = "gateway.networking.k8s.io/bundle-version"
gatewayAPICRDBundleSupportedVersion = "v1.0.0"
)

// gatewayClassReconciler reconciles GatewayClass objects.
type gatewayClassReconciler struct {
gatewayController gatewayapi_v1beta1.GatewayController
Expand Down Expand Up @@ -141,19 +147,25 @@ func (r *gatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request
return ctrl.Result{}, nil
}

// Collect various status conditions here so we can update using
// setConditions.
statusConditions := map[string]metav1.Condition{}

statusConditions[string(gatewayapi_v1.GatewayClassConditionStatusSupportedVersion)] = r.getSupportedVersionCondition(ctx)

ok, params, err := r.isValidParametersRef(ctx, gatewayClass.Spec.ParametersRef)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error checking gateway class's parametersRef: %w", err)
}
if !ok {
if err := r.setAcceptedCondition(
ctx,
gatewayClass,
metav1.ConditionFalse,
gatewayapi_v1.GatewayClassReasonInvalidParameters,
"Invalid ParametersRef, must be a reference to an existing namespaced projectcontour.io/ContourDeployment resource",
); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set gateway class %s Accepted condition: %w", req, err)
statusConditions[string(gatewayapi_v1.GatewayClassConditionStatusAccepted)] = metav1.Condition{
Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionFalse,
Reason: string(gatewayapi_v1.GatewayClassReasonInvalidParameters),
Message: "Invalid ParametersRef, must be a reference to an existing namespaced projectcontour.io/ContourDeployment resource",
}
if err := r.setConditions(ctx, gatewayClass, statusConditions); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
Expand Down Expand Up @@ -227,70 +239,103 @@ func (r *gatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request
}

if len(invalidParamsMessages) > 0 {
if err := r.setAcceptedCondition(
ctx,
gatewayClass,
metav1.ConditionFalse,
gatewayapi_v1.GatewayClassReasonInvalidParameters,
strings.Join(invalidParamsMessages, "; "),
); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set gateway class %s Accepted condition: %w", req, err)
statusConditions[string(gatewayapi_v1.GatewayClassConditionStatusAccepted)] = metav1.Condition{
Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionFalse,
Reason: string(gatewayapi_v1.GatewayClassReasonInvalidParameters),
Message: strings.Join(invalidParamsMessages, "; "),
}
if err := r.setConditions(ctx, gatewayClass, statusConditions); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}
}

if err := r.setAcceptedCondition(ctx, gatewayClass, metav1.ConditionTrue, gatewayapi_v1.GatewayClassReasonAccepted, "GatewayClass has been accepted by the controller"); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set gateway class %s Accepted condition: %w", req, err)
statusConditions[string(gatewayapi_v1.GatewayClassConditionStatusAccepted)] = metav1.Condition{
Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionTrue,
Reason: string(gatewayapi_v1.GatewayClassReasonAccepted),
Message: "GatewayClass has been accepted by the controller",
}
if err := r.setConditions(ctx, gatewayClass, statusConditions); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}

func (r *gatewayClassReconciler) setAcceptedCondition(
ctx context.Context,
gatewayClass *gatewayapi_v1beta1.GatewayClass,
status metav1.ConditionStatus,
reason gatewayapi_v1beta1.GatewayClassConditionReason,
message string,
) error {
currentAcceptedCondition := metav1.Condition{
Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted),
Status: status,
ObservedGeneration: gatewayClass.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(reason),
Message: message,
}
var newConds []metav1.Condition
for _, cond := range gatewayClass.Status.Conditions {
if cond.Type == string(gatewayapi_v1.GatewayClassConditionStatusAccepted) {
if cond.Status == status {
func (r *gatewayClassReconciler) setConditions(ctx context.Context, gatewayClass *gatewayapi_v1beta1.GatewayClass, newConds map[string]metav1.Condition) error {
var unchangedConds, updatedConds []metav1.Condition
for _, existing := range gatewayClass.Status.Conditions {
if cond, ok := newConds[existing.Type]; ok {
if existing.Status == cond.Status {
// If status hasn't changed, don't change the condition, just
// update the generation.
currentAcceptedCondition = cond
currentAcceptedCondition.ObservedGeneration = gatewayClass.Generation
changed := existing
changed.ObservedGeneration = gatewayClass.Generation
updatedConds = append(updatedConds, changed)
delete(newConds, cond.Type)
}

continue
} else {
unchangedConds = append(unchangedConds, existing)
}

newConds = append(newConds, cond)
}

r.log.WithValues("gatewayclass-name", gatewayClass.Name).Info(fmt.Sprintf("setting gateway class's Accepted condition to %s", status))
transitionTime := metav1.Now()
for _, c := range newConds {
r.log.WithValues("gatewayclass-name", gatewayClass.Name).Info(fmt.Sprintf("setting gateway class's %s condition to %s", c.Type, c.Status))
c.ObservedGeneration = gatewayClass.Generation
c.LastTransitionTime = transitionTime
updatedConds = append(updatedConds, c)
}

// nolint:gocritic
gatewayClass.Status.Conditions = append(newConds, currentAcceptedCondition)
gatewayClass.Status.Conditions = append(unchangedConds, updatedConds...)

if err := r.client.Status().Update(ctx, gatewayClass); err != nil {
return fmt.Errorf("failed to set gatewayclass %s accepted condition: %w", gatewayClass.Name, err)
return fmt.Errorf("failed to update gateway class %s status: %w", gatewayClass.Name, err)
}

return nil
}

func (r *gatewayClassReconciler) getSupportedVersionCondition(ctx context.Context) metav1.Condition {
cond := metav1.Condition{
Type: string(gatewayapi_v1.GatewayClassConditionStatusSupportedVersion),
// Assume false until we get to the happy case.
Status: metav1.ConditionFalse,
Reason: string(gatewayapi_v1.GatewayClassReasonUnsupportedVersion),
}
gatewayClassCRD := &apiextensions_v1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "gatewayclasses." + gatewayapi_v1.GroupName,
},
}
if err := r.client.Get(ctx, client.ObjectKeyFromObject(gatewayClassCRD), gatewayClassCRD); err != nil {
errorMsg := "Error fetching gatewayclass CRD resource to validate supported version"
r.log.Error(err, errorMsg)
cond.Message = fmt.Sprintf("%s: %s. Resources will be reconciled with best-effort.", errorMsg, err)
return cond
}

version, ok := gatewayClassCRD.Annotations[gatewayAPIBundleVersionAnnotation]
if !ok {
cond.Message = fmt.Sprintf("Bundle version annotation %s not found. Resources will be reconciled with best-effort.", gatewayAPIBundleVersionAnnotation)
return cond
}
if version != gatewayAPICRDBundleSupportedVersion {
cond.Message = fmt.Sprintf("Gateway API CRD bundle version %s is not supported. Resources will be reconciled with best-effort.", version)
return cond
}

// No errors found, we can return true.
cond.Status = metav1.ConditionTrue
cond.Reason = string(gatewayapi_v1.GatewayClassReasonSupportedVersion)
cond.Message = fmt.Sprintf("Gateway API CRD bundle version %s is supported.", gatewayAPICRDBundleSupportedVersion)
return cond
}

// isValidParametersRef returns true if the provided ParametersReference is
// to a ContourDeployment resource that exists.
func (r *gatewayClassReconciler) isValidParametersRef(ctx context.Context, ref *gatewayapi_v1beta1.ParametersReference) (bool, *contour_api_v1alpha1.ContourDeployment, error) {
Expand Down
Loading

0 comments on commit ac49aee

Please sign in to comment.