diff --git a/api/adc/types.go b/api/adc/types.go index 978b844d5..a5577efa7 100644 --- a/api/adc/types.go +++ b/api/adc/types.go @@ -68,22 +68,22 @@ type Metadata struct { } type Resources struct { - ConsumerGroups []*ConsumerGroup `json:"consumer_groups,omitempty" yaml:"consumer_groups,omitempty"` - Consumers []*ConsumerElement `json:"consumers,omitempty" yaml:"consumers,omitempty"` - GlobalRules Plugins `json:"global_rules,omitempty" yaml:"global_rules,omitempty"` - PluginMetadata Plugins `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"` - Services []*Service `json:"services,omitempty" yaml:"services,omitempty"` - SSLs []*SSL `json:"ssls,omitempty" yaml:"ssls,omitempty"` + ConsumerGroups []*ConsumerGroup `json:"consumer_groups,omitempty" yaml:"consumer_groups,omitempty"` + Consumers []*Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"` + GlobalRules Plugins `json:"global_rules,omitempty" yaml:"global_rules,omitempty"` + PluginMetadata Plugins `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"` + Services []*Service `json:"services,omitempty" yaml:"services,omitempty"` + SSLs []*SSL `json:"ssls,omitempty" yaml:"ssls,omitempty"` } type ConsumerGroup struct { Metadata `json:",inline" yaml:",inline"` - Consumers []ConsumerElement `json:"consumers,omitempty" yaml:"consumers,omitempty"` - Name string `json:"name" yaml:"name"` - Plugins Plugins `json:"plugins" yaml:"plugins"` + Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"` + Name string `json:"name" yaml:"name"` + Plugins Plugins `json:"plugins" yaml:"plugins"` } -type ConsumerElement struct { +type Consumer struct { Credentials []Credential `json:"credentials,omitempty" yaml:"credentials,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` @@ -415,6 +415,19 @@ func ComposeServiceNameWithRule(namespace, name string, rule string) string { return buf.String() } +func ComposeConsumerName(namespace, name string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + p := make([]byte, 0, len(namespace)+len(name)+1) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + + return buf.String() +} + // NewDefaultUpstream returns an empty Upstream with default values. func NewDefaultService() *Service { return &Service{ diff --git a/api/v1alpha1/consumer_types.go b/api/v1alpha1/consumer_types.go index f1f4ab34a..b4364df14 100644 --- a/api/v1alpha1/consumer_types.go +++ b/api/v1alpha1/consumer_types.go @@ -22,9 +22,13 @@ type ConsumerSpec struct { } type GatewayRef struct { - Name string `json:"name,omitempty"` - Kind string `json:"kind,omitempty"` - Group string `json:"group,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // +kubebuilder:default=Gateway + Kind *string `json:"kind,omitempty"` + // +kubebuilder:default=gateway.networking.k8s.io + Group *string `json:"group,omitempty"` Namespace *string `json:"namespace,omitempty"` } diff --git a/api/v1alpha1/pluginconfig_types.go b/api/v1alpha1/pluginconfig_types.go index aaa225d45..b62730b04 100644 --- a/api/v1alpha1/pluginconfig_types.go +++ b/api/v1alpha1/pluginconfig_types.go @@ -36,7 +36,7 @@ type Plugin struct { // The plugin name. Name string `json:"name" yaml:"name"` // Plugin configuration. - Config apiextensionsv1.JSON `json:"config" yaml:"config"` + Config apiextensionsv1.JSON `json:"config,omitempty" yaml:"config,omitempty"` } func init() { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 22efa4cab..562df618a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -242,6 +242,16 @@ func (in *GatewayProxySpec) DeepCopy() *GatewayProxySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayRef) DeepCopyInto(out *GatewayRef) { *out = *in + if in.Kind != nil { + in, out := &in.Kind, &out.Kind + *out = new(string) + **out = **in + } + if in.Group != nil { + in, out := &in.Group, &out.Group + *out = new(string) + **out = **in + } if in.Namespace != nil { in, out := &in.Namespace, &out.Namespace *out = new(string) diff --git a/config/crd/bases/gateway.apisix.io_consumers.yaml b/config/crd/bases/gateway.apisix.io_consumers.yaml index efbfff195..a914c4979 100644 --- a/config/crd/bases/gateway.apisix.io_consumers.yaml +++ b/config/crd/bases/gateway.apisix.io_consumers.yaml @@ -67,13 +67,18 @@ spec: gatewayRef: properties: group: + default: gateway.networking.k8s.io type: string kind: + default: Gateway type: string name: + minLength: 1 type: string namespace: type: string + required: + - name type: object plugins: items: @@ -85,7 +90,6 @@ spec: description: The plugin name. type: string required: - - config - name type: object type: array diff --git a/config/crd/bases/gateway.apisix.io_pluginconfigs.yaml b/config/crd/bases/gateway.apisix.io_pluginconfigs.yaml index 92f0ad79b..4365147fd 100644 --- a/config/crd/bases/gateway.apisix.io_pluginconfigs.yaml +++ b/config/crd/bases/gateway.apisix.io_pluginconfigs.yaml @@ -49,7 +49,6 @@ spec: description: The plugin name. type: string required: - - config - name type: object type: array diff --git a/internal/controller/consumer_controller.go b/internal/controller/consumer_controller.go index c372e967b..823a2f533 100644 --- a/internal/controller/consumer_controller.go +++ b/internal/controller/consumer_controller.go @@ -4,12 +4,22 @@ import ( "context" "github.com/api7/api7-ingress-controller/api/v1alpha1" + "github.com/api7/api7-ingress-controller/internal/controller/indexer" "github.com/api7/api7-ingress-controller/internal/provider" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) // ConsumerReconciler reconciles a Gateway object. @@ -24,10 +34,59 @@ type ConsumerReconciler struct { //nolint:revive // SetupWithManager sets up the controller with the Manager. func (r *ConsumerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.Consumer{}). + For(&v1alpha1.Consumer{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(r.checkGatewayRef), + ), + ). + WithEventFilter(predicate.GenerationChangedPredicate{}). + Watches(&gatewayv1.Gateway{}, + handler.EnqueueRequestsFromMapFunc(r.listConsumersForGateway), + builder.WithPredicates( + predicate.Funcs{ + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return true + }, + }, + ), + ). Complete(r) } +func (r *ConsumerReconciler) listConsumersForGateway(ctx context.Context, obj client.Object) []reconcile.Request { + gateway, ok := obj.(*gatewayv1.Gateway) + if !ok { + r.Log.Error(nil, "failed to convert to Gateway", "object", obj) + return nil + } + consumerList := &v1alpha1.ConsumerList{} + if err := r.List(ctx, consumerList, client.MatchingFields{ + indexer.ConsumerGatewayRef: indexer.GenIndexKey(gateway.Name, gateway.GetNamespace()), + }); err != nil { + r.Log.Error(err, "failed to list consumers") + return nil + } + requests := make([]reconcile.Request, 0, len(consumerList.Items)) + for _, consumer := range consumerList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: consumer.Name, + Namespace: consumer.Namespace, + }, + }) + } + return requests +} + func (r *ConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { consumer := new(v1alpha1.Consumer) if err := r.Get(ctx, req.NamespacedName, consumer); err != nil { @@ -43,8 +102,107 @@ func (r *ConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c if err := r.Provider.Delete(ctx, consumer); err != nil { return ctrl.Result{}, err } + return ctrl.Result{}, nil } return ctrl.Result{}, err } + + var statusErr error + tctx := provider.NewDefaultTranslateContext() + + if err := r.processSpec(ctx, tctx, consumer); err != nil { + r.Log.Error(err, "failed to process consumer spec", "consumer", consumer) + statusErr = err + } + + if err := r.Provider.Update(ctx, tctx, consumer); err != nil { + r.Log.Error(err, "failed to update consumer", "consumer", consumer) + statusErr = err + } + + if err := r.updateStatus(ctx, consumer, statusErr); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } + +func (r *ConsumerReconciler) processSpec(ctx context.Context, tctx *provider.TranslateContext, consumer *v1alpha1.Consumer) error { + for _, credential := range consumer.Spec.Credentials { + if credential.SecretRef == nil { + continue + } + ns := consumer.GetNamespace() + if credential.SecretRef.Namespace != nil { + ns = *credential.SecretRef.Namespace + } + secret := corev1.Secret{} + if err := r.Get(ctx, client.ObjectKey{ + Name: credential.SecretRef.Name, + Namespace: ns, + }, &secret); err != nil { + if client.IgnoreNotFound(err) == nil { + continue + } + r.Log.Error(err, "failed to get secret", "secret", credential.SecretRef.Name) + return err + } + + tctx.Secrets[types.NamespacedName{ + Namespace: ns, + Name: credential.SecretRef.Name, + }] = &secret + + } + return nil +} + +func (r *ConsumerReconciler) updateStatus(ctx context.Context, consumer *v1alpha1.Consumer, err error) error { + condition := NewCondition(consumer.Generation, true, "Successfully") + if err != nil { + condition = NewCondition(consumer.Generation, false, err.Error()) + } + if !VerifyConditions(&consumer.Status.Conditions, condition) { + return nil + } + meta.SetStatusCondition(&consumer.Status.Conditions, condition) + if err := r.Status().Update(ctx, consumer); err != nil { + r.Log.Error(err, "failed to update consumer status", "consumer", consumer) + return err + } + return nil +} + +func (r *ConsumerReconciler) checkGatewayRef(object client.Object) bool { + consumer, ok := object.(*v1alpha1.Consumer) + if !ok { + return false + } + if consumer.Spec.GatewayRef.Name == "" { + return false + } + if consumer.Spec.GatewayRef.Kind != nil && *consumer.Spec.GatewayRef.Kind != KindGateway { + return false + } + if consumer.Spec.GatewayRef.Group != nil && *consumer.Spec.GatewayRef.Group != gatewayv1.GroupName { + return false + } + ns := consumer.GetNamespace() + if consumer.Spec.GatewayRef.Namespace != nil { + ns = *consumer.Spec.GatewayRef.Namespace + } + gateway := &gatewayv1.Gateway{} + if err := r.Get(context.Background(), client.ObjectKey{ + Name: consumer.Spec.GatewayRef.Name, + Namespace: ns, + }, gateway); err != nil { + r.Log.Error(err, "failed to get gateway", "gateway", consumer.Spec.GatewayRef.Name) + return false + } + gatewayClass := &gatewayv1.GatewayClass{} + if err := r.Client.Get(context.Background(), client.ObjectKey{Name: string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil { + r.Log.Error(err, "failed to get gateway class", "gateway", gateway.GetName(), "gatewayclass", gateway.Spec.GatewayClassName) + return false + } + return matchesController(string(gatewayClass.Spec.ControllerName)) +} diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index 05ab2f590..7942e6493 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -3,6 +3,7 @@ package indexer import ( "context" + "github.com/api7/api7-ingress-controller/api/v1alpha1" networkingv1 "k8s.io/api/networking/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -10,13 +11,14 @@ import ( ) const ( - ServiceIndexRef = "serviceRefs" - ExtensionRef = "extensionRef" - ParametersRef = "parametersRef" - ParentRefs = "parentRefs" - IngressClass = "ingressClass" - SecretIndexRef = "secretRefs" - IngressClassRef = "ingressClassRef" + ServiceIndexRef = "serviceRefs" + ExtensionRef = "extensionRef" + ParametersRef = "parametersRef" + ParentRefs = "parentRefs" + IngressClass = "ingressClass" + SecretIndexRef = "secretRefs" + IngressClassRef = "ingressClassRef" + ConsumerGatewayRef = "consumerGatewayRef" ) func SetupIndexer(mgr ctrl.Manager) error { @@ -29,6 +31,9 @@ func SetupIndexer(mgr ctrl.Manager) error { if err := setupIngressIndexer(mgr); err != nil { return err } + if err := setupConsumerIndexer(mgr); err != nil { + return err + } return nil } @@ -44,6 +49,31 @@ func setupGatewayIndexer(mgr ctrl.Manager) error { return nil } +func setupConsumerIndexer(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &v1alpha1.Consumer{}, + ConsumerGatewayRef, + ConsumerGatewayRefIndexFunc, + ); err != nil { + return err + } + return nil +} +func ConsumerGatewayRefIndexFunc(rawObj client.Object) []string { + consumer := rawObj.(*v1alpha1.Consumer) + + if consumer.Spec.GatewayRef.Name == "" { + return nil + } + + ns := consumer.GetNamespace() + if consumer.Spec.GatewayRef.Namespace != nil { + ns = *consumer.Spec.GatewayRef.Namespace + } + return []string{GenIndexKey(ns, consumer.Spec.GatewayRef.Name)} +} + func setupHTTPRouteIndexer(mgr ctrl.Manager) error { if err := mgr.GetFieldIndexer().IndexField( context.Background(), diff --git a/internal/controller/status.go b/internal/controller/status.go new file mode 100644 index 000000000..d60e2616b --- /dev/null +++ b/internal/controller/status.go @@ -0,0 +1,46 @@ +package controller + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ConditionTypeAvailable string = "Available" + ConditionTypeProgressing string = "Progressing" + ConditionTypeDegraded string = "Degraded" + + ConditionReasonSynced string = "ResourceSynced" + ConditionReasonSyncAbort string = "ResourceSyncAbort" +) + +func NewCondition(observedGeneration int64, status bool, message string) metav1.Condition { + condition := metav1.ConditionTrue + reason := ConditionReasonSynced + if !status { + condition = metav1.ConditionFalse + reason = ConditionReasonSyncAbort + } + return metav1.Condition{ + Type: ConditionTypeAvailable, + Reason: reason, + Status: condition, + Message: message, + ObservedGeneration: observedGeneration, + } +} + +func VerifyConditions(conditions *[]metav1.Condition, newCondition metav1.Condition) bool { + existingCondition := meta.FindStatusCondition(*conditions, newCondition.Type) + if existingCondition == nil { + return true + } + + if existingCondition.ObservedGeneration > newCondition.ObservedGeneration { + return false + } + if *existingCondition == newCondition { + return false + } + return true +} diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 5d1674d9b..04e28ad27 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -15,6 +15,7 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" types "github.com/api7/api7-ingress-controller/api/adc" + "github.com/api7/api7-ingress-controller/api/v1alpha1" "github.com/api7/api7-ingress-controller/internal/controller/config" "github.com/api7/api7-ingress-controller/internal/controller/label" "github.com/api7/api7-ingress-controller/internal/provider" @@ -64,6 +65,9 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *networkingv1.Ingress: result, err = d.translator.TranslateIngress(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "service", "ssl") + case *v1alpha1.Consumer: + result, err = d.translator.TranslateConsumerV1alpha1(tctx, t.DeepCopy()) + resourceTypes = append(resourceTypes, "consumer") } if err != nil { return err @@ -80,6 +84,7 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, PluginMetadata: result.PluginMetadata, Services: result.Services, SSLs: result.SSL, + Consumers: result.Consumers, }, ResourceTypes: resourceTypes, }) @@ -96,6 +101,9 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { labels = label.GenLabel(obj) case *gatewayv1.Gateway: // delete all resources + case *v1alpha1.Consumer: + resourceTypes = append(resourceTypes, "consumer") + labels = label.GenLabel(obj) } return d.sync(Task{ @@ -140,18 +148,22 @@ func (d *adcClient) sync(task Task) error { args = append(args, "--include-resource-type", t) } + adcEnv := []string{ + "ADC_EXPERIMENTAL_FEATURE_FLAGS=remote-state-file,parallel-backend-request", + "ADC_RUNNING_MODE=ingress", + "ADC_BACKEND=api7ee", + "ADC_SERVER=" + d.ServerAddr, + "ADC_TOKEN=" + d.Token, + } + var stdout, stderr bytes.Buffer cmd := exec.Command("adc", args...) cmd.Stdout = &stdout cmd.Stderr = &stderr cmd.Env = append(cmd.Env, os.Environ()...) - cmd.Env = append(cmd.Env, - "ADC_EXPERIMENTAL_FEATURE_FLAGS=remote-state-file,parallel-backend-request", - "ADC_RUNNING_MODE=ingress", - "ADC_BACKEND=api7ee", - "ADC_SERVER="+d.ServerAddr, - "ADC_TOKEN="+d.Token, - ) + cmd.Env = append(cmd.Env, adcEnv...) + + log.Debug("running adc command", zap.String("command", cmd.String()), zap.Strings("env", adcEnv)) var result types.SyncResult if err := cmd.Run(); err != nil { diff --git a/internal/provider/adc/translator/consumer.go b/internal/provider/adc/translator/consumer.go new file mode 100644 index 000000000..8bb1f0f55 --- /dev/null +++ b/internal/provider/adc/translator/consumer.go @@ -0,0 +1,70 @@ +package translator + +import ( + "encoding/json" + + "github.com/api7/api7-ingress-controller/api/adc" + adctypes "github.com/api7/api7-ingress-controller/api/adc" + "github.com/api7/api7-ingress-controller/api/v1alpha1" + "github.com/api7/api7-ingress-controller/internal/provider" + "k8s.io/apimachinery/pkg/types" +) + +func (t *Translator) TranslateConsumerV1alpha1(tctx *provider.TranslateContext, consumerV *v1alpha1.Consumer) (*TranslateResult, error) { + result := &TranslateResult{} + if consumerV == nil { + return result, nil + } + + username := adctypes.ComposeConsumerName(consumerV.Namespace, consumerV.Name) + consumer := &adctypes.Consumer{ + Username: username, + } + credentials := make([]adctypes.Credential, 0, len(consumerV.Spec.Credentials)) + for _, credentialSpec := range consumerV.Spec.Credentials { + credential := adc.Credential{} + credential.Name = credentialSpec.Name + credential.Type = credentialSpec.Type + if credentialSpec.SecretRef != nil { + ns := consumerV.Namespace + if credentialSpec.SecretRef.Namespace != nil { + ns = *credentialSpec.SecretRef.Namespace + } + secret := tctx.Secrets[types.NamespacedName{ + Namespace: ns, + Name: credentialSpec.SecretRef.Name, + }] + if secret == nil { + continue + } + authConfig := make(map[string]any) + for k, v := range secret.Data { + authConfig[k] = v + } + credential.Config = authConfig + } else { + authConfig := make(map[string]any) + if err := json.Unmarshal(credentialSpec.Config.Raw, &authConfig); err != nil { + t.Log.Error(err, "failed to unmarshal credential config", "credential", credentialSpec) + continue + } + credential.Config = authConfig + } + credentials = append(credentials, credential) + } + consumer.Credentials = credentials + + plugins := adctypes.Plugins{} + for _, plugin := range consumerV.Spec.Plugins { + pluginName := plugin.Name + pluginConfig := make(map[string]any) + if err := json.Unmarshal(plugin.Config.Raw, &pluginConfig); err != nil { + t.Log.Error(err, "failed to unmarshal plugin config", "plugin", plugin) + continue + } + plugins[pluginName] = pluginConfig + } + consumer.Plugins = plugins + result.Consumers = append(result.Consumers, consumer) + return result, nil +} diff --git a/internal/provider/adc/translator/httproute.go b/internal/provider/adc/translator/httproute.go index 1a29b477e..d285563dc 100644 --- a/internal/provider/adc/translator/httproute.go +++ b/internal/provider/adc/translator/httproute.go @@ -52,12 +52,17 @@ func (t *Translator) fillPluginFromExtensionRef(plugins adctypes.Plugins, namesp Namespace: namespace, Name: string(extensionRef.Name), }] + if pluginconfig == nil { + return + } for _, plugin := range pluginconfig.Spec.Plugins { pluginName := plugin.Name var pluginconfig map[string]any - if err := json.Unmarshal(plugin.Config.Raw, &pluginconfig); err != nil { - log.Errorw("plugin config unmarshal failed", zap.Error(err)) - continue + if len(plugin.Config.Raw) > 0 { + if err := json.Unmarshal(plugin.Config.Raw, &pluginconfig); err != nil { + log.Errorw("plugin config unmarshal failed", zap.Error(err)) + continue + } } plugins[pluginName] = pluginconfig } diff --git a/internal/provider/adc/translator/translator.go b/internal/provider/adc/translator/translator.go index 7be48c10c..e06519da3 100644 --- a/internal/provider/adc/translator/translator.go +++ b/internal/provider/adc/translator/translator.go @@ -15,4 +15,5 @@ type TranslateResult struct { SSL []*adctypes.SSL GlobalRules adctypes.Plugins PluginMetadata adctypes.Plugins + Consumers []*adctypes.Consumer } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a8fd9e236..1aca4a183 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -21,6 +21,7 @@ type TranslateContext struct { BackendRefs []gatewayv1.BackendRef GatewayTLSConfig []gatewayv1.GatewayTLSConfig GatewayProxy *v1alpha1.GatewayProxy + Credentials []v1alpha1.Credential EndpointSlices map[types.NamespacedName][]discoveryv1.EndpointSlice Secrets map[types.NamespacedName]*corev1.Secret PluginConfigs map[types.NamespacedName]*v1alpha1.PluginConfig diff --git a/test/e2e/crds/consumer.go b/test/e2e/crds/consumer.go new file mode 100644 index 000000000..618175d9a --- /dev/null +++ b/test/e2e/crds/consumer.go @@ -0,0 +1,314 @@ +package gatewayapi + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/api7/api7-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Consumer", func() { + s := scaffold.NewDefaultScaffold() + + var defaultGatewayClass = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + + var defaultGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: api7ee +spec: + gatewayClassName: %s + listeners: + - name: http1 + protocol: HTTP + port: 80 +` + + var defaultHTTPRoute = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: PluginConfig +metadata: + name: auth-plugin-config +spec: + plugins: + - name: multi-auth + config: + auth_plugins: + - basic-auth: {} + - key-auth: + header: apikey +--- + +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: api7ee + hostnames: + - "httpbin.org" + rules: + - matches: + - path: + type: Exact + value: /get + filters: + - type: ExtensionRef + extensionRef: + group: gateway.api7.io + kind: PluginConfig + name: auth-plugin-config + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + var beforeEachHTTP = func() { + By("create GatewayClass") + gatewayClassName := fmt.Sprintf("api7-%d", time.Now().Unix()) + gatewayString := fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()) + err := s.CreateResourceFromStringWithNamespace(gatewayString, "") + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(5 * time.Second) + + By("check GatewayClass condition") + gcyaml, err := s.GetResourceYaml("GatewayClass", gatewayClassName) + Expect(err).NotTo(HaveOccurred(), "getting GatewayClass yaml") + Expect(gcyaml).To(ContainSubstring(`status: "True"`), "checking GatewayClass condition status") + Expect(gcyaml).To( + ContainSubstring("message: the gatewayclass has been accepted by the api7-ingress-controller"), + "checking GatewayClass condition message", + ) + + By("create Gateway") + err = s.CreateResourceFromString(fmt.Sprintf(defaultGateway, gatewayClassName)) + Expect(err).NotTo(HaveOccurred(), "creating Gateway") + time.Sleep(5 * time.Second) + + By("check Gateway condition") + gwyaml, err := s.GetResourceYaml("Gateway", "api7ee") + Expect(err).NotTo(HaveOccurred(), "getting Gateway yaml") + Expect(gwyaml).To(ContainSubstring(`status: "True"`), "checking Gateway condition status") + Expect(gwyaml).To( + ContainSubstring("message: the gateway has been accepted by the api7-ingress-controller"), + "checking Gateway condition message", + ) + + s.ResourceApplied("httproute", "httpbin", defaultHTTPRoute, 1) + } + + Context("Consumer plugins", func() { + var keyAuthConsumer = `apiVersion: gateway.apisix.io/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: api7ee + plugins: + - name: key-auth + config: + key: sample-key +` + var basicAuthConsumer = `apiVersion: gateway.apisix.io/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: api7ee + plugins: + - name: basic-auth + config: + username: sample-user + password: sample-password +` + + BeforeEach(beforeEachHTTP) + + It("key-auth", func() { + s.ResourceApplied("Consumer", "consumer-sample", keyAuthConsumer, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("delete Consumer") + err := s.DeleteResourceFromString(keyAuthConsumer) + Expect(err).NotTo(HaveOccurred(), "deleting Consumer") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(401) + }) + + It("basic-auth", func() { + s.ResourceApplied("Consumer", "consumer-sample", basicAuthConsumer, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("delete Consumer") + err := s.DeleteResourceFromString(basicAuthConsumer) + Expect(err).NotTo(HaveOccurred(), "deleting Consumer") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(401) + }) + }) + + Context("Credential", func() { + var defaultCredential = `apiVersion: gateway.apisix.io/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: api7ee + credentials: + - type: basic-auth + name: basic-auth-sample + config: + username: sample-user + password: sample-password + - type: key-auth + name: key-auth-sample + config: + key: sample-key + - type: key-auth + name: key-auth-sample2 + config: + key: sample-key2 +` + var updateCredential = `apiVersion: gateway.apisix.io/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: api7ee + credentials: + - type: basic-auth + name: basic-auth-sample + config: + username: sample-user + password: sample-password + plugins: + - name: key-auth + config: + key: consumer-key +` + BeforeEach(beforeEachHTTP) + + It("Create/Update/Delete", func() { + s.ResourceApplied("Consumer", "consumer-sample", defaultCredential, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key2"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("update Consumer") + s.ResourceApplied("Consumer", "consumer-sample", updateCredential, 2) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key2"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "consumer-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("delete Consumer") + err := s.DeleteResourceFromString(updateCredential) + Expect(err).NotTo(HaveOccurred(), "deleting Consumer") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(401) + }) + }) + + PContext("SecretRef", func() { + }) +}) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index daa720c38..8cff31d21 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -24,6 +24,7 @@ import ( . "github.com/onsi/gomega" _ "github.com/api7/api7-ingress-controller/test/e2e/adminapi" + _ "github.com/api7/api7-ingress-controller/test/e2e/crds" "github.com/api7/api7-ingress-controller/test/e2e/framework" _ "github.com/api7/api7-ingress-controller/test/e2e/gatewayapi" ) diff --git a/test/e2e/scaffold/k8s.go b/test/e2e/scaffold/k8s.go index 5a9aa9efb..e6cb5a3ad 100644 --- a/test/e2e/scaffold/k8s.go +++ b/test/e2e/scaffold/k8s.go @@ -223,3 +223,22 @@ func (s *Scaffold) RunDigDNSClientFromK8s(args ...string) (string, error) { kubectlArgs = append(kubectlArgs, args...) return s.RunKubectlAndGetOutput(kubectlArgs...) } + +func (s *Scaffold) ResourceApplied(resourType, resourceName, resourceRaw string, observedGeneration int) { + Expect(s.CreateResourceFromString(resourceRaw)). + NotTo(HaveOccurred(), fmt.Sprintf("creating %s", resourType)) + + Eventually(func() string { + hryaml, err := s.GetResourceYaml(resourType, resourceName) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("getting %s yaml", resourType)) + return hryaml + }).WithTimeout(8*time.Second).ProbeEvery(2*time.Second). + Should( + SatisfyAll( + ContainSubstring(`status: "True"`), + ContainSubstring(fmt.Sprintf("observedGeneration: %d", observedGeneration)), + ), + fmt.Sprintf("checking %s condition status", resourType), + ) + time.Sleep(1 * time.Second) +}