blob: fe069b4e6660e3ab7d87d69f6521aaa7e4a28553 [file] [log] [blame]
// 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),
})
}