diff --git a/manifests/00-cluster-role.yaml b/manifests/00-cluster-role.yaml index e29ed4bd76..00012ab244 100644 --- a/manifests/00-cluster-role.yaml +++ b/manifests/00-cluster-role.yaml @@ -168,6 +168,14 @@ rules: verbs: - '*' +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways/status + verbs: + - patch + - update + - apiGroups: - apiextensions.k8s.io resources: diff --git a/pkg/operator/controller/gateway-labeler/controller.go b/pkg/operator/controller/gateway-labeler/controller.go index a614607248..82f9ae0972 100644 --- a/pkg/operator/controller/gateway-labeler/controller.go +++ b/pkg/operator/controller/gateway-labeler/controller.go @@ -150,29 +150,8 @@ func NewUnmanaged(mgr manager.Manager) (controller.Controller, error) { // Watch gateways so that we update labels when a gateway is updated or // something modifies or removes the label. - gatewayHasOurController := func(o client.Object) bool { - gateway := o.(*gatewayapiv1.Gateway) + gatewayHasOurController := operatorcontroller.GatewayHasOurController(log, reconciler.cache) - if gateway.Labels[key] == value { - return false - } - - gatewayClassName := types.NamespacedName{ - Namespace: "", // Gatewayclasses are cluster-scoped. - Name: string(gateway.Spec.GatewayClassName), - } - var gatewayClass gatewayapiv1.GatewayClass - if err := reconciler.cache.Get(context.Background(), gatewayClassName, &gatewayClass); err != nil { - log.Error(err, "failed to get gatewayclass for gateway", "gateway", gateway.Name, "namespace", gateway.Namespace, "gatewayclass", gatewayClassName.Name) - return false - } - - if gatewayClass.Spec.ControllerName == operatorcontroller.OpenShiftGatewayClassControllerName { - return true - } - - return false - } gatewayPredicate := predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { return gatewayHasOurController(e.Object) diff --git a/pkg/operator/controller/gateway-service-dns/controller.go b/pkg/operator/controller/gateway-service-dns/controller.go index 8ff94a3eb4..f83589c16c 100644 --- a/pkg/operator/controller/gateway-service-dns/controller.go +++ b/pkg/operator/controller/gateway-service-dns/controller.go @@ -37,15 +37,6 @@ import ( const ( controllerName = "service_dns_controller" - - // gatewayNameLabelKey is the key of a label that Istio adds to - // deployments that it creates for gateways that it manages. Istio uses - // this label in the selector of any service that it creates for a - // gateway. - gatewayNameLabelKey = "gateway.networking.k8s.io/gateway-name" - // managedByIstioLabelKey is the key of a label that Istio adds to - // resources that it manages. - managedByIstioLabelKey = "gateway.istio.io/managed" ) var log = logf.Logger.WithName(controllerName) @@ -68,7 +59,7 @@ func NewUnmanaged(mgr manager.Manager, config Config) (controller.Controller, er scheme := mgr.GetClient().Scheme() mapper := mgr.GetClient().RESTMapper() isServiceNeedingDNS := predicate.NewPredicateFuncs(func(o client.Object) bool { - _, ok := o.(*corev1.Service).Labels[managedByIstioLabelKey] + _, ok := o.(*corev1.Service).Labels[operatorcontroller.ManagedByIstioLabelKey] return ok }) gatewayListenersChanged := predicate.Funcs{ @@ -91,7 +82,7 @@ func NewUnmanaged(mgr manager.Manager, config Config) (controller.Controller, er gatewayToService := func(ctx context.Context, o client.Object) []reconcile.Request { var services corev1.ServiceList listOpts := []client.ListOption{ - client.MatchingLabels{gatewayNameLabelKey: o.GetName()}, + client.MatchingLabels{operatorcontroller.GatewayNameLabelKey: o.GetName()}, client.InNamespace(config.OperandNamespace), } requests := []reconcile.Request{} @@ -172,15 +163,15 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( return reconcile.Result{}, err } - if len(service.Labels[gatewayNameLabelKey]) == 0 { - log.Info("service does not have a label with the expected key; reconciliation will be skipped", "request", request, "labelKey", gatewayNameLabelKey) + if len(service.Labels[operatorcontroller.GatewayNameLabelKey]) == 0 { + log.Info("service does not have a label with the expected key; reconciliation will be skipped", "request", request, "labelKey", operatorcontroller.GatewayNameLabelKey) return reconcile.Result{}, nil } var gateway gatewayapiv1.Gateway gatewayName := types.NamespacedName{ Namespace: service.Namespace, - Name: service.Labels[gatewayNameLabelKey], + Name: service.Labels[operatorcontroller.GatewayNameLabelKey], } if err := r.cache.Get(ctx, gatewayName, &gateway); err != nil { if apierrors.IsNotFound(err) { @@ -234,7 +225,7 @@ func getGatewayHostnames(gateway *gatewayapiv1.Gateway) sets.String { // returns a list of any errors that result from ensuring those DNSRecord CRs. func (r *reconciler) ensureDNSRecordsForGateway(ctx context.Context, gateway *gatewayapiv1.Gateway, service *corev1.Service, domains []string, infraConfig *configv1.Infrastructure, dnsConfig *configv1.DNS) []error { labels := map[string]string{ - gatewayNameLabelKey: gateway.Name, + operatorcontroller.GatewayNameLabelKey: gateway.Name, } for k, v := range service.Labels { labels[k] = v @@ -265,7 +256,7 @@ func (r *reconciler) ensureDNSRecordsForGateway(ctx context.Context, gateway *ga // that result from deleting those DNSRecord CRs. func (r *reconciler) deleteStaleDNSRecordsForGateway(ctx context.Context, gateway *gatewayapiv1.Gateway, service *corev1.Service, domains sets.String) []error { listOpts := []client.ListOption{ - client.MatchingLabels{gatewayNameLabelKey: gateway.Name}, + client.MatchingLabels{operatorcontroller.GatewayNameLabelKey: gateway.Name}, client.InNamespace(r.config.OperandNamespace), } var dnsrecords iov1.DNSRecordList diff --git a/pkg/operator/controller/gateway-status/controller.go b/pkg/operator/controller/gateway-status/controller.go new file mode 100644 index 0000000000..b465b76b72 --- /dev/null +++ b/pkg/operator/controller/gateway-status/controller.go @@ -0,0 +1,260 @@ +package gatewaystatus + +import ( + "context" + "fmt" + + configv1 "github.com/openshift/api/config/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + logf "github.com/openshift/cluster-ingress-operator/pkg/log" + operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" + "github.com/openshift/cluster-ingress-operator/pkg/resources/status" +) + +const ( + controllerName = "gateway_status_controller" +) + +var logger = logf.Logger.WithName(controllerName) + +// reconciler reconciles gateways, adding missing conditions to the resource +type reconciler struct { + cache cache.Cache + client client.Client + recorder record.EventRecorder +} + +// NewUnmanaged creates and returns a controller that adds watches for changes on +// Services, DNSRecords and Gateways and when managed, adds the proper status +// conditions to these Gateways. +// This is an unmanaged controller, which means that the manager does not start it. +func NewUnmanaged(mgr manager.Manager) (controller.Controller, error) { + // Create a new cache for gateways so it can watch all namespaces. + // (Using the operator cache for gateways in all namespaces would cause + // it to cache other resources in all namespaces.) + // This cache is optimized just for the resources that concern this controller: + // * Just Openshift managed Gateway Class + // * Just resources on "openshift-ingress" namespace + // * Removing managed fields to reduce memory footprint + gatewaysCache, err := cache.New(mgr.GetConfig(), cache.Options{ + Scheme: mgr.GetScheme(), + Mapper: mgr.GetRESTMapper(), + // defaultCacheConfig is a config that will be set on the cache of this controller + // while watching for resources. + // It will: + // - Strip managed fields to reduce memory footprint + // - Default to watch just resources on the "openshift-ingress" namespace, + // as this is the only managed namespace by this controller + DefaultNamespaces: map[string]cache.Config{ + operatorcontroller.DefaultOperandNamespace: { + Transform: cache.TransformStripManagedFields(), + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("error initializing cache: %w", err) + } + // Make sure the manager starts the cache with the other runnables. + if err := mgr.Add(gatewaysCache); err != nil { + // We skip returning the error here to follow the pattern of other cache scenarios. + // We may not have the CRDs added/initialized yet, and this will happen + // during gatewayapi controller initialization so having the cache added + // but not synced is fine for now + logger.Error(err, "error adding the cache to manager") + } + + reconciler := &reconciler{ + cache: gatewaysCache, + client: mgr.GetClient(), + recorder: mgr.GetEventRecorderFor(controllerName), + } + c, err := controller.NewUnmanaged(controllerName, controller.Options{Reconciler: reconciler}) + if err != nil { + return nil, fmt.Errorf("error initializing controller: %w", err) + } + + gatewayHasOurController := operatorcontroller.GatewayHasOurController(logger, reconciler.cache) + gatewayPredicate := predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return gatewayHasOurController(e.Object) + }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + UpdateFunc: func(e event.UpdateEvent) bool { + return gatewayHasOurController(e.ObjectNew) + }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + } + + isManagedResource := predicate.NewPredicateFuncs(func(object client.Object) bool { + labels := object.GetLabels() + gwName, ok := labels[operatorcontroller.GatewayNameLabelKey] + return ok && gwName != "" + }) + + if err := c.Watch(source.Kind[client.Object](gatewaysCache, &gatewayapiv1.Gateway{}, &handler.EnqueueRequestForObject{}, gatewayPredicate)); err != nil { + return nil, fmt.Errorf("error initializing gateway watcher: %w", err) + } + + if err := c.Watch(source.Kind[client.Object](gatewaysCache, &corev1.Service{}, handler.EnqueueRequestsFromMapFunc(gatewayFromResourceLabel), isManagedResource)); err != nil { + return nil, fmt.Errorf("error initializing service watcher: %w", err) + } + + if err := c.Watch(source.Kind[client.Object](gatewaysCache, &iov1.DNSRecord{}, handler.EnqueueRequestsFromMapFunc(gatewayFromResourceLabel), isManagedResource)); err != nil { + return nil, fmt.Errorf("error initializing dnsrecord watcher: %w", err) + } + + return c, nil +} + +// Reconcile expects request to refer to a gateway and adds Openshift DNS and LoadBalancer +// conditions to it +func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + log := logger.WithValues("name", request.Name, "namespace", request.Namespace) + log.Info("Reconciling gateway") + + sourceGateway := &gatewayapiv1.Gateway{} + if err := r.cache.Get(ctx, request.NamespacedName, sourceGateway); err != nil { + log.Error(err, "error fetching the gateway object") + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + gateway := sourceGateway.DeepCopy() + + var errs []error + + childSvc := &corev1.Service{} + err := fetchFirstMatchingFromGateway(ctx, r.cache, childSvc, gateway) + if err != nil { + log.Error(err, "error fetching the service from gateway") + } + + childDNSRecord := &iov1.DNSRecord{} + err = fetchFirstMatchingFromGateway(ctx, r.cache, childDNSRecord, gateway) + if err != nil { + log.Error(err, "error fetching the dnsrecord from gateway") + } + + dnsConfig := &configv1.DNS{} + if err := r.client.Get(ctx, types.NamespacedName{Name: "cluster"}, dnsConfig); err != nil { + log.Error(err, "error fetching the cluster object") + return reconcile.Result{}, fmt.Errorf("failed to get dns 'cluster': %v", err) + } + + infraConfig := &configv1.Infrastructure{} + if err := r.client.Get(ctx, types.NamespacedName{Name: "cluster"}, infraConfig); err != nil { + log.Error(err, "error fetching the infrastructure 'cluster' object") + return reconcile.Result{}, fmt.Errorf("failed to get infrastructure 'cluster': %v", err) + } + + operandEvents := &corev1.EventList{} + if err := r.client.List(ctx, operandEvents, client.InNamespace(operatorcontroller.DefaultOperandNamespace)); err != nil { + log.Error(err, "error fetching the events from namespace") + errs = append(errs, fmt.Errorf("failed to list events in namespace %q: %v", operatorcontroller.DefaultOperandNamespace, err)) + } + + // Initialize the conditions if they are null, as the compute* functions receives a pointer + if gateway.Status.Conditions == nil { + gateway.Status.Conditions = make([]metav1.Condition, 0) + } + + // WARNING: one thing to be aware is that conditions on Gateway resource are limited to 8: https://github.com/kubernetes-sigs/gateway-api/blob/a8fe5c8732a37ef471d86afaf570ff8ad0ef0221/apis/v1/gateway_types.go#L691 + // So if the Gateway controller (in our case Istio) is adding 2 conditions, we have 6 more to add. + status.ComputeGatewayAPIDNSStatus(childDNSRecord, dnsConfig, gateway.GetGeneration(), &gateway.Status.Conditions) + status.ComputeGatewayAPILoadBalancerStatus(childSvc, operandEvents.Items, gateway.GetGeneration(), &gateway.Status.Conditions) + + log.Info("new conditions to be added", "conditions", gateway.Status.Conditions) + if err := r.client.Status().Patch(ctx, gateway, client.MergeFrom(sourceGateway)); err != nil { + log.Error(err, "error patching the gateway status") + errs = append(errs, err) + } else { + r.recorder.Eventf(gateway, "Normal", "AddedConditions", "Added Openshift conditions to gateway %s", gateway.Name) + } + + return reconcile.Result{}, utilerrors.NewAggregate(errs) +} + +// The Service and DNSRecord are created with labels that indicate they are Gateway API +// managed, and which Gateway is related to them. The namespace is the same of the resouce. +// We can then simply rely on these labels to decide which Gateway to add to the reconciliation +// queue instead of going with different approaches for the same resource +// Example of the labels we care: gateway.networking.k8s.io/gateway-name: gatewayname +func gatewayFromResourceLabel(_ context.Context, o client.Object) []reconcile.Request { + labels := o.GetLabels() + namespace := o.GetNamespace() + gwName, ok := labels[operatorcontroller.GatewayNameLabelKey] + // No label is present, we should not add anything to reconciliation queue + if !ok || gwName == "" { + logger.Info(fmt.Sprintf("object %T does not contain the label", o), "namespace", o.GetNamespace(), "name", o.GetName()) + return []reconcile.Request{} + } + + logger.Info(fmt.Sprintf("object %T does contain the label", o), "namespace", o.GetNamespace(), "name", o.GetName(), "gateway", gwName) + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: gwName, + }, + }, + } +} + +type gatewayMatcheableResource interface { + *corev1.Service | *iov1.DNSRecord +} + +func fetchFirstMatchingFromGateway[T gatewayMatcheableResource](ctx context.Context, kclient client.Reader, obj T, gw *gatewayapiv1.Gateway) error { + // At this moment we just know that services must have the Gateway API label with the + // same name of the Gateway. So we need a list, this list should have at least + // 1 item, and we care just about the first one (1:1 on Gateway/Service relation) + listOpts := []client.ListOption{ + client.MatchingLabels{operatorcontroller.GatewayNameLabelKey: gw.GetName()}, + client.InNamespace(operatorcontroller.DefaultOperandNamespace), + } + + switch t := any(obj).(type) { + case *corev1.Service: + list := &corev1.ServiceList{} + if err := kclient.List(ctx, list, listOpts...); err != nil { + return err + } + if len(list.Items) == 0 { + err := fmt.Errorf("no services where found for Gateway") + return err + } + + *t = *list.Items[0].DeepCopy() + + case *iov1.DNSRecord: + list := &iov1.DNSRecordList{} + if err := kclient.List(ctx, list, listOpts...); err != nil { + return err + } + if len(list.Items) == 0 { + err := fmt.Errorf("no dnsrecord where found for Gateway") + return err + } + *t = *list.Items[0].DeepCopy() + + default: + return fmt.Errorf("unsupported type %T", obj) + } + + return nil +} diff --git a/pkg/operator/controller/gateway-status/controller_test.go b/pkg/operator/controller/gateway-status/controller_test.go new file mode 100644 index 0000000000..36621ee256 --- /dev/null +++ b/pkg/operator/controller/gateway-status/controller_test.go @@ -0,0 +1,565 @@ +package gatewaystatus + +import ( + "context" + "testing" + + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + condutils "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache/informertest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + testutil "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/test/util" +) + +var ( + commonDNSZone = configv1.DNSZone{ + ID: "somezone.on.some.provider", + } +) + +// common functions and vars for tests +var ( + dnsConfigFn = func(addzone bool) *configv1.DNS { + cfg := &configv1.DNS{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: configv1.DNSSpec{ + BaseDomain: "example.tld", + }, + } + if addzone { + cfg.Spec.PublicZone = commonDNSZone.DeepCopy() + } + return cfg + } + + infraConfigFn = &configv1.Infrastructure{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Status: configv1.InfrastructureStatus{ + PlatformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + }, + } + gwFn = func(name string, conditions ...metav1.Condition) *gatewayapiv1.Gateway { + return &gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "openshift-ingress", + Name: name, + }, + Status: gatewayapiv1.GatewayStatus{ + Conditions: conditions, + }, + } + } + + svcFn = func(name string, labels map[string]string, ingresses ...corev1.LoadBalancerIngress) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Namespace: "openshift-ingress", + Name: name, + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: ingresses, + }, + }, + } + } + eventFn = func(kind, namespace, name, component, message, reason string) *corev1.Event { + return &corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + InvolvedObject: corev1.ObjectReference{ + Kind: kind, + Namespace: namespace, + Name: name, + }, + Message: message, + Reason: reason, + Source: corev1.EventSource{ + Component: component, + }, + } + } + + exampleManagedGatewayLabel = map[string]string{ + "gateway.istio.io/managed": "openshift.io-gateway-controller", + "gateway.networking.k8s.io/gateway-name": "example-gateway", + } + + exampleConditions = []metav1.Condition{ + { + Type: string(gatewayapiv1.GatewayConditionAccepted), + Reason: string(gatewayapiv1.GatewayReasonAccepted), + Status: metav1.ConditionTrue, + Message: "Resource accepted", + }, + { + Type: string(gatewayapiv1.GatewayConditionProgrammed), + Reason: string(gatewayapiv1.GatewayReasonProgrammed), + Status: metav1.ConditionTrue, + Message: "Resource programmed, service created", + }, + } + + ingHostFn = func(hostname string) corev1.LoadBalancerIngress { + return corev1.LoadBalancerIngress{ + Hostname: hostname, + } + } + + dnsRecordFn = func(name, dnsName string, policy iov1.DNSManagementPolicy, labels map[string]string, status iov1.DNSRecordStatus) *iov1.DNSRecord { + return &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Namespace: "openshift-ingress", + Name: name, + }, + Spec: iov1.DNSRecordSpec{ + DNSName: dnsName, + RecordType: iov1.CNAMERecordType, + RecordTTL: 30, + DNSManagementPolicy: policy, + }, + Status: status, + } + } + + reqFn = func(ns, name string) reconcile.Request { + return reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: ns, + Name: name, + }, + } + } +) + +func Test_Reconcile(t *testing.T) { + + tests := []struct { + name string + existingObjects []runtime.Object + expectedConditions []metav1.Condition + reconcileRequest reconcile.Request + expectError string + }{ + { + name: "gateway not found, should not return any error", + existingObjects: []runtime.Object{ + infraConfigFn, + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectError: "", + }, + { + name: "missing dns config", + existingObjects: []runtime.Object{ + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("example-gateway", exampleManagedGatewayLabel, ingHostFn("gwapi.example.tld")), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: exampleConditions, + expectError: `dnses.config.openshift.io "cluster" not found`, + }, + { + name: "missing infrastructure config", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + gwFn("example-gateway", exampleConditions...), + svcFn("example-gateway", exampleManagedGatewayLabel, ingHostFn("gwapi.example.tld")), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: exampleConditions, + expectError: `infrastructures.config.openshift.io "cluster" not found`, + }, + { + name: "missing loadbalancer and dnsrecord should add the right condition about missing both resources", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerManagedIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + }, + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "ServiceNotFound", + Message: "The LoadBalancer service resource is missing", + }, + { + Type: operatorv1.DNSManagedIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "RecordNotFound", + Message: "The wildcard record resource was not found.", + }, + }, + expectError: "", + }, + { + name: "missing dnsconfig zone should add the right condition", + existingObjects: []runtime.Object{ + dnsConfigFn(false), + infraConfigFn, + gwFn("example-gateway"), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.DNSManagedIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "NoDNSZones", + Message: "No DNS zones are defined in the cluster dns config.", + }, + }, + expectError: "", + }, + { + name: "load balancer with missing ingress status should be reflected on gateway condition", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "LoadBalancerPending", + Message: "The LoadBalancer service is pending", + }, + }, + expectError: "", + }, + { + name: "load balancer with missing ingress status and extra event should be reflected on gateway condition", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel), + eventFn("Service", "openshift-ingress", "openshift-ingress-lb", "service-controller", "unavailable", "SyncLoadBalancerFailed"), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "SyncLoadBalancerFailed", + Message: "The service-controller component is reporting SyncLoadBalancerFailed events like: unavailable\nThe cloud-controller-manager logs may contain more details.", + }, + }, + expectError: "", + }, + { + name: "load balancer with valid ingress status should be reflected on gateway condition", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "gwapi.example.tld", + }), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "RecordNotFound", + Message: "The wildcard record resource was not found.", + }, + }, + expectError: "", + }, + { + name: "load balancer valid, dns unmanaged should be properly reflected on conditions", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "something.somewhere.somehow", + }), + dnsRecordFn("openshift-ingress-dns", "gwapi.example.tld", iov1.UnmanagedDNS, exampleManagedGatewayLabel, iov1.DNSRecordStatus{}), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionUnknown, + Reason: "UnmanagedDNS", + Message: "The DNS management policy is set to Unmanaged.", + }, + }, + expectError: "", + }, + { + name: "load balancer valid, dns managed but no zones should reflect on conditions", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "something.somewhere.somehow", + }), + dnsRecordFn("openshift-ingress-dns", "gwapi.example.tld", iov1.ManagedDNS, exampleManagedGatewayLabel, iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{}, + }), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "NoZones", + Message: "The record isn't present in any zones.", + }, + }, + expectError: "", + }, + { + name: "load balancer valid, dns managed with one failed zone should reflect on conditions", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "something.somewhere.somehow", + }), + dnsRecordFn("openshift-ingress-dns", "gwapi.example.tld", iov1.ManagedDNS, exampleManagedGatewayLabel, iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: *commonDNSZone.DeepCopy(), + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionFalse), + }, + }, + }, + }, + }), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "FailedZones", + Message: "The record failed to provision in some zones: [{somezone.on.some.provider map[]}]", + }, + }, + expectError: "", + }, + { + name: "load balancer valid, dns managed with one zone in unknown state should reflect on conditions", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "something.somewhere.somehow", + }), + dnsRecordFn("openshift-ingress-dns", "gwapi.example.tld", iov1.ManagedDNS, exampleManagedGatewayLabel, iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: *commonDNSZone.DeepCopy(), + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionUnknown), + }, + }, + }, + }, + }), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionFalse, + Reason: "UnknownZones", + Message: "Provisioning of the record is in an unknown state in some zones: [{somezone.on.some.provider map[]}]", + }, + }, + expectError: "", + }, + { + name: "load balancer valid, dns managed with one zone in valid state should reflect on conditions", + existingObjects: []runtime.Object{ + dnsConfigFn(true), + infraConfigFn, + gwFn("example-gateway", exampleConditions...), + svcFn("openshift-ingress-lb", exampleManagedGatewayLabel, corev1.LoadBalancerIngress{ + Hostname: "something.somewhere.somehow", + }), + dnsRecordFn("openshift-ingress-dns", "gwapi.example.tld", iov1.ManagedDNS, exampleManagedGatewayLabel, iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: *commonDNSZone.DeepCopy(), + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionTrue), + }, + { + Type: iov1.DNSRecordFailedConditionType, + Status: string(operatorv1.ConditionFalse), + }, + }, + }, + { + DNSZone: configv1.DNSZone{ + ID: "not.one.that.we.manage", + }, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionTrue), + }, + }, + }, + }, + }), + }, + reconcileRequest: reqFn("openshift-ingress", "example-gateway"), + expectedConditions: []metav1.Condition{ + { + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }, + { + Type: operatorv1.DNSReadyIngressConditionType, + Status: metav1.ConditionTrue, + Reason: "NoFailedZones", + Message: "The record is provisioned in all reported zones.", + }, + }, + expectError: "", + }, + } + + scheme := runtime.NewScheme() + iov1.AddToScheme(scheme) + corev1.AddToScheme(scheme) + gatewayapiv1.Install(scheme) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(tc.existingObjects...). + WithStatusSubresource(&gatewayapiv1.Gateway{}). + Build() + cl := &testutil.FakeClientRecorder{ + Client: fakeClient, + T: t, + Updated: []client.Object{}, + } + informer := informertest.FakeInformers{Scheme: scheme} + cache := testutil.FakeCache{Informers: &informer, Reader: cl} + reconciler := &reconciler{ + cache: cache, + client: cl, + recorder: record.NewFakeRecorder(1), + } + res, err := reconciler.Reconcile(ctx, tc.reconcileRequest) + if tc.expectError == "" { + if assert.NoError(t, err) { + assert.Equal(t, reconcile.Result{}, res) + } + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tc.expectError) + } + } + + if len(tc.expectedConditions) > 0 { + gw := &gatewayapiv1.Gateway{} + err := cl.Get(ctx, tc.reconcileRequest.NamespacedName, gw) + assert.NoError(t, err) + // Instead of diffing conditions we will try to search if the condition + // we want exists and has the right values + for _, cond := range tc.expectedConditions { + found := condutils.FindStatusCondition(gw.Status.Conditions, cond.Type) + if found == nil { + assert.NotNil(t, found, "condition not found %+v", cond) + } else { + // Reset the transition time just because we don't care about it now + // On go 1.25 we can run this inside synctest + now := metav1.Now() + found.LastTransitionTime = now + cond.LastTransitionTime = now + assert.Equal(t, cond, *found, "conditions are not equal") + } + } + } + + }) + } +} diff --git a/pkg/operator/controller/ingress/status.go b/pkg/operator/controller/ingress/status.go index 4d3f3b4451..07662380f0 100644 --- a/pkg/operator/controller/ingress/status.go +++ b/pkg/operator/controller/ingress/status.go @@ -16,6 +16,7 @@ import ( "github.com/openshift/cluster-ingress-operator/pkg/manifests" "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" + "github.com/openshift/cluster-ingress-operator/pkg/resources/status" "github.com/openshift/cluster-ingress-operator/pkg/util/retryableerror" configv1 "github.com/openshift/api/config/v1" @@ -86,9 +87,9 @@ func (r *reconciler) syncIngressControllerStatus(ic *operatorv1.IngressControlle updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeDeploymentReplicasMinAvailableCondition(deployment, pods)) updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeDeploymentReplicasAllAvailableCondition(deployment)) updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeDeploymentRollingOutCondition(deployment)) - updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeLoadBalancerStatus(ic, service, operandEvents)...) + updated.Status.Conditions = MergeConditions(updated.Status.Conditions, status.ComputeLoadBalancerStatus(ic, service, operandEvents)...) updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeLoadBalancerProgressingStatus(updated, service, platformStatus)) - updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeDNSStatus(ic, wildcardRecord, platformStatus, dnsConfig)...) + updated.Status.Conditions = MergeConditions(updated.Status.Conditions, status.ComputeDNSStatus(ic, wildcardRecord, platformStatus, dnsConfig)...) updated.Status.Conditions = MergeConditions(updated.Status.Conditions, computeIngressAvailableCondition(updated.Status.Conditions)) degradedCondition, err := computeIngressDegradedCondition(updated.Status.Conditions, updated.Name) errs = append(errs, err) @@ -972,74 +973,6 @@ func conditionsEqual(a, b []operatorv1.OperatorCondition) bool { return cmp.Equal(a, b, conditionCmpOpts...) } -// computeLoadBalancerStatus returns the set of current -// LoadBalancer-prefixed conditions for the given ingress controller, which are -// used later to determine the ingress controller's Degraded or Available status. -func computeLoadBalancerStatus(ic *operatorv1.IngressController, service *corev1.Service, operandEvents []corev1.Event) []operatorv1.OperatorCondition { - // Compute the LoadBalancerManagedIngressConditionType condition - if ic.Status.EndpointPublishingStrategy == nil || - ic.Status.EndpointPublishingStrategy.Type != operatorv1.LoadBalancerServiceStrategyType { - return []operatorv1.OperatorCondition{ - { - Type: operatorv1.LoadBalancerManagedIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "EndpointPublishingStrategyExcludesManagedLoadBalancer", - Message: "The configured endpoint publishing strategy does not include a managed load balancer", - }, - } - } - - conditions := []operatorv1.OperatorCondition{} - - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.LoadBalancerManagedIngressConditionType, - Status: operatorv1.ConditionTrue, - Reason: "WantedByEndpointPublishingStrategy", - Message: "The endpoint publishing strategy supports a managed load balancer", - }) - - // Compute the LoadBalancerReadyIngressConditionType condition - switch { - case service == nil: - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.LoadBalancerReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "ServiceNotFound", - Message: "The LoadBalancer service resource is missing", - }) - case isProvisioned(service): - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.LoadBalancerReadyIngressConditionType, - Status: operatorv1.ConditionTrue, - Reason: "LoadBalancerProvisioned", - Message: "The LoadBalancer service is provisioned", - }) - case isPending(service): - reason := "LoadBalancerPending" - message := "The LoadBalancer service is pending" - - // Try and find a more specific reason for for the pending status. - createFailedReason := "SyncLoadBalancerFailed" - failedLoadBalancerEvents := getEventsByReason(operandEvents, "service-controller", createFailedReason) - for _, event := range failedLoadBalancerEvents { - involved := event.InvolvedObject - if involved.Kind == "Service" && involved.Namespace == service.Namespace && involved.Name == service.Name && involved.UID == service.UID { - reason = "SyncLoadBalancerFailed" - message = fmt.Sprintf("The %s component is reporting SyncLoadBalancerFailed events like: %s\n%s", - event.Source.Component, event.Message, "The cloud-controller-manager logs may contain more details.") - break - } - } - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.LoadBalancerReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: reason, - Message: message, - }) - } - return conditions -} - // computeLoadBalancerProgressingStatus returns the LoadBalancerProgressing // conditions for the given ingress controller. These conditions subsequently determine // the ingress controller's Progressing status. @@ -1127,139 +1060,6 @@ func updateIngressControllerFloatingIPOpenStackStatus(ic *operatorv1.IngressCont } } -func isProvisioned(service *corev1.Service) bool { - ingresses := service.Status.LoadBalancer.Ingress - return len(ingresses) > 0 && (len(ingresses[0].Hostname) > 0 || len(ingresses[0].IP) > 0) -} - -func isPending(service *corev1.Service) bool { - return !isProvisioned(service) -} - -func getEventsByReason(events []corev1.Event, component, reason string) []corev1.Event { - var filtered []corev1.Event - for i := range events { - event := events[i] - if event.Source.Component == component && event.Reason == reason { - filtered = append(filtered, event) - } - } - return filtered -} - -func computeDNSStatus(ic *operatorv1.IngressController, wildcardRecord *iov1.DNSRecord, status *configv1.PlatformStatus, dnsConfig *configv1.DNS) []operatorv1.OperatorCondition { - if dnsConfig.Spec.PublicZone == nil && dnsConfig.Spec.PrivateZone == nil { - return []operatorv1.OperatorCondition{ - { - Type: operatorv1.DNSManagedIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "NoDNSZones", - Message: "No DNS zones are defined in the cluster dns config.", - }, - } - } - - if ic.Status.EndpointPublishingStrategy.Type != operatorv1.LoadBalancerServiceStrategyType { - return []operatorv1.OperatorCondition{ - { - Type: operatorv1.DNSManagedIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "UnsupportedEndpointPublishingStrategy", - Message: "The endpoint publishing strategy doesn't support DNS management.", - }, - } - } - var conditions []operatorv1.OperatorCondition - // The default value for DNSManagementPolicy is "Managed". - if lb := ic.Status.EndpointPublishingStrategy.LoadBalancer; lb != nil && lb.DNSManagementPolicy == operatorv1.UnmanagedLoadBalancerDNS { - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSManagedIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "UnmanagedLoadBalancerDNS", - Message: "The DNS management policy is set to Unmanaged.", - }) - } else { - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSManagedIngressConditionType, - Status: operatorv1.ConditionTrue, - Reason: "Normal", - Message: "DNS management is supported and zones are specified in the cluster DNS config.", - }) - } - - switch { - case wildcardRecord == nil: - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "RecordNotFound", - Message: "The wildcard record resource was not found.", - }) - case wildcardRecord.Spec.DNSManagementPolicy == iov1.UnmanagedDNS: - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionUnknown, - Reason: "UnmanagedDNS", - Message: "The DNS management policy is set to Unmanaged.", - }) - case len(wildcardRecord.Status.Zones) == 0: - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "NoZones", - Message: "The record isn't present in any zones.", - }) - case len(wildcardRecord.Status.Zones) > 0: - var failedZones []configv1.DNSZone - var unknownZones []configv1.DNSZone - for _, zone := range wildcardRecord.Status.Zones { - for _, cond := range zone.Conditions { - if cond.Type != iov1.DNSRecordPublishedConditionType { - continue - } - if !checkZoneInConfig(dnsConfig, zone.DNSZone) { - continue - } - switch cond.Status { - case string(operatorv1.ConditionFalse): - // check to see if the zone is in the dnsConfig.Spec - // fix:BZ1942657 - relates to status changes when updating DNS PrivateZone config - failedZones = append(failedZones, zone.DNSZone) - case string(operatorv1.ConditionUnknown): - unknownZones = append(unknownZones, zone.DNSZone) - } - } - } - if len(failedZones) != 0 { - // TODO: Add failed condition reasons - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "FailedZones", - Message: fmt.Sprintf("The record failed to provision in some zones: %v", failedZones), - }) - } else if len(unknownZones) != 0 { - // This condition is an edge case where DNSManaged=True but - // there was an internal error during publishing record. - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionFalse, - Reason: "UnknownZones", - Message: fmt.Sprintf("Provisioning of the record is in an unknown state in some zones: %v", unknownZones), - }) - } else { - conditions = append(conditions, operatorv1.OperatorCondition{ - Type: operatorv1.DNSReadyIngressConditionType, - Status: operatorv1.ConditionTrue, - Reason: "NoFailedZones", - Message: "The record is provisioned in all reported zones.", - }) - } - } - - return conditions -} - // checkZoneInConfig - private utility to check for a zone in the current config func checkZoneInConfig(dnsConfig *configv1.DNS, zone configv1.DNSZone) bool { return zonesMatch(&zone, dnsConfig.Spec.PublicZone) || zonesMatch(&zone, dnsConfig.Spec.PrivateZone) diff --git a/pkg/operator/controller/ingress/status_test.go b/pkg/operator/controller/ingress/status_test.go index adbc19f323..418edabff2 100644 --- a/pkg/operator/controller/ingress/status_test.go +++ b/pkg/operator/controller/ingress/status_test.go @@ -17,6 +17,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/openshift/cluster-ingress-operator/pkg/resources/status" util "github.com/openshift/cluster-ingress-operator/pkg/util" retryable "github.com/openshift/cluster-ingress-operator/pkg/util/retryableerror" @@ -1643,7 +1644,9 @@ func Test_computeLoadBalancerStatus(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := computeLoadBalancerStatus(test.controller, test.service, test.events) + // The function was moved to another package, keeping the line here to + // show that no breaking change was caused + actual := status.ComputeLoadBalancerStatus(test.controller, test.service, test.events) conditionsCmpOpts := []cmp.Option{ cmpopts.IgnoreFields(operatorv1.OperatorCondition{}, "LastTransitionTime", "Message"), @@ -3257,7 +3260,9 @@ func Test_computeDNSStatus(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - actualConditions := computeDNSStatus(tc.controller, tc.record, tc.platformStatus, tc.dnsConfig) + // The function was moved to another package, keeping the line here to + // show that no breaking change was caused + actualConditions := status.ComputeDNSStatus(tc.controller, tc.record, tc.platformStatus, tc.dnsConfig) opts := cmpopts.IgnoreFields(operatorv1.OperatorCondition{}, "Message", "LastTransitionTime") if !cmp.Equal(actualConditions, tc.expect, opts) { t.Fatalf("found diff between actual and expected operator condition:\n%s", cmp.Diff(actualConditions, tc.expect, opts)) diff --git a/pkg/operator/controller/names.go b/pkg/operator/controller/names.go index 749bbf8a23..e0563d64c7 100644 --- a/pkg/operator/controller/names.go +++ b/pkg/operator/controller/names.go @@ -78,6 +78,15 @@ const ( IstioRevLabelKey = "istio.io/rev" GatewayClassIndexFieldName = "gatewayclassController" + + // GatewayNameLabelKey is the key of a label that Istio adds to + // deployments that it creates for gateways that it manages. Istio uses + // this label in the selector of any service that it creates for a + // gateway. + GatewayNameLabelKey = "gateway.networking.k8s.io/gateway-name" + // managedByIstioLabelKey is the key of a label that Istio adds to + // resources that it manages. + ManagedByIstioLabelKey = "gateway.istio.io/managed" ) // IngressClusterOperatorName returns the namespaced name of the ClusterOperator diff --git a/pkg/operator/controller/predicates.go b/pkg/operator/controller/predicates.go new file mode 100644 index 0000000000..35b8cac2f2 --- /dev/null +++ b/pkg/operator/controller/predicates.go @@ -0,0 +1,44 @@ +package controller + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// GatewayHasOurController returns a function that will use the provided logger and +// clients, receive an object and return a boolean that represents if the provided +// object is a Gateway managed by our Gateway Class +func GatewayHasOurController(logger logr.Logger, crclient client.Reader) func(o client.Object) bool { + key, value := IstioRevLabelKey, IstioName("").Name + + return func(o client.Object) bool { + gateway, ok := o.(*gatewayapiv1.Gateway) + if !ok { + return false + } + + if gateway.Labels[key] == value { + return false + } + + gatewayClassName := types.NamespacedName{ + Namespace: "", // Gatewayclasses are cluster-scoped. + Name: string(gateway.Spec.GatewayClassName), + } + var gatewayClass gatewayapiv1.GatewayClass + if err := crclient.Get(context.Background(), gatewayClassName, &gatewayClass); err != nil { + logger.Error(err, "failed to get gatewayclass for gateway", "gateway", gateway.Name, "namespace", gateway.Namespace, "gatewayclass", gatewayClassName.Name) + return false + } + + if gatewayClass.Spec.ControllerName == OpenShiftGatewayClassControllerName { + return true + } + + return false + } +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index d6c138d21f..52948c06b9 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -36,6 +36,7 @@ import ( dnscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/dns" gatewaylabelercontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/gateway-labeler" gatewayservicednscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/gateway-service-dns" + gatewaystatuscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/gateway-status" gatewayapicontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/gatewayapi" gatewayclasscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/gatewayclass" ingress "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/ingress" @@ -334,6 +335,12 @@ func New(config operatorconfig.Config, kubeConfig *rest.Config) (*Operator, erro return nil, fmt.Errorf("failed to create gateway-labeler controller: %w", err) } + // Set up the gateway-status controller. + gatewayStatusController, err := gatewaystatuscontroller.NewUnmanaged(mgr) + if err != nil { + return nil, fmt.Errorf("failed to create gateway-status controller: %w", err) + } + // Set up the gatewayapi controller. if _, err := gatewayapicontroller.New(mgr, gatewayapicontroller.Config{ GatewayAPIEnabled: gatewayAPIEnabled, @@ -344,6 +351,7 @@ func New(config operatorconfig.Config, kubeConfig *rest.Config) (*Operator, erro gatewayClassController, gatewayServiceDNSController, gatewayLabelController, + gatewayStatusController, }, }); err != nil { return nil, fmt.Errorf("failed to create gatewayapi controller: %w", err) diff --git a/pkg/resources/status/gatewayapi.go b/pkg/resources/status/gatewayapi.go new file mode 100644 index 0000000000..4365003fdb --- /dev/null +++ b/pkg/resources/status/gatewayapi.go @@ -0,0 +1,59 @@ +package status + +import ( + configv1 "github.com/openshift/api/config/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + corev1 "k8s.io/api/core/v1" + condutils "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ComputeGatewayAPIDNSStatus will update inplace DNSRecord conditions for GatewayAPI, +// using the same logic of Ingress Controller condition status. +// This function is a wrapper for the IngressController one, but deals with +// metav1.Condition and also inplace replacements as Gateway API is capable of doing +// condition merge +func ComputeGatewayAPIDNSStatus(dnsRecord *iov1.DNSRecord, dnsConfig *configv1.DNS, generation int64, existingConditions *[]metav1.Condition) { + // During the fetch of dnsRecord, it can come back as an empty structure instead + // of a null resource. In this case, we turn it into "null" again to keep + // the compatibility with ComputeLoadBalancerStatus + if dnsRecord != nil && dnsRecord.Name == "" { + dnsRecord = nil + } + ingressConditions := ComputeDNSStatus(nil, dnsRecord, nil, dnsConfig) + for _, condition := range ingressConditions { + gwCondition := metav1.Condition{ + Type: condition.Type, + Status: metav1.ConditionStatus(condition.Status), + Reason: condition.Reason, + Message: condition.Message, + ObservedGeneration: generation, + } + condutils.SetStatusCondition(existingConditions, gwCondition) + } +} + +// ComputeGatewayAPILoadBalancerStatus will update inplace LoadBalancer conditions +// for GatewayAPI, using the same logic of Ingress Controller condition status. +// This function is a wrapper for the IngressController one, but deals with +// metav1.Condition and also inplace replacements as Gateway API is capable of doing +// condition merge +func ComputeGatewayAPILoadBalancerStatus(service *corev1.Service, operandEvents []corev1.Event, generation int64, existingConditions *[]metav1.Condition) { + // During the fetch of service, it can come back as an empty structure instead + // of a null resource. In this case, we turn it into "null" again to keep + // the compatibility with ComputeLoadBalancerStatus + if service != nil && service.Name == "" { + service = nil + } + ingressConditions := ComputeLoadBalancerStatus(nil, service, operandEvents) + for _, condition := range ingressConditions { + gwCondition := metav1.Condition{ + Type: condition.Type, + Status: metav1.ConditionStatus(condition.Status), + Reason: condition.Reason, + Message: condition.Message, + ObservedGeneration: generation, + } + condutils.SetStatusCondition(existingConditions, gwCondition) + } +} diff --git a/pkg/resources/status/gatewayapi_test.go b/pkg/resources/status/gatewayapi_test.go new file mode 100644 index 0000000000..2b18150e4d --- /dev/null +++ b/pkg/resources/status/gatewayapi_test.go @@ -0,0 +1,433 @@ +package status_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + "github.com/openshift/cluster-ingress-operator/pkg/resources/status" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + defaultZoneID = "xxxx" +) + +var ( + defaultDNSConfig = &configv1.DNS{ + Spec: configv1.DNSSpec{ + PublicZone: &configv1.DNSZone{ + ID: defaultZoneID, + }, + }, + } +) + +func TestComputeGatewayAPIDNSStatus(t *testing.T) { + tests := []struct { + name string + dnsRecord *iov1.DNSRecord + dnsConfig *configv1.DNS + generation int64 + expectedConditions []metav1.Condition + }{ + { + name: "a null dnsconfig should return DNSManaged=False and NoDNSZones", + dnsConfig: nil, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "False", + Reason: "NoDNSZones", + Message: "No DNS zones are defined in the cluster dns config.", + ObservedGeneration: 1, + }, + }, + }, + { + name: "a null dnsrecord should return DNSManaged=True and DNSReady=False", + dnsConfig: defaultDNSConfig, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "False", + Reason: "RecordNotFound", + Message: "The wildcard record resource was not found.", + ObservedGeneration: 1, + }, + }, + }, + { + name: "an unmanaged dnsrecord should return DNSManaged=True and DNSReady=False with Reason=UnmanagedDNS", + dnsConfig: defaultDNSConfig, + dnsRecord: &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somedns", + }, + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.UnmanagedDNS, + }, + }, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "Unknown", + Reason: "UnmanagedDNS", + Message: "The DNS management policy is set to Unmanaged.", + ObservedGeneration: 1, + }, + }, + }, + { + name: "a dnsrecord without zones should return DNSManaged=True and DNSReady=False with Reason=NoZones", + dnsConfig: defaultDNSConfig, + dnsRecord: &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somedns", + }, + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{}, + }, + }, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "False", + Reason: "NoZones", + Message: "The record isn't present in any zones.", + ObservedGeneration: 1, + }, + }, + }, + { + name: "a dnsrecord with failed zones should return DNSManaged=True and DNSReady=False with Reason=FailedZones", + dnsConfig: defaultDNSConfig, + dnsRecord: &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somedns", + }, + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ + ID: defaultZoneID, + }, + Conditions: []iov1.DNSZoneCondition{ + { + Type: "Published", + Status: "False", + }, + }, + }, + }, + }, + }, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "False", + Reason: "FailedZones", + Message: "The record failed to provision in some zones: [{xxxx map[]}]", + ObservedGeneration: 1, + }, + }, + }, + { + name: "a dnsrecord with unknown zones should return DNSManaged=True and DNSReady=False with Reason=UnknownZones", + dnsConfig: defaultDNSConfig, + dnsRecord: &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somedns", + }, + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ + ID: defaultZoneID, + }, + Conditions: []iov1.DNSZoneCondition{ + { + Type: "Published", + Status: "Unknown", + }, + }, + }, + }, + }, + }, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "False", + Reason: "UnknownZones", + Message: "Provisioning of the record is in an unknown state in some zones: [{xxxx map[]}]", + ObservedGeneration: 1, + }, + }, + }, + { + name: "a dnsrecord with valid zones should return DNSManaged=True and DNSReady=False with Reason=NoFailedZones", + dnsConfig: defaultDNSConfig, + dnsRecord: &iov1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somedns", + }, + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ + ID: defaultZoneID, + }, + Conditions: []iov1.DNSZoneCondition{ + { + Type: "Published", + Status: "True", + }, + }, + }, + }, + }, + }, + generation: 1, + expectedConditions: []metav1.Condition{ + { + Type: "DNSManaged", + Status: "True", + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + ObservedGeneration: 1, + }, + { + Type: "DNSReady", + Status: "True", + Reason: "NoFailedZones", + Message: "The record is provisioned in all reported zones.", + ObservedGeneration: 1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conditions := make([]metav1.Condition, 0) + status.ComputeGatewayAPIDNSStatus(tt.dnsRecord, tt.dnsConfig, tt.generation, &conditions) + conditionsCmpOpts := []cmp.Option{ + cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b operatorv1.OperatorCondition) bool { return a.Type < b.Type }), + } + if !cmp.Equal(conditions, tt.expectedConditions, conditionsCmpOpts...) { + t.Fatalf("expected:\n%#v\ngot:\n%#v", tt.expectedConditions, conditions) + } + }) + } +} + +func TestComputeGatewayAPILoadBalancerStatus(t *testing.T) { + tests := []struct { + name string + service *corev1.Service + events []corev1.Event + generation int64 + expectedConditions []metav1.Condition + }{ + { + name: "null service should return LoadBalancerManaged=True and LoadBalancerReady=False with reason=ServiceNotFound", + service: nil, + generation: 1, + events: []corev1.Event{}, + expectedConditions: []metav1.Condition{ + { + Type: "LoadBalancerManaged", + Status: "True", + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + ObservedGeneration: 1, + }, + { + Type: "LoadBalancerReady", + Status: "False", + Reason: "ServiceNotFound", + Message: "The LoadBalancer service resource is missing", + ObservedGeneration: 1, + }, + }, + }, + { + name: "service provisioned and with status should return LoadBalancerManaged=True and LoadBalancerReady=True with reason=LoadBalancerProvisioned", + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somesvc", + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + Hostname: "xpto.tld", + }, + }, + }, + }, + }, + generation: 1, + events: []corev1.Event{}, + expectedConditions: []metav1.Condition{ + { + Type: "LoadBalancerManaged", + Status: "True", + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + ObservedGeneration: 1, + }, + { + Type: "LoadBalancerReady", + Status: "True", + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + ObservedGeneration: 1, + }, + }, + }, + { + name: "service with missing status and missing events should return LoadBalancerReady=False with reason=LoadBalancerPending", + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somesvc", + }, + Status: corev1.ServiceStatus{}, + }, + generation: 1, + events: []corev1.Event{}, + expectedConditions: []metav1.Condition{ + { + Type: "LoadBalancerManaged", + Status: "True", + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + ObservedGeneration: 1, + }, + { + Type: "LoadBalancerReady", + Status: "False", + Reason: "LoadBalancerPending", + Message: "The LoadBalancer service is pending", + ObservedGeneration: 1, + }, + }, + }, + { + name: "service with missing status and related events should return LoadBalancerReady=False with reason=SyncLoadBalancerFailed", + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "someservice", + Namespace: "somens", + UID: "2", + }, + Status: corev1.ServiceStatus{}, + }, + generation: 1, + events: []corev1.Event{ + { + InvolvedObject: corev1.ObjectReference{ + Kind: "Service", + Namespace: "somens", + Name: "someservice", + UID: "2", + }, + Source: corev1.EventSource{ + Component: "service-controller", + }, + Reason: "SyncLoadBalancerFailed", + Message: "failed", + }, + }, + expectedConditions: []metav1.Condition{ + { + Type: "LoadBalancerManaged", + Status: "True", + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + ObservedGeneration: 1, + }, + { + Type: "LoadBalancerReady", + Status: "False", + Reason: "SyncLoadBalancerFailed", + Message: "The service-controller component is reporting SyncLoadBalancerFailed events like: failed\nThe cloud-controller-manager logs may contain more details.", + ObservedGeneration: 1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conditions := make([]metav1.Condition, 0) + status.ComputeGatewayAPILoadBalancerStatus(tt.service, tt.events, tt.generation, &conditions) + conditionsCmpOpts := []cmp.Option{ + cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b operatorv1.OperatorCondition) bool { return a.Type < b.Type }), + } + if !cmp.Equal(conditions, tt.expectedConditions, conditionsCmpOpts...) { + t.Fatalf("expected:\n%#v\ngot:\n%#v", tt.expectedConditions, conditions) + } + }) + } +} diff --git a/pkg/resources/status/status.go b/pkg/resources/status/status.go new file mode 100644 index 0000000000..7bf6f1aa05 --- /dev/null +++ b/pkg/resources/status/status.go @@ -0,0 +1,289 @@ +package status + +import ( + "fmt" + + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + corev1 "k8s.io/api/core/v1" + utilclock "k8s.io/utils/clock" +) + +// clock is to enable unit testing +var clock utilclock.Clock = utilclock.RealClock{} + +// ComputeDNSStatus computes the status conditions based on the DNSRecord, the current +// configuration of an Ingress Controller, and the Platform. It will return an array of +// conditions that signals if a DNS record for a resource could be published, +// and the reason on success or failure +func ComputeDNSStatus(ic *operatorv1.IngressController, wildcardRecord *iov1.DNSRecord, status *configv1.PlatformStatus, dnsConfig *configv1.DNS) []operatorv1.OperatorCondition { + // In case there is no managed DNS zone configured, return a single condition + // that DNSManaged=False because no zone is configured + if dnsConfig == nil || (dnsConfig.Spec.PublicZone == nil && dnsConfig.Spec.PrivateZone == nil) { + return []operatorv1.OperatorCondition{ + { + Type: operatorv1.DNSManagedIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "NoDNSZones", + Message: "No DNS zones are defined in the cluster dns config.", + }, + } + } + + // The condition below is only possible when an ingress controller is requesting + // a DNS. In case the ingresscontroller resource is null (eg.: GatewayAPI) + // this condition will not be verified. + // Otherwise, it will return a single condition that DNSManaged=False in case + // the EndpointPublishingStrategy is not "LoadBalancerService" + if ic != nil && ic.Status.EndpointPublishingStrategy.Type != operatorv1.LoadBalancerServiceStrategyType { + return []operatorv1.OperatorCondition{ + { + Type: operatorv1.DNSManagedIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "UnsupportedEndpointPublishingStrategy", + Message: "The endpoint publishing strategy doesn't support DNS management.", + }, + } + } + var conditions []operatorv1.OperatorCondition + + // In case this is an ingresscontroller resource, and it contains a DNSManagementPolicy = 'Unmanaged' + // the "DNSManaged" condition should be false, and in any other case (GatewayAPI, ManagedDNS) + // return the condition as True + if ic != nil && ic.Status.EndpointPublishingStrategy.LoadBalancer != nil && ic.Status.EndpointPublishingStrategy.LoadBalancer.DNSManagementPolicy == operatorv1.UnmanagedLoadBalancerDNS { + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSManagedIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "UnmanagedLoadBalancerDNS", + Message: "The DNS management policy is set to Unmanaged.", + }) + } else { + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSManagedIngressConditionType, + Status: operatorv1.ConditionTrue, + Reason: "Normal", + Message: "DNS management is supported and zones are specified in the cluster DNS config.", + }) + } + + // The switch below is specific for the "DNSReady" condition. + switch { + // A null wildcardRecord means that getting the DNSRecord failed by a number of + // reasons, including it may not be found/created, and in this case DNSReady="False" + // as no DNSRecord is found + case wildcardRecord == nil: + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "RecordNotFound", + Message: "The wildcard record resource was not found.", + }) + // In case the ManagementPolicy of a DNSRecord = "Unmanaged", this means our + // controller will not reconcile it, and in this case DNSReady should be Unknown + case wildcardRecord.Spec.DNSManagementPolicy == iov1.UnmanagedDNS: + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionUnknown, + Reason: "UnmanagedDNS", + Message: "The DNS management policy is set to Unmanaged.", + }) + // In case the DNSRecord contains no zones, this means that this record couldn't + // fit into any DNSZone, and in this case DNSReady=False with the Reason of "NoZones" + case len(wildcardRecord.Status.Zones) == 0: + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "NoZones", + Message: "The record isn't present in any zones.", + }) + case len(wildcardRecord.Status.Zones) > 0: + var failedZones []configv1.DNSZone + var unknownZones []configv1.DNSZone + // The loop below will check if any of the zones existing on DNSRecord + // status (status.zones.condition["Published"].Status) are failed (Status=False) + // or unknown (Status=Unknown) and in this case, it will reflect on the + // returned conditions with DNSReady=False and reason being either "FailedZones" + // or "UnknownZones" + for _, zone := range wildcardRecord.Status.Zones { + for _, cond := range zone.Conditions { + if cond.Type != iov1.DNSRecordPublishedConditionType { + continue + } + if !checkZoneInConfig(dnsConfig, zone.DNSZone) { + continue + } + switch cond.Status { + case string(operatorv1.ConditionFalse): + // check to see if the zone is in the dnsConfig.Spec + // fix:BZ1942657 - relates to status changes when updating DNS PrivateZone config + failedZones = append(failedZones, zone.DNSZone) + case string(operatorv1.ConditionUnknown): + unknownZones = append(unknownZones, zone.DNSZone) + } + } + } + if len(failedZones) != 0 { + // TODO: Add failed condition reasons + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "FailedZones", + Message: fmt.Sprintf("The record failed to provision in some zones: %v", failedZones), + }) + } else if len(unknownZones) != 0 { + // This condition is an edge case where DNSManaged=True but + // there was an internal error during publishing record. + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "UnknownZones", + Message: fmt.Sprintf("Provisioning of the record is in an unknown state in some zones: %v", unknownZones), + }) + } else { + // Otherwise if no status.zones.condition["Published"].Status is False + // add the condition DNSReady=True + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.DNSReadyIngressConditionType, + Status: operatorv1.ConditionTrue, + Reason: "NoFailedZones", + Message: "The record is provisioned in all reported zones.", + }) + } + } + + return conditions +} + +// ComputeLoadBalancerStatus returns the set of current +// LoadBalancer-prefixed conditions for the given ingress controller, which are +// used later to determine the ingress controller's Degraded or Available status. +func ComputeLoadBalancerStatus(ic *operatorv1.IngressController, service *corev1.Service, operandEvents []corev1.Event) []operatorv1.OperatorCondition { + // Compute the LoadBalancerManagedIngressConditionType condition + // The condition below is only possible when an ingress controller is requesting + // a LoadBalancer. In case the ingresscontroller resource is null (eg.: GatewayAPI, + // that has the LoadBalancer managed by Istio) this condition will not be verified. + // Otherwise, it will return a single condition that LoadBalancerManaged=False in case + // the EndpointPublishingStrategy is not "LoadBalancerService" + if ic != nil && (ic.Status.EndpointPublishingStrategy == nil || + ic.Status.EndpointPublishingStrategy.Type != operatorv1.LoadBalancerServiceStrategyType) { + return []operatorv1.OperatorCondition{ + { + Type: operatorv1.LoadBalancerManagedIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "EndpointPublishingStrategyExcludesManagedLoadBalancer", + Message: "The configured endpoint publishing strategy does not include a managed load balancer", + }, + } + } + + conditions := []operatorv1.OperatorCondition{} + + // Initial condition for any resource that is managed is LoadBalancerManaged=True + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.LoadBalancerManagedIngressConditionType, + Status: operatorv1.ConditionTrue, + Reason: "WantedByEndpointPublishingStrategy", + Message: "The endpoint publishing strategy supports a managed load balancer", + }) + + // Compute the LoadBalancerReadyIngressConditionType (LoadBalancerReady) condition + switch { + // A null service means that getting the Service failed by some reason like + // not be found/created, and in this case LoadBalancerReady="False" + // as no service is found + case service == nil: + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: "ServiceNotFound", + Message: "The LoadBalancer service resource is missing", + }) + // In case the service is created and has a .status.Loadbalancer.Ingress populated + // by a service/loadbalancer controller, the LoadBalancerReady can be marked as "True" + case isProvisioned(service): + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: operatorv1.ConditionTrue, + Reason: "LoadBalancerProvisioned", + Message: "The LoadBalancer service is provisioned", + }) + // In case the service is created but does not has a .status.Loadbalancer.Ingress populated + // by a service/loadbalancer controller, the LoadBalancerReady can be marked as "False" + // defaulting the reason to LoadBalancerPending. + // As our service controller creates events that can be helpful to give additional + // context to users, on a best-effort we try to get the events related with this + // service to provide a better context on the condition + case isPending(service): + reason := "LoadBalancerPending" + message := "The LoadBalancer service is pending" + + // Try and find a more specific reason for for the pending status. + createFailedReason := "SyncLoadBalancerFailed" + failedLoadBalancerEvents := getEventsByReason(operandEvents, "service-controller", createFailedReason) + for _, event := range failedLoadBalancerEvents { + involved := event.InvolvedObject + if involved.Kind == "Service" && involved.Namespace == service.Namespace && involved.Name == service.Name && involved.UID == service.UID { + reason = "SyncLoadBalancerFailed" + message = fmt.Sprintf("The %s component is reporting SyncLoadBalancerFailed events like: %s\n%s", + event.Source.Component, event.Message, "The cloud-controller-manager logs may contain more details.") + break + } + } + conditions = append(conditions, operatorv1.OperatorCondition{ + Type: operatorv1.LoadBalancerReadyIngressConditionType, + Status: operatorv1.ConditionFalse, + Reason: reason, + Message: message, + }) + } + return conditions +} + +// getEventsByReason receives a list of events, and returns a filtered list +// containing just the events that were generated by a specific component and reason +func getEventsByReason(events []corev1.Event, component, reason string) []corev1.Event { + var filtered []corev1.Event + for i := range events { + event := events[i] + if event.Source.Component == component && event.Reason == reason { + filtered = append(filtered, event) + } + } + return filtered +} + +// isProvisioned defines if a service contains a .status.LoadBalancer.Ingress +func isProvisioned(service *corev1.Service) bool { + ingresses := service.Status.LoadBalancer.Ingress + return len(ingresses) > 0 && (len(ingresses[0].Hostname) > 0 || len(ingresses[0].IP) > 0) +} + +// isPending defines if a service does not contains a .status.LoadBalancer.Ingress +func isPending(service *corev1.Service) bool { + return !isProvisioned(service) +} + +// checkZoneInConfig - private utility to check for a zone in the current config +func checkZoneInConfig(dnsConfig *configv1.DNS, zone configv1.DNSZone) bool { + return zonesMatch(&zone, dnsConfig.Spec.PublicZone) || zonesMatch(&zone, dnsConfig.Spec.PrivateZone) +} + +// zonesMatch returns a Boolean value indicating whether two DNS zones have the +// matching ID or "Name" tag. If either or both zones are nil, this function +// returns false. +func zonesMatch(a, b *configv1.DNSZone) bool { + if a == nil || b == nil { + return false + } + + if a.ID != "" && b.ID != "" && a.ID == b.ID { + return true + } + + if a.Tags["Name"] != "" && b.Tags["Name"] != "" && a.Tags["Name"] == b.Tags["Name"] { + return true + } + + return false +} diff --git a/pkg/resources/status/status_test.go b/pkg/resources/status/status_test.go new file mode 100644 index 0000000000..f69904427f --- /dev/null +++ b/pkg/resources/status/status_test.go @@ -0,0 +1,750 @@ +package status + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + iov1 "github.com/openshift/api/operatoringress/v1" + "github.com/openshift/cluster-ingress-operator/pkg/manifests" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func Test_computeLoadBalancerStatus(t *testing.T) { + tests := []struct { + name string + controller *operatorv1.IngressController + service *corev1.Service + events []corev1.Event + expect []operatorv1.OperatorCondition + }{ + { + name: "lb provisioned", + controller: ingressController("default", operatorv1.LoadBalancerServiceStrategyType), + service: provisionedLBservice("default"), + expect: []operatorv1.OperatorCondition{ + cond(operatorv1.LoadBalancerManagedIngressConditionType, operatorv1.ConditionTrue, "WantedByEndpointPublishingStrategy", clock.Now()), + cond(operatorv1.LoadBalancerReadyIngressConditionType, operatorv1.ConditionTrue, "LoadBalancerProvisioned", clock.Now()), + }, + }, + { + name: "no events for current lb", + controller: ingressController("default", operatorv1.LoadBalancerServiceStrategyType), + service: pendingLBService("default", "1"), + events: []corev1.Event{ + schedulerEvent(), + failedCreateLBEvent("secondary", "2"), + failedCreateLBEvent("default", "3"), + }, + expect: []operatorv1.OperatorCondition{ + cond(operatorv1.LoadBalancerManagedIngressConditionType, operatorv1.ConditionTrue, "WantedByEndpointPublishingStrategy", clock.Now()), + cond(operatorv1.LoadBalancerReadyIngressConditionType, operatorv1.ConditionFalse, "LoadBalancerPending", clock.Now()), + }, + }, + { + name: "lb pending, create failed events", + controller: ingressController("default", operatorv1.LoadBalancerServiceStrategyType), + service: pendingLBService("default", "1"), + events: []corev1.Event{ + schedulerEvent(), + failedCreateLBEvent("secondary", "3"), + failedCreateLBEvent("default", "1"), + }, + expect: []operatorv1.OperatorCondition{ + cond(operatorv1.LoadBalancerManagedIngressConditionType, operatorv1.ConditionTrue, "WantedByEndpointPublishingStrategy", clock.Now()), + cond(operatorv1.LoadBalancerReadyIngressConditionType, operatorv1.ConditionFalse, "SyncLoadBalancerFailed", clock.Now()), + }, + }, + { + name: "unmanaged", + controller: ingressController("default", operatorv1.HostNetworkStrategyType), + expect: []operatorv1.OperatorCondition{ + cond(operatorv1.LoadBalancerManagedIngressConditionType, operatorv1.ConditionFalse, "EndpointPublishingStrategyExcludesManagedLoadBalancer", clock.Now()), + }, + }, + { + name: "lb service missing", + controller: ingressController("default", operatorv1.LoadBalancerServiceStrategyType), + expect: []operatorv1.OperatorCondition{ + cond(operatorv1.LoadBalancerManagedIngressConditionType, operatorv1.ConditionTrue, "WantedByEndpointPublishingStrategy", clock.Now()), + cond(operatorv1.LoadBalancerReadyIngressConditionType, operatorv1.ConditionFalse, "ServiceNotFound", clock.Now()), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := ComputeLoadBalancerStatus(test.controller, test.service, test.events) + + conditionsCmpOpts := []cmp.Option{ + cmpopts.IgnoreFields(operatorv1.OperatorCondition{}, "LastTransitionTime", "Message"), + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b operatorv1.OperatorCondition) bool { return a.Type < b.Type }), + } + if !cmp.Equal(actual, test.expect, conditionsCmpOpts...) { + t.Fatalf("expected:\n%#v\ngot:\n%#v", test.expect, actual) + } + }) + } +} + +func Test_computeDNSStatus(t *testing.T) { + tests := []struct { + name string + controller *operatorv1.IngressController + record *iov1.DNSRecord + platformStatus *configv1.PlatformStatus + dnsConfig *configv1.DNS + expect []operatorv1.OperatorCondition + }{ + { + name: "DNSManaged false due to NoDNSZones", + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + PublicZone: nil, + PrivateZone: nil, + }, + }, + expect: []operatorv1.OperatorCondition{{ + Type: "DNSManaged", + Status: operatorv1.ConditionFalse, + Reason: "NoDNSZones", + }}, + }, + { + name: "DNSManaged false due to UnsupportedEndpointPublishingStrategy", + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.HostNetworkStrategyType, + }, + }, + }, + expect: []operatorv1.OperatorCondition{{ + Type: "DNSManaged", + Status: operatorv1.ConditionFalse, + Reason: "UnsupportedEndpointPublishingStrategy", + }}, + }, + { + name: "DNSManaged false due to UnmanagedLoadBalancerDNS", + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.UnmanagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.UnmanagedDNS, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionFalse, + Reason: "UnmanagedLoadBalancerDNS", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionUnknown, + Reason: "UnmanagedDNS", + }, + }, + }, + { + name: "DNSManaged true due to dnsManagementPolicy=Managed, and DNSReady is true due to NoFailedZones", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionTrue), + LastTransitionTime: metav1.Now(), + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionTrue, + Reason: "NoFailedZones", + }, + }, + }, + { + name: "DNSManaged true due to nil status.endpointPublishingStrategy.loadBalancer, and DNSReady is true due to NoFailedZones", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: nil, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionTrue), + LastTransitionTime: metav1.Now(), + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionTrue, + Reason: "NoFailedZones", + }, + }, + }, + { + name: "DNSManaged true but DNSReady is false due to RecordNotFound", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: nil, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionFalse, + Reason: "RecordNotFound", + }, + }, + }, + { + name: "DNSManaged true but DNSReady is false due to NoZones", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{}, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionFalse, + Reason: "NoZones", + }, + }, + }, + { + name: "DNSManaged true but DNSReady is Unknown due to UnmanagedDNS", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.UnmanagedDNS, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{}, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionUnknown, + Reason: "UnmanagedDNS", + }, + }, + }, + { + name: "DNSManaged true and DNSReady is false due to FailedZones", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionFalse), + LastTransitionTime: metav1.Now(), + Reason: "FailedZones", + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{ + ID: "zone1", + }, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionFalse, + Reason: "FailedZones", + }, + }, + }, + { + name: "DNSManaged true and DNSReady is true with even with previously FailedZones", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordFailedConditionType, + Status: string(operatorv1.ConditionTrue), + LastTransitionTime: metav1.NewTime(time.Now().Add(5 * time.Minute)), + Reason: "FailedZones", + }, + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionTrue), + LastTransitionTime: metav1.NewTime(time.Now().Add(15 * time.Minute)), + Reason: "NoFailedZones", + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{ + ID: "zone1", + }, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionTrue, + Reason: "NoFailedZones", + }, + }, + }, + { + name: "DNSManaged true and DNSReady is false due to unknown condition", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionUnknown), + LastTransitionTime: metav1.NewTime(time.Now().Add(15 * time.Minute)), + Reason: "UnknownZones", + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{ + ID: "zone1", + }, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionFalse, + Reason: "UnknownZones", + }, + }, + }, + { + // This text checks if precedence is given to failed zones over unknown zones. + name: "DNSManaged true and DNSReady is false due to failed and unknown conditions", + controller: &operatorv1.IngressController{ + Status: operatorv1.IngressControllerStatus{ + Domain: "apps.basedomain.com", + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: operatorv1.LoadBalancerServiceStrategyType, + LoadBalancer: &operatorv1.LoadBalancerStrategy{ + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + }, + }, + }, + }, + record: &iov1.DNSRecord{ + Spec: iov1.DNSRecordSpec{ + DNSManagementPolicy: iov1.ManagedDNS, + }, + Status: iov1.DNSRecordStatus{ + Zones: []iov1.DNSZoneStatus{ + { + DNSZone: configv1.DNSZone{ID: "zone1"}, + Conditions: []iov1.DNSZoneCondition{ + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionUnknown), + LastTransitionTime: metav1.NewTime(time.Now().Add(15 * time.Minute)), + Reason: "UnknownZones", + }, + { + Type: iov1.DNSRecordPublishedConditionType, + Status: string(operatorv1.ConditionFalse), + LastTransitionTime: metav1.NewTime(time.Now().Add(15 * time.Minute)), + Reason: "FailedZones", + }, + }, + }, + }, + }, + }, + platformStatus: &configv1.PlatformStatus{ + Type: configv1.AWSPlatformType, + }, + dnsConfig: &configv1.DNS{ + Spec: configv1.DNSSpec{ + BaseDomain: "basedomain.com", + PublicZone: &configv1.DNSZone{}, + PrivateZone: &configv1.DNSZone{ + ID: "zone1", + }, + }, + }, + expect: []operatorv1.OperatorCondition{ + { + Type: "DNSManaged", + Status: operatorv1.ConditionTrue, + Reason: "Normal", + }, + { + Type: "DNSReady", + Status: operatorv1.ConditionFalse, + Reason: "FailedZones", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actualConditions := ComputeDNSStatus(tc.controller, tc.record, tc.platformStatus, tc.dnsConfig) + opts := cmpopts.IgnoreFields(operatorv1.OperatorCondition{}, "Message", "LastTransitionTime") + if !cmp.Equal(actualConditions, tc.expect, opts) { + t.Fatalf("found diff between actual and expected operator condition:\n%s", cmp.Diff(actualConditions, tc.expect, opts)) + } + }) + } +} + +func ingressController(name string, t operatorv1.EndpointPublishingStrategyType) *operatorv1.IngressController { + return &operatorv1.IngressController{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Status: operatorv1.IngressControllerStatus{ + EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{ + Type: t, + }, + }, + } +} + +func pendingLBService(owner string, UID types.UID) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: owner, + Labels: map[string]string{ + manifests.OwningIngressControllerLabel: owner, + }, + UID: UID, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + }, + } +} + +func provisionedLBservice(owner string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: owner, + Labels: map[string]string{ + manifests.OwningIngressControllerLabel: owner, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + {Hostname: "lb.cloudprovider.example.com"}, + }, + }, + }, + } +} + +func failedCreateLBEvent(service string, UID types.UID) corev1.Event { + return corev1.Event{ + Type: "Warning", + Reason: "SyncLoadBalancerFailed", + Message: "failed to ensure load balancer for service openshift-ingress/router-default: TooManyLoadBalancers: Exceeded quota of account", + Source: corev1.EventSource{ + Component: "service-controller", + }, + InvolvedObject: corev1.ObjectReference{ + Kind: "Service", + Name: service, + UID: UID, + }, + } +} + +func schedulerEvent() corev1.Event { + return corev1.Event{ + Type: "Normal", + Reason: "Scheduled", + Source: corev1.EventSource{ + Component: "default-scheduler", + }, + InvolvedObject: corev1.ObjectReference{ + Kind: "Pod", + Name: "router-default-1", + }, + } +} + +func cond(t string, status operatorv1.ConditionStatus, reason string, lt time.Time) operatorv1.OperatorCondition { + return operatorv1.OperatorCondition{ + Type: t, + Status: status, + Reason: reason, + LastTransitionTime: metav1.NewTime(lt), + } +} diff --git a/test/e2e/gateway_api_test.go b/test/e2e/gateway_api_test.go index d88ea03dbe..af3ae5e201 100644 --- a/test/e2e/gateway_api_test.go +++ b/test/e2e/gateway_api_test.go @@ -6,6 +6,7 @@ package e2e import ( "context" "fmt" + "math/rand/v2" "strings" "testing" "time" @@ -16,6 +17,10 @@ import ( operatorclient "github.com/openshift/cluster-ingress-operator/pkg/operator/client" operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" util "github.com/openshift/cluster-ingress-operator/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + condutils "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/fields" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -101,6 +106,7 @@ func TestGatewayAPI(t *testing.T) { t.Run("testGatewayAPIDNS", testGatewayAPIDNS) t.Run("testGatewayAPIDNSListenerUpdate", testGatewayAPIDNSListenerUpdate) t.Run("testGatewayAPIDNSListenerWithNoHostname", testGatewayAPIDNSListenerWithNoHostname) + t.Run("testGatewayOpenshiftConditions", testGatewayOpenshiftConditions) } else { t.Log("Gateway API Controller not enabled, skipping controller tests") @@ -587,7 +593,381 @@ func testGatewayAPIDNS(t *testing.T) { } }) } +} + +// This e2e test will verify the following scenarios: +// 1 - Creating a gateway with the right base domain but outside of `openshift-ingress` +// namespace will not generate a DNSRecord nor add conditions to the gateway +// 2 - Creating a Gateway on `openshift-ingress` namespace using the wrong base +// domain should add DNS conditions that there are no managed zones for this case +// 3 - Creating a Gateway with the right base domain on `openshift-ingress` will +// add the conditions on the gateway reflecting the right status of LoadBalancer and DNSRecord +// 4 - Bumping some label on the Gateway should trigger a reconciliation that will +// bump the generation of conditions +// 5 - Adding a label on DNSRecord and/or Service will trigger a reconciliation +// that should be verified by a newly recorded event +func testGatewayOpenshiftConditions(t *testing.T) { + if infraConfig.Status.PlatformStatus == nil { + t.Skip("test skipped on nil platform") + } + + if infraConfig.Status.PlatformStatus.Type != configv1.AWSPlatformType && infraConfig.Status.PlatformStatus.Type != configv1.GCPPlatformType { + t.Skip("test skipped on non-aws or non-gcp platform") + } + + domain := "gwcondtest." + dnsConfig.Spec.BaseDomain + + gatewayClass, err := createGatewayClass(t, operatorcontroller.OpenShiftDefaultGatewayClassName, operatorcontroller.OpenShiftGatewayClassControllerName) + require.NoError(t, err, "failed to create gatewayclass") + + t.Run("creating a new gateway outside of 'openshift-ingress' namespace should not get openshift conditions", func(t *testing.T) { + rnd := rand.IntN(1000) + name := fmt.Sprintf("gw-test-%d", rnd) + testDomain := fmt.Sprintf("some-%d.%s", rnd, domain) + gateway, err := createGateway(gatewayClass, name, "default", testDomain) + require.NoError(t, err, "failed to create gateway", "name", name) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(kclient.Delete(context.TODO(), gateway)), "failed to clean test gateway", "name", name) + }) + + gateway, err = assertGatewaySuccessful(t, "default", name) + require.NoError(t, err, "failed waiting gateway to be ready") + // Give some time to guarantee our controller will watch the change but ignore it + time.Sleep(time.Second) + + // Get gateway a 2nd time to check the conditions + gateway, err = assertGatewaySuccessful(t, "default", name) + require.NoError(t, err, "failed waiting gateway to have conditions") + require.Nil(t, condutils.FindStatusCondition(gateway.Status.Conditions, "DNSManaged"), "condition should not be present") + require.Nil(t, condutils.FindStatusCondition(gateway.Status.Conditions, "DNSReady"), "condition should not be present") + require.Nil(t, condutils.FindStatusCondition(gateway.Status.Conditions, "LoadBalancerManaged"), "condition should not be present") + require.Nil(t, condutils.FindStatusCondition(gateway.Status.Conditions, "LoadBalancerReady"), "condition should not be present") + }) + + t.Run("creating a new gateway with the wrong base domain should add openshift conditions reflecting the failure", func(t *testing.T) { + rnd := rand.IntN(1000) + name := fmt.Sprintf("gw-test-%d", rnd) + testDomain := fmt.Sprintf("some-%d.not.something.managed.tld", rnd) + gateway, err := createGateway(gatewayClass, name, operatorcontroller.DefaultOperandNamespace, testDomain) + require.NoError(t, err, "failed to create gateway", "name", name) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(kclient.Delete(context.TODO(), gateway)), "failed to clean test gateway", "name", name) + }) + + gateway, err = assertGatewaySuccessful(t, operatorcontroller.DefaultOperandNamespace, name) + require.NoError(t, err, "failed waiting gateway to be ready") + + assert.Eventuallyf(t, func() bool { + gw := &gatewayapiv1.Gateway{} + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + if err := kclient.Get(context.Background(), nsName, gw); err != nil { + t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) + return false + } + + if condutils.IsStatusConditionTrue(gw.Status.Conditions, "DNSManaged") && + condutils.IsStatusConditionPresentAndEqual(gw.Status.Conditions, "DNSReady", metav1.ConditionUnknown) && + condutils.IsStatusConditionTrue(gw.Status.Conditions, "LoadBalancerManaged") && + condutils.IsStatusConditionTrue(gw.Status.Conditions, "LoadBalancerReady") { + + return true + } + t.Logf("conditions are not yet the expected: %v, retrying...", gw.Status.Conditions) + return false + }, 30*time.Second, 2*time.Second, "error waiting for openshift conditions to be present on Gateway") + }) + + /* + {DNSManaged True 1 2025-10-15 22:22:53 +0000 UTC Normal DNS management is supported and zones are specified in the cluster DNS config.} {DNSReady True 1 2025-10-15 22:23:20 +0000 UTC NoFailedZones The record is provisioned in all reported zones.} {LoadBalancerManaged True 1 2025-10-15 22:22:53 +0000 UTC WantedByEndpointPublishingStrategy The endpoint publishing strategy supports a managed load balancer} {LoadBalancerReady True 1 2025-10-15 22:23:18 +0000 UTC LoadBalancerProvisioned The LoadBalancer service is provisioned}]*/ + + t.Run("creating a new gateway with the right base domain", func(t *testing.T) { + rnd := rand.IntN(1000) + name := fmt.Sprintf("gw-test-%d", rnd) + testDomain := fmt.Sprintf("some-%d.%s", rnd, domain) + gateway, err := createGateway(gatewayClass, name, operatorcontroller.DefaultOperandNamespace, testDomain) + require.NoError(t, err, "failed to create gateway", "name", name) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(kclient.Delete(context.TODO(), gateway)), "failed to clean test gateway", "name", name) + }) + + gateway, err = assertGatewaySuccessful(t, operatorcontroller.DefaultOperandNamespace, name) + require.NoError(t, err, "failed waiting gateway to be ready") + + err = assertExpectedDNSRecords(t, map[expectedDnsRecord]bool{ + {dnsName: "*." + testDomain + ".", gatewayName: name}: true}) + + assert.NoError(t, err, "dnsrecord never got ready") + + t.Run("should add openshift conditions", func(t *testing.T) { + assert.Eventuallyf(t, func() bool { + gw := &gatewayapiv1.Gateway{} + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + if err := kclient.Get(context.Background(), nsName, gw); err != nil { + t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) + return false + } + + if condutils.IsStatusConditionTrue(gw.Status.Conditions, "DNSManaged") && + condutils.IsStatusConditionTrue(gw.Status.Conditions, "DNSReady") && + condutils.IsStatusConditionTrue(gw.Status.Conditions, "LoadBalancerManaged") && + condutils.IsStatusConditionTrue(gw.Status.Conditions, "LoadBalancerReady") { + + return true + } + t.Logf("conditions are not yet the expected: %v, retrying...", gw.Status.Conditions) + return false + }, 30*time.Second, 2*time.Second, "error waiting for openshift conditions to be present on Gateway") + + // Check also for the existing event + assert.Eventually(t, func() bool { + events, err := getMatchingEventsFromGateway(t, kclient, gateway, "Normal", "AddedConditions") + if err != nil { + t.Logf("error fetching the events from namespace: %s; retrying...", err) + return false + } + t.Logf("events found: (%d): %+v", len(events.Items), events.Items) + return len(events.Items) > 0 + }, 30*time.Second, 2*time.Second, "error fetching matching resource to add conditions") + }) + + t.Run("should bump openshift conditions when the gateway is changed", func(t *testing.T) { + // Try to add a new infrastructure label, forcing the generation to bump + originalGateway := &gatewayapiv1.Gateway{} + assert.Eventually(t, func() bool { + gw := &gatewayapiv1.Gateway{} + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + if err := kclient.Get(context.Background(), nsName, gw); err != nil { + t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) + return false + } + originalGateway = gw.DeepCopy() + if gw.Spec.Infrastructure == nil { + gw.Spec.Infrastructure = &gatewayapiv1.GatewayInfrastructure{} + } + if gw.Spec.Infrastructure.Labels == nil { + gw.Spec.Infrastructure.Labels = make(map[gatewayapiv1.LabelKey]gatewayapiv1.LabelValue) + } + + gw.Spec.Infrastructure.Labels["something"] = "somelabel" + + if err := kclient.Patch(context.Background(), gw, client.MergeFrom(originalGateway)); err != nil { + t.Logf("failed to patch gateway %v: %v; retrying...", nsName, err) + return false + } + return true + }, 30*time.Second, 2*time.Second, "timeout waiting to patch the gateway") + + gw := &gatewayapiv1.Gateway{} + // Get the Gateway and check if conditions are there, and if their generation are different from the originalGateway value + assert.Eventually(t, func() bool { + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + if err := kclient.Get(context.Background(), nsName, gw); err != nil { + t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) + return false + } + + dnsManaged := condutils.FindStatusCondition(gw.Status.Conditions, "DNSManaged") + dnsReady := condutils.FindStatusCondition(gw.Status.Conditions, "DNSReady") + loadBalancerManaged := condutils.FindStatusCondition(gw.Status.Conditions, "LoadBalancerManaged") + loadBalancerReady := condutils.FindStatusCondition(gw.Status.Conditions, "LoadBalancerReady") + + // Check if all conditions are not null and have a different generation from the original one + // before adding the label + if (dnsManaged != nil && dnsManaged.ObservedGeneration != originalGateway.Generation) && + (dnsReady != nil && dnsReady.ObservedGeneration != originalGateway.Generation) && + (loadBalancerManaged != nil && loadBalancerManaged.ObservedGeneration != originalGateway.Generation) && + (loadBalancerReady != nil && loadBalancerReady.ObservedGeneration != originalGateway.Generation) { + return true + } + + t.Logf("conditions are not yet the expected: %v, retrying...", gw.Status.Conditions) + return false + }, 30*time.Second, 2*time.Second, "error waiting for openshift conditions to be present on Gateway") + // We expect exactly 6 conditions. If we get more than it, Istio is adding + // more conditions and we need to be aware that Gateway API status.conditons has a maxItems of 8 + assert.Len(t, gw.Status.Conditions, 6) + // We expect an event to happen, so try to get this event to guarantee it was properly added + assert.Eventually(t, func() bool { + events, err := getMatchingEventsFromGateway(t, kclient, gw, "Normal", "AddedConditions") + if err != nil { + t.Logf("error fetching the events from namespace: %s; retrying...", err) + return false + } + t.Logf("events found: (%d): %+v", len(events.Items), events.Items) + return len(events.Items) > 0 + }, 30*time.Second, 5*time.Second, "error fetching matching resource to add conditions") + }) + + // This test will delete the Gateway service. This should kick a new reconciliation + // from Istio to recreate the services, and the condition "Programmed" should have + // a different lastTransitionTime before the service being deleted. + // But the condition "DNSManaged" and "LoadBalancerManaged" + // should have the original timestamp, meaning they weren't changed + t.Run("should not replace openshift conditions when Istio reconciles the gateway", func(t *testing.T) { + originalGateway := &gatewayapiv1.Gateway{} + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + require.Eventually(t, func() bool { + if err := kclient.Get(context.Background(), nsName, originalGateway); err != nil { + t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) + return false + } + return true + }, 30*time.Second, 2*time.Second) + + // These lastTransitionTime should not change + // Also do a sanity check that they are true / ready + originalDNSManagedCondition := condutils.FindStatusCondition(originalGateway.Status.Conditions, "DNSManaged") + require.NotNil(t, originalDNSManagedCondition) + require.Equal(t, metav1.ConditionTrue, originalDNSManagedCondition.Status) + originalLoadBalancerManagedCondition := condutils.FindStatusCondition(originalGateway.Status.Conditions, "LoadBalancerManaged") + require.NotNil(t, originalLoadBalancerManagedCondition) + require.Equal(t, metav1.ConditionTrue, originalLoadBalancerManagedCondition.Status) + + // These lastTransitionTime should change once the service is deleted and reprovisioned + originalLoadBalancerReadyCondition := condutils.FindStatusCondition(originalGateway.Status.Conditions, "LoadBalancerReady") + require.NotNil(t, originalLoadBalancerReadyCondition) + require.Equal(t, metav1.ConditionTrue, originalLoadBalancerReadyCondition.Status) + originalProgrammedCondition := condutils.FindStatusCondition(originalGateway.Status.Conditions, "Programmed") + require.NotNil(t, originalProgrammedCondition) + require.Equal(t, metav1.ConditionTrue, originalProgrammedCondition.Status) + + t.Run("deleting a service managed by Istio", func(t *testing.T) { + ctx := context.Background() + svcList := &corev1.ServiceList{} + assert.Eventually(t, func() bool { + if err := kclient.List(ctx, svcList, + client.InNamespace(operatorcontroller.DefaultOperandNamespace), + client.MatchingLabels{operatorcontroller.GatewayNameLabelKey: originalGateway.GetName()}, + ); err != nil { + t.Logf("Failed to list services attached to Gateway %s; retrying...: %s", originalGateway.GetName(), err) + return false + } + return true + }, 30*time.Second, 2*time.Second) + + require.Len(t, svcList.Items, 1) + svc := svcList.Items[0] + + // Delete the service + assert.Eventually(t, func() bool { + if err := kclient.Delete(ctx, &svc); client.IgnoreNotFound(err) != nil { + t.Logf("Failed to delete service %s attached to Gateway %s; retrying...: %s", svc.GetName(), originalGateway.GetName(), err) + return false + } + return true + }, 30*time.Second, time.Second) + }) + + currentGateway := &gatewayapiv1.Gateway{} + var currentDNSManagedCondition, currentLoadBalancerManagedCondition *metav1.Condition + t.Run("lastTransitionTime should change for some conditions and not for others", func(t *testing.T) { + assert.Eventually(t, func() bool { + + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + + if err := kclient.Get(context.Background(), nsName, currentGateway); err != nil { + t.Logf("Failed to get current gateway %v: %v; retrying...", nsName, err) + return false + } + + currentDNSManagedCondition = condutils.FindStatusCondition(currentGateway.Status.Conditions, "DNSManaged") + currentLoadBalancerManagedCondition = condutils.FindStatusCondition(currentGateway.Status.Conditions, "LoadBalancerManaged") + currentLoadBalancerReadyCondition := condutils.FindStatusCondition(currentGateway.Status.Conditions, "LoadBalancerReady") + currentProgrammedCondition := condutils.FindStatusCondition(currentGateway.Status.Conditions, "Programmed") + + // Expect conditions to be ready + if (currentDNSManagedCondition == nil || currentDNSManagedCondition.Status != metav1.ConditionTrue) || + (currentLoadBalancerManagedCondition == nil || currentLoadBalancerManagedCondition.Status != metav1.ConditionTrue) || + (currentLoadBalancerReadyCondition == nil || currentLoadBalancerReadyCondition.Status != metav1.ConditionTrue) || + (currentProgrammedCondition == nil || currentProgrammedCondition.Status != metav1.ConditionTrue) { + + t.Logf("conditions on gateway %s are not ready yet: %+v", currentGateway.GetName(), currentGateway.Status.Conditions) + return false + } + + // Expect LoadBalancerReady and Programmed condition to have a new transition time + if !currentLoadBalancerReadyCondition.LastTransitionTime.After(originalLoadBalancerReadyCondition.LastTransitionTime.Time) || + !currentProgrammedCondition.LastTransitionTime.After(originalProgrammedCondition.LastTransitionTime.Time) { + t.Logf("conditions on gateway %s didn't changed yet: %+v", currentGateway.GetName(), currentGateway.Status.Conditions) + return false + } + + return true + }, 3*time.Minute, 3*time.Second) + }) + // After conditions are bumped, the original ones should not change + assert.Equal(t, originalDNSManagedCondition.LastTransitionTime, currentDNSManagedCondition.LastTransitionTime, "the DNSManaged condition LastTransitionTime should not change") + assert.Equal(t, originalLoadBalancerManagedCondition.LastTransitionTime, currentLoadBalancerManagedCondition.LastTransitionTime, "the LoadBalancerManaged condition LastTransitionTime should not change") + }) + + // This test verifies if creating a 2nd Gateway using the same domain of the 1st one returns + // all of the conditions as Ready. + // This test should be changed and fixed once https://github.com/openshift/cluster-ingress-operator/pull/1229 + // is merged, as this will become an unsupported scenario (2 gateways with a conflicting DNS) + t.Run("should not conflict with a second Gateway created with the same domain", func(t *testing.T) { + dupName := gateway.GetName() + "-dup" + dupGateway, err := createGateway(gatewayClass, dupName, operatorcontroller.DefaultOperandNamespace, testDomain) + require.NoError(t, err, "failed to create gateway", "name", name) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(kclient.Delete(context.TODO(), dupGateway)), "failed to clean duplicated test gateway", "name", name) + }) + assert.Eventually(t, func() bool { + current := &gatewayapiv1.Gateway{} + nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name} + + if err := kclient.Get(context.Background(), nsName, current); err != nil { + t.Logf("Failed to get current gateway %v: %v; retrying...", nsName, err) + return false + } + if !condutils.IsStatusConditionTrue(current.Status.Conditions, "DNSManaged") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "DNSReady") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "LoadBalancerManaged") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "LoadBalancerReady") { + t.Logf("current gateway %v does not have the right conditions yet %v; retrying...", nsName, err) + return false + } + + duplicate := &gatewayapiv1.Gateway{} + duplicate.SetName(dupName) + duplicate.SetNamespace(dupGateway.Namespace) + if err := kclient.Get(context.Background(), client.ObjectKeyFromObject(duplicate), duplicate); err != nil { + t.Logf("Failed to get current gateway %v: %v; retrying...", nsName, err) + return false + } + + // This should be false once the duplicate DNSRecord PR is merged + if !condutils.IsStatusConditionTrue(current.Status.Conditions, "DNSManaged") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "DNSReady") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "LoadBalancerManaged") || + !condutils.IsStatusConditionTrue(current.Status.Conditions, "LoadBalancerReady") { + t.Logf("duplicate gateway %v does not have the right conditions yet %v; retrying...", nsName, err) + return false + } + + return true + }, 3*time.Minute, 3*time.Second) + }) + + }) +} + +func getMatchingEventsFromGateway(t *testing.T, kclient client.Reader, gw *gatewayapiv1.Gateway, eventType, reason string) (*corev1.EventList, error) { + t.Helper() + operandEvents := &corev1.EventList{} + fieldSelector := fields.Set{ + "involvedObject.kind": "Gateway", + "involvedObject.namespace": gw.Namespace, + "involvedObject.name": gw.Name, + "type": eventType, + "reason": reason, + } + + t.Logf("using field selector: %+v", fieldSelector) + err := kclient.List(context.Background(), operandEvents, + client.InNamespace(operatorcontroller.DefaultOperandNamespace), + client.MatchingFieldsSelector{Selector: fields.SelectorFromSet(fieldSelector)}) + return operandEvents, err } func testGatewayAPIDNSListenerUpdate(t *testing.T) { diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go new file mode 100644 index 0000000000..9684347245 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/doc.go @@ -0,0 +1,29 @@ +// Package require implements the same assertions as the `assert` package but +// stops test execution when a test fails. +// +// # Example Usage +// +// The following is a complete example using require in a standard test function: +// +// import ( +// "testing" +// "github.com/stretchr/testify/require" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// require.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// # Assertions +// +// The `require` package have same global functions as in the `assert` package, +// but instead of returning a boolean result they call `t.FailNow()`. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package require diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements.go b/vendor/github.com/stretchr/testify/require/forward_requirements.go new file mode 100644 index 0000000000..1dcb2338c4 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/forward_requirements.go @@ -0,0 +1,16 @@ +package require + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require_forward.go.tmpl -include-format-funcs" diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go new file mode 100644 index 0000000000..d8921950d7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -0,0 +1,2124 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Condition(t, comp, msgAndArgs...) { + return + } + t.FailNow() +} + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Conditionf(t, comp, msg, args...) { + return + } + t.FailNow() +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// require.Contains(t, "Hello World", "World") +// require.Contains(t, ["Hello", "World"], "World") +// require.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Contains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// require.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// require.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// require.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Containsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// require.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// require.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// require.Empty(t, obj) +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Empty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// require.Emptyf(t, obj, "error message %s", "formatted") +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Emptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// Equal asserts that two objects are equal. +// +// require.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equal(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// require.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualError(t, theError, errString, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// require.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualErrorf(t, theError, errString, msg, args...) { + return + } + t.FailNow() +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// require.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// require.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// require.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// require.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// require.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// require.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Equalf asserts that two objects are equal. +// +// require.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equalf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if require.Error(t, err) { +// require.Equal(t, expectedError, err) +// } +func Error(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Error(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// require.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContains(t, theError, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// require.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContainsf(t, theError, contains, msg, args...) { + return + } + t.FailNow() +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if require.Errorf(t, err, "error message %s", "formatted") { +// require.Equal(t, expectedErrorf, err) +// } +func Errorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Errorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventually(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// require.EventuallyWithT(t, func(c *require.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithT(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// require.EventuallyWithTf(t, func(c *require.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithTf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// require.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventuallyf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Exactly asserts that two objects are equal in value and type. +// +// require.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactly(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// require.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactlyf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Fail(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNow(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNowf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Failf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// False asserts that the specified value is false. +// +// require.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.False(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Falsef asserts that the specified value is false. +// +// require.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Falsef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// Greater asserts that the first element is greater than the second +// +// require.Greater(t, 2, 1) +// require.Greater(t, float64(2), float64(1)) +// require.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greater(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// require.GreaterOrEqual(t, 2, 1) +// require.GreaterOrEqual(t, 2, 2) +// require.GreaterOrEqual(t, "b", "a") +// require.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// require.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// require.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Greaterf asserts that the first element is greater than the second +// +// require.Greaterf(t, 2, 1, "error message %s", "formatted") +// require.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") +// require.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greaterf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// require.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPError(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// require.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPErrorf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// require.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirect(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// require.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirectf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// require.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCode(t, handler, method, url, values, statuscode, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// require.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCodef(t, handler, method, url, values, statuscode, msg, args...) { + return + } + t.FailNow() +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// require.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccess(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// require.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccessf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// Implements asserts that an object is implemented by the specified interface. +// +// require.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// require.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// require.InDelta(t, math.Pi, 22/7.0, 0.01) +func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDelta(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValues(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValuesf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlicef(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// require.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlicef(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonf(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// IsDecreasing asserts that the collection is decreasing +// +// require.IsDecreasing(t, []int{2, 1, 0}) +// require.IsDecreasing(t, []float{2, 1}) +// require.IsDecreasing(t, []string{"b", "a"}) +func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsDecreasingf asserts that the collection is decreasing +// +// require.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") +// require.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsIncreasing asserts that the collection is increasing +// +// require.IsIncreasing(t, []int{1, 2, 3}) +// require.IsIncreasing(t, []float{1, 2}) +// require.IsIncreasing(t, []string{"a", "b"}) +func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsIncreasingf asserts that the collection is increasing +// +// require.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") +// require.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// require.IsNonDecreasing(t, []int{1, 1, 2}) +// require.IsNonDecreasing(t, []float{1, 2}) +// require.IsNonDecreasing(t, []string{"a", "b"}) +func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// require.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// require.IsNonIncreasing(t, []int{2, 1, 1}) +// require.IsNonIncreasing(t, []float{2, 1}) +// require.IsNonIncreasing(t, []string{"b", "a"}) +func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// require.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsType(t, expectedType, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsTypef asserts that the specified objects are of the same type. +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsTypef(t, expectedType, object, msg, args...) { + return + } + t.FailNow() +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// require.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// require.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// require.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Len(t, object, length, msgAndArgs...) { + return + } + t.FailNow() +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// require.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lenf(t, object, length, msg, args...) { + return + } + t.FailNow() +} + +// Less asserts that the first element is less than the second +// +// require.Less(t, 1, 2) +// require.Less(t, float64(1), float64(2)) +// require.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Less(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// require.LessOrEqual(t, 1, 2) +// require.LessOrEqual(t, 2, 2) +// require.LessOrEqual(t, "a", "b") +// require.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// require.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// require.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Lessf asserts that the first element is less than the second +// +// require.Lessf(t, 1, 2, "error message %s", "formatted") +// require.Lessf(t, float64(1), float64(2), "error message %s", "formatted") +// require.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lessf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Negative asserts that the specified element is negative +// +// require.Negative(t, -1) +// require.Negative(t, -1.23) +func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negative(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Negativef asserts that the specified element is negative +// +// require.Negativef(t, -1, "error message %s", "formatted") +// require.Negativef(t, -1.23, "error message %s", "formatted") +func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negativef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Never(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Neverf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Nil asserts that the specified object is nil. +// +// require.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Nilf asserts that the specified object is nil. +// +// require.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if require.NoError(t, err) { +// require.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoError(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if require.NoErrorf(t, err, "error message %s", "formatted") { +// require.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoErrorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// require.NotContains(t, "Hello World", "Earth") +// require.NotContains(t, ["Hello", "World"], "Earth") +// require.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// require.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// require.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// require.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContainsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// +// require.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +func NotElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// require.NotElementsMatchf(t, [1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if require.NotEmpty(t, obj) { +// require.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmpty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if require.NotEmptyf(t, obj, "error message %s", "formatted") { +// require.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotEqual asserts that the specified values are NOT equal. +// +// require.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqual(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// require.NotEqualValues(t, obj1, obj2) +func NotEqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// require.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") +func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// require.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// require.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// require.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// NotNil asserts that the specified object is not nil. +// +// require.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotNilf asserts that the specified object is not nil. +// +// require.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// require.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// require.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// require.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// require.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// require.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// require.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// NotSame asserts that two pointers do not reference the same object. +// +// require.NotSame(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSame(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// require.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSamef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotSubset asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// require.NotSubset(t, [1, 3, 4], [1, 2]) +// require.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSubsetf asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// require.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") +// require.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZerof(t, i, msg, args...) { + return + } + t.FailNow() +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// require.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// require.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithError(t TestingT, errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithError(t, errString, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// require.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithErrorf(t TestingT, errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithErrorf(t, errString, f, msg, args...) { + return + } + t.FailNow() +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// require.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValue(t, expected, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// require.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValuef(t, expected, f, msg, args...) { + return + } + t.FailNow() +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// require.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// Positive asserts that the specified element is positive +// +// require.Positive(t, 1) +// require.Positive(t, 1.23) +func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positive(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Positivef asserts that the specified element is positive +// +// require.Positivef(t, 1, "error message %s", "formatted") +// require.Positivef(t, 1.23, "error message %s", "formatted") +func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positivef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Regexp asserts that a specified regexp matches a string. +// +// require.Regexp(t, regexp.MustCompile("start"), "it's starting") +// require.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// Regexpf asserts that a specified regexp matches a string. +// +// require.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// require.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// Same asserts that two pointers reference the same object. +// +// require.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Same(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Samef asserts that two pointers reference the same object. +// +// require.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Samef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Subset asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// require.Subset(t, [1, 2, 3], [1, 2]) +// require.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// Subsetf asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// require.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") +// require.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// True asserts that the specified value is true. +// +// require.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.True(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Truef asserts that the specified value is true. +// +// require.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Truef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// require.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// require.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDurationf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// require.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRange(t, actual, start, end, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// require.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRangef(t, actual, start, end, msg, args...) { + return + } + t.FailNow() +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zerof(t, i, msg, args...) { + return + } + t.FailNow() +} diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl new file mode 100644 index 0000000000..8b32836850 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go.tmpl @@ -0,0 +1,6 @@ +{{ replace .Comment "assert." "require."}} +func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { + if h, ok := t.(tHelper); ok { h.Helper() } + if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return } + t.FailNow() +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go new file mode 100644 index 0000000000..1bd87304f4 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -0,0 +1,1674 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Emptyf(obj, "error message %s", "formatted") +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValues(S{1, 2}, S{1, 3}) => true +// a.EqualExportedValues(S{1, 2}, S{2, 3}) => false +func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValuesf(S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// a.EqualExportedValuesf(S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValuesf(a.t, expected, actual, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err) { +// assert.Equal(t, expectedError, err) +// } +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Error(a.t, err, msgAndArgs...) +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAs(a.t, err, target, msgAndArgs...) +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAsf(a.t, err, target, msg, args...) +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContainsf(a.t, theError, contains, msg, args...) +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIs(a.t, err, target, msgAndArgs...) +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIsf(a.t, err, target, msg, args...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Errorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithT(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123), int64(123), "error message %s", "formatted") +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2), float64(1), "error message %s", "formatted") +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCode(myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCode(a.t, handler, method, url, values, statuscode, msgAndArgs...) +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCodef(myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCodef(a.t, handler, method, url, values, statuscode, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, 22/7.0, 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsDecreasing asserts that the collection is decreasing +// +// a.IsDecreasing([]int{2, 1, 0}) +// a.IsDecreasing([]float{2, 1}) +// a.IsDecreasing([]string{"b", "a"}) +func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasing(a.t, object, msgAndArgs...) +} + +// IsDecreasingf asserts that the collection is decreasing +// +// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted") +// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasingf(a.t, object, msg, args...) +} + +// IsIncreasing asserts that the collection is increasing +// +// a.IsIncreasing([]int{1, 2, 3}) +// a.IsIncreasing([]float{1, 2}) +// a.IsIncreasing([]string{"a", "b"}) +func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasing(a.t, object, msgAndArgs...) +} + +// IsIncreasingf asserts that the collection is increasing +// +// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted") +// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasingf(a.t, object, msg, args...) +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// a.IsNonDecreasing([]int{1, 1, 2}) +// a.IsNonDecreasing([]float{1, 2}) +// a.IsNonDecreasing([]string{"a", "b"}) +func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasing(a.t, object, msgAndArgs...) +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasingf(a.t, object, msg, args...) +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// a.IsNonIncreasing([]int{2, 1, 1}) +// a.IsNonIncreasing([]float{2, 1}) +// a.IsNonIncreasing([]string{"b", "a"}) +func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasing(a.t, object, msgAndArgs...) +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasingf(a.t, object, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1), float64(2), "error message %s", "formatted") +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lessf(a.t, e1, e2, msg, args...) +} + +// Negative asserts that the specified element is negative +// +// a.Negative(-1) +// a.Negative(-1.23) +func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negative(a.t, e, msgAndArgs...) +} + +// Negativef asserts that the specified element is negative +// +// a.Negativef(-1, "error message %s", "formatted") +// a.Negativef(-1.23, "error message %s", "formatted") +func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negativef(a.t, e, msg, args...) +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Never(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Neverf(a.t, condition, waitFor, tick, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nilf(a.t, object, msg, args...) +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExists(a.t, path, msgAndArgs...) +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExistsf(a.t, path, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoErrorf(a.t, err, msg, args...) +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExists(a.t, path, msgAndArgs...) +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExistsf(a.t, path, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContainsf(a.t, s, contains, msg, args...) +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true +// +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +func (a *Assertions) NotElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// a.NotElementsMatchf([1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatchf(a.t, listA, listB, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValues(obj1, obj2) +func (a *Assertions) NotEqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValues(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValuesf(obj1, obj2, "error message %s", "formatted") +func (a *Assertions) NotEqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValuesf(a.t, expected, actual, msg, args...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAs(a.t, err, target, msgAndArgs...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAsf(a.t, err, target, msg, args...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIs(a.t, err, target, msgAndArgs...) +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIsf(a.t, err, target, msg, args...) +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// a.NotImplements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) NotImplements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplements(a.t, interfaceObject, object, msgAndArgs...) +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// a.NotImplementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) NotImplementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplementsf(a.t, interfaceObject, object, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSame asserts that two pointers do not reference the same object. +// +// a.NotSame(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSame(a.t, expected, actual, msgAndArgs...) +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// a.NotSamef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSamef(a.t, expected, actual, msg, args...) +} + +// NotSubset asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// a.NotSubset([1, 3, 4], [1, 2]) +// a.NotSubset({"x": 1, "y": 2}, {"z": 3}) +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the specified list(array, slice...) or map does NOT +// contain all elements given in the specified subset list(array, slice...) or +// map. +// +// a.NotSubsetf([1, 3, 4], [1, 2], "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithError("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithError(errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithError(a.t, errString, f, msgAndArgs...) +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithErrorf("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithErrorf(errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithErrorf(a.t, errString, f, msg, args...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panicsf(a.t, f, msg, args...) +} + +// Positive asserts that the specified element is positive +// +// a.Positive(1) +// a.Positive(1.23) +func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positive(a.t, e, msgAndArgs...) +} + +// Positivef asserts that the specified element is positive +// +// a.Positivef(1, "error message %s", "formatted") +// a.Positivef(1.23, "error message %s", "formatted") +func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positivef(a.t, e, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// a.Subset([1, 2, 3], [1, 2]) +// a.Subset({"x": 1, "y": 2}, {"x": 1}) +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the specified list(array, slice...) or map contains all +// elements given in the specified subset list(array, slice...) or map. +// +// a.Subsetf([1, 2, 3], [1, 2], "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRangef(a.t, actual, start, end, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zerof(a.t, i, msg, args...) +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl new file mode 100644 index 0000000000..54124df1d3 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) { + if h, ok := a.t.(tHelper); ok { h.Helper() } + {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go new file mode 100644 index 0000000000..6b7ce929eb --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -0,0 +1,29 @@ +package require + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) + FailNow() +} + +type tHelper = interface { + Helper() +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs" diff --git a/vendor/modules.txt b/vendor/modules.txt index cbd7985c75..134d54bd70 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -574,6 +574,7 @@ github.com/spf13/pflag ## explicit; go 1.17 github.com/stretchr/testify/assert github.com/stretchr/testify/assert/yaml +github.com/stretchr/testify/require # github.com/summerwind/h2spec v0.0.0-20200804131034-70ac22940108 ## explicit; go 1.12 github.com/summerwind/h2spec