| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you 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 framework |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "reflect" |
| "slices" |
| "strings" |
| "time" |
| |
| "github.com/gruntwork-io/terratest/modules/testing" |
| "github.com/pkg/errors" |
| "github.com/stretchr/testify/require" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/types" |
| "k8s.io/apimachinery/pkg/util/wait" |
| "sigs.k8s.io/controller-runtime/pkg/client" |
| gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" |
| "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" |
| |
| "github.com/apache/apisix-ingress-controller/api/v1alpha1" |
| apiv2 "github.com/apache/apisix-ingress-controller/api/v2" |
| ) |
| |
| func HTTPRouteMustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, refNN, hrNN types.NamespacedName, condition metav1.Condition) { |
| err := PollUntilHTTPRouteHaveStatus(cli, timeout, hrNN, func(hr *gatewayv1.HTTPRoute) bool { |
| for _, parent := range hr.Status.Parents { |
| if err := kubernetes.ConditionsHaveLatestObservedGeneration(hr, parent.Conditions); err != nil { |
| log.Printf("HTTPRoute %s (parentRef=%v) %v", hrNN, parentRefToString(parent.ParentRef), err) |
| return false |
| } |
| if (refNN.Name == "" || parent.ParentRef.Name == gatewayv1.ObjectName(refNN.Name)) && |
| (refNN.Namespace == "" || (parent.ParentRef.Namespace != nil && string(*parent.ParentRef.Namespace) == refNN.Namespace)) { |
| if findConditionInList(parent.Conditions, condition) { |
| log.Printf("found condition %v in %v for %s reference %s", condition, parent.Conditions, hrNN, refNN) |
| return true |
| } else { |
| log.Printf("NOT FOUND condition %v in %v for %s reference %s", condition, parent.Conditions, hrNN, refNN) |
| } |
| } |
| } |
| return false |
| }) |
| require.NoError(t, err, "error waiting for HTTPRoute status to have a Condition matching %+v", condition) |
| } |
| |
| func PollUntilHTTPRouteHaveStatus(cli client.Client, timeout time.Duration, hrNN types.NamespacedName, f func(route *gatewayv1.HTTPRoute) bool) error { |
| if err := gatewayv1.Install(cli.Scheme()); err != nil { |
| return err |
| } |
| return genericPollResource(new(gatewayv1.HTTPRoute), cli, timeout, hrNN, f) |
| } |
| |
| func HTTPRoutePolicyMustHaveCondition(t testing.TestingT, client client.Client, timeout time.Duration, refNN, hrpNN types.NamespacedName, |
| condition metav1.Condition) { |
| err := PollUntilHTTPRoutePolicyHaveStatus(client, timeout, hrpNN, func(httpRoutePolicy *v1alpha1.HTTPRoutePolicy) bool { |
| for _, ancestor := range httpRoutePolicy.Status.Ancestors { |
| if err := kubernetes.ConditionsHaveLatestObservedGeneration(httpRoutePolicy, ancestor.Conditions); err != nil { |
| log.Printf("HTTPRoutePolicy %s (parentRef=%v) %v", hrpNN, parentRefToString(ancestor.AncestorRef), err) |
| return false |
| } |
| |
| if ancestor.AncestorRef.Name == gatewayv1.ObjectName(refNN.Name) && |
| (refNN.Namespace == "" || (ancestor.AncestorRef.Namespace != nil && string(*ancestor.AncestorRef.Namespace) == refNN.Namespace)) { |
| if findConditionInList(ancestor.Conditions, condition) { |
| log.Printf("found condition %v in list %v for %s reference %s", condition, ancestor.Conditions, hrpNN, refNN) |
| return true |
| } else { |
| log.Printf("NOT FOUND condition %v in %v for %s reference %s", condition, ancestor.Conditions, hrpNN, refNN) |
| } |
| } |
| } |
| return false |
| }) |
| |
| require.NoError(t, err, "error waiting for HTTPRoutePolicy %s status to have a Condition matching %+v", hrpNN, condition) |
| } |
| |
| func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration, hrpNN types.NamespacedName, |
| f func(httpRoutePolicy *v1alpha1.HTTPRoutePolicy) bool) error { |
| if err := v1alpha1.AddToScheme(cli.Scheme()); err != nil { |
| return err |
| } |
| return genericPollResource(new(v1alpha1.HTTPRoutePolicy), cli, timeout, hrpNN, f) |
| } |
| |
| func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) { |
| f := func(object client.Object) bool { |
| value := reflect.Indirect(reflect.ValueOf(object)) |
| status, ok := value.FieldByName("Status").Interface().(apiv2.ApisixStatus) |
| if !ok { |
| return false |
| } |
| if err := kubernetes.ConditionsHaveLatestObservedGeneration(object, status.Conditions); err != nil { |
| return false |
| } |
| return findConditionInList(status.Conditions, cond) |
| } |
| err := PollUntilAPIv2MustHaveStatus(cli, timeout, nn, obj, f) |
| |
| require.NoError(t, err, "error waiting %s status to have a Condition matching %+v", nn, cond) |
| } |
| |
| func PollUntilAPIv2MustHaveStatus(cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, f func(client.Object) bool) error { |
| if err := apiv2.AddToScheme(cli.Scheme()); err != nil { |
| return err |
| } |
| return wait.PollUntilContextTimeout(context.Background(), 2*time.Second, timeout, true, func(ctx context.Context) (done bool, err error) { |
| if err := cli.Get(ctx, nn, obj); err != nil { |
| return false, errors.Wrapf(err, "error fetching Object %s", nn) |
| } |
| return f(obj), nil |
| }) |
| } |
| |
| func parentRefToString(p gatewayv1.ParentReference) string { |
| if p.Namespace != nil && *p.Namespace != "" { |
| return fmt.Sprintf("%v/%v", p.Namespace, p.Name) |
| } |
| return string(p.Name) |
| } |
| |
| func findConditionInList(conditions []metav1.Condition, expected metav1.Condition) bool { |
| return slices.ContainsFunc(conditions, func(item metav1.Condition) bool { |
| // an empty Status string means "Match any status". |
| // an empty Reason string means "Match any reason". |
| return expected.Type == item.Type && |
| (expected.Status == "" || expected.Status == item.Status) && |
| (expected.Reason == "" || expected.Reason == item.Reason) && |
| (expected.Message == "" || strings.Contains(item.Message, expected.Message)) |
| }) |
| } |
| |
| func genericPollResource[Obj client.Object](obj Obj, cli client.Client, timeout time.Duration, nn types.NamespacedName, predicate func(Obj) bool) error { |
| return wait.PollUntilContextTimeout(context.Background(), time.Second, timeout, true, func(ctx context.Context) (done bool, err error) { |
| if err := cli.Get(ctx, nn, obj); err != nil { |
| return false, errors.Wrapf(err, "error fetching Object %s", nn) |
| } |
| return predicate(obj), nil |
| }) |
| } |
| |
| func NewApplier(t testing.TestingT, cli client.Client, apply func(string) error) Applier { |
| return &applier{ |
| t: t, |
| cli: cli, |
| apply: apply, |
| } |
| } |
| |
| type Applier interface { |
| MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) |
| } |
| |
| type applier struct { |
| t testing.TestingT |
| cli client.Client |
| apply func(string) error |
| } |
| |
| func (a *applier) MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) { |
| require.NoError(a.t, a.apply(spec), "creating %s", nn) |
| |
| APIv2MustHaveCondition(a.t, a.cli, 180*time.Second, nn, obj, metav1.Condition{ |
| Type: string(gatewayv1.RouteConditionAccepted), |
| Status: metav1.ConditionTrue, |
| Reason: string(gatewayv1.GatewayReasonAccepted), |
| }) |
| } |