From 23e5ebdb837cf581db94a613e02e292167d52eae Mon Sep 17 00:00:00 2001 From: Alex Zhang Date: Fri, 28 May 2021 10:17:44 +0800 Subject: [PATCH] feat: apisixconsumer translator (#474) --- pkg/ingress/controller.go | 2 +- pkg/kube/translation/apisix_consumer.go | 47 ++++++ pkg/kube/translation/apisix_consumer_test.go | 74 +++++++++ pkg/kube/translation/plugin.go | 50 ++++++ pkg/kube/translation/plugin_test.go | 154 ++++++++++++++++++- pkg/kube/translation/translator.go | 5 +- pkg/types/apisix/v1/plugin_types.go | 26 ++++ pkg/types/apisix/v1/types.go | 23 +++ pkg/types/apisix/v1/zz_generated.deepcopy.go | 77 ++++++++-- 9 files changed, 440 insertions(+), 18 deletions(-) create mode 100644 pkg/kube/translation/apisix_consumer.go create mode 100644 pkg/kube/translation/apisix_consumer_test.go diff --git a/pkg/ingress/controller.go b/pkg/ingress/controller.go index 9914c0283c..c283034e5d 100644 --- a/pkg/ingress/controller.go +++ b/pkg/ingress/controller.go @@ -435,7 +435,7 @@ func (c *Controller) namespaceWatching(key string) (ok bool) { } ns, _, err := cache.SplitMetaNamespaceKey(key) if err != nil { - // Ignore resource with invalid key. + // Ignore resource pkg/types/apisix/v1/plugin_types.gowith invalid key. ok = false log.Warnf("resource %s was ignored since: %s", key, err) return diff --git a/pkg/kube/translation/apisix_consumer.go b/pkg/kube/translation/apisix_consumer.go new file mode 100644 index 0000000000..67672c654d --- /dev/null +++ b/pkg/kube/translation/apisix_consumer.go @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package translation + +import ( + "fmt" + + configv2alpha1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1" + apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" +) + +func (t *translator) TranslateApisixConsumer(ac *configv2alpha1.ApisixConsumer) (*apisixv1.Consumer, error) { + // As the CRD schema ensures that only one authN can be configured, + // so here the order is no matter. + + plugins := make(apisixv1.Plugins) + if ac.Spec.AuthParameter.KeyAuth != nil { + cfg, err := t.translateConsumerKeyAuthPlugin(ac.Namespace, ac.Spec.AuthParameter.KeyAuth) + if err != nil { + return nil, fmt.Errorf("invalid key auth config: %s", err) + } + plugins["key-auth"] = cfg + } else if ac.Spec.AuthParameter.BasicAuth != nil { + cfg, err := t.translateConsumerBasicAuthPlugin(ac.Namespace, ac.Spec.AuthParameter.BasicAuth) + if err != nil { + return nil, fmt.Errorf("invalid basic auth config: %s", err) + } + plugins["basic-auth"] = cfg + } + + consumer := apisixv1.NewDefaultConsumer() + consumer.Username = apisixv1.ComposeConsumerName(ac.Namespace, ac.Name) + consumer.Plugins = plugins + return consumer, nil +} diff --git a/pkg/kube/translation/apisix_consumer_test.go b/pkg/kube/translation/apisix_consumer_test.go new file mode 100644 index 0000000000..cf1c796976 --- /dev/null +++ b/pkg/kube/translation/apisix_consumer_test.go @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package translation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv2alpha1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1" + apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" +) + +func TestTranslateApisixConsumer(t *testing.T) { + ac := &configv2alpha1.ApisixConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "jack", + Namespace: "qa", + }, + Spec: configv2alpha1.ApisixConsumerSpec{ + AuthParameter: configv2alpha1.ApisixConsumerAuthParameter{ + BasicAuth: &configv2alpha1.ApisixConsumerBasicAuth{ + Value: &configv2alpha1.ApisixConsumerBasicAuthValue{ + Username: "jack", + Password: "jacknice", + }, + }, + }, + }, + } + consumer, err := (&translator{}).TranslateApisixConsumer(ac) + assert.Nil(t, err) + assert.Len(t, consumer.Plugins, 1) + cfg := consumer.Plugins["basic-auth"].(*apisixv1.BasicAuthConsumerConfig) + assert.Equal(t, cfg.Username, "jack") + assert.Equal(t, cfg.Password, "jacknice") + + ac = &configv2alpha1.ApisixConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "jack", + Namespace: "qa", + }, + Spec: configv2alpha1.ApisixConsumerSpec{ + AuthParameter: configv2alpha1.ApisixConsumerAuthParameter{ + KeyAuth: &configv2alpha1.ApisixConsumerKeyAuth{ + Value: &configv2alpha1.ApisixConsumerKeyAuthValue{ + Key: "qwerty", + }, + }, + }, + }, + } + consumer, err = (&translator{}).TranslateApisixConsumer(ac) + assert.Nil(t, err) + assert.Len(t, consumer.Plugins, 1) + cfg2 := consumer.Plugins["key-auth"].(*apisixv1.KeyAuthConsumerConfig) + assert.Equal(t, cfg2.Key, "qwerty") + + // No test test cases for secret references as we already test them + // in plugin_test.go. +} diff --git a/pkg/kube/translation/plugin.go b/pkg/kube/translation/plugin.go index 9e14356a2d..d58379d72f 100644 --- a/pkg/kube/translation/plugin.go +++ b/pkg/kube/translation/plugin.go @@ -15,10 +15,18 @@ package translation import ( + "errors" + configv2alpha1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1" apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) +var ( + _errKeyNotFoundOrInvalid = errors.New("key \"key\" not found or invalid in secret") + _errUsernameNotFoundOrInvalid = errors.New("key \"username\" not found or invalid in secret") + _errPasswordNotFoundOrInvalid = errors.New("key \"password\" not found or invalid in secret") +) + func (t *translator) translateTrafficSplitPlugin(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute, defaultBackendWeight int, backends []*configv2alpha1.ApisixRouteHTTPBackend) (*apisixv1.TrafficSplitConfig, error) { var ( @@ -60,3 +68,45 @@ func (t *translator) translateTrafficSplitPlugin(ctx *TranslateContext, ar *conf } return tsCfg, nil } + +func (t *translator) translateConsumerKeyAuthPlugin(consumerNamespace string, cfg *configv2alpha1.ApisixConsumerKeyAuth) (*apisixv1.KeyAuthConsumerConfig, error) { + if cfg.Value != nil { + return &apisixv1.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil + } + + sec, err := t.SecretLister.Secrets(consumerNamespace).Get(cfg.SecretRef.Name) + if err != nil { + return nil, err + } + raw, ok := sec.Data["key"] + if !ok || len(raw) == 0 { + return nil, _errKeyNotFoundOrInvalid + } + return &apisixv1.KeyAuthConsumerConfig{Key: string(raw)}, nil +} + +func (t *translator) translateConsumerBasicAuthPlugin(consumerNamespace string, cfg *configv2alpha1.ApisixConsumerBasicAuth) (*apisixv1.BasicAuthConsumerConfig, error) { + if cfg.Value != nil { + return &apisixv1.BasicAuthConsumerConfig{ + Username: cfg.Value.Username, + Password: cfg.Value.Password, + }, nil + } + + sec, err := t.SecretLister.Secrets(consumerNamespace).Get(cfg.SecretRef.Name) + if err != nil { + return nil, err + } + raw1, ok := sec.Data["username"] + if !ok || len(raw1) == 0 { + return nil, _errUsernameNotFoundOrInvalid + } + raw2, ok := sec.Data["password"] + if !ok || len(raw2) == 0 { + return nil, _errPasswordNotFoundOrInvalid + } + return &apisixv1.BasicAuthConsumerConfig{ + Username: string(raw1), + Password: string(raw2), + }, nil +} diff --git a/pkg/kube/translation/plugin_test.go b/pkg/kube/translation/plugin_test.go index 172429a479..c249be65bb 100644 --- a/pkg/kube/translation/plugin_test.go +++ b/pkg/kube/translation/plugin_test.go @@ -18,8 +18,6 @@ import ( "context" "testing" - "github.com/apache/apisix-ingress-controller/pkg/id" - "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,6 +26,7 @@ import ( "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" + "github.com/apache/apisix-ingress-controller/pkg/id" configv2alpha1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1" apisixfake "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/clientset/versioned/fake" apisixinformers "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/informers/externalversions" @@ -533,3 +532,154 @@ func TestTranslateTrafficSplitPluginBadCases(t *testing.T) { assert.Nil(t, cfg) assert.Equal(t, err.Error(), "conflict headless service and backend resolve granularity") } + +func TestTranslateConsumerKeyAuthPluginWithInPlaceValue(t *testing.T) { + keyAuth := &configv2alpha1.ApisixConsumerKeyAuth{ + Value: &configv2alpha1.ApisixConsumerKeyAuthValue{Key: "abc"}, + } + cfg, err := (&translator{}).translateConsumerKeyAuthPlugin("default", keyAuth) + assert.Nil(t, err) + assert.Equal(t, cfg.Key, "abc") +} + +func TestTranslateConsumerKeyAuthWithSecretRef(t *testing.T) { + sec := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc-key-auth", + }, + Data: map[string][]byte{ + "key": []byte("abc"), + }, + } + client := fake.NewSimpleClientset() + informersFactory := informers.NewSharedInformerFactory(client, 0) + secretInformer := informersFactory.Core().V1().Secrets().Informer() + secretLister := informersFactory.Core().V1().Secrets().Lister() + processCh := make(chan struct{}) + stopCh := make(chan struct{}) + secretInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(_ interface{}) { + processCh <- struct{}{} + }, + UpdateFunc: func(_, _ interface{}) { + processCh <- struct{}{} + }, + }) + go secretInformer.Run(stopCh) + + tr := &translator{ + &TranslatorOptions{ + SecretLister: secretLister, + }, + } + _, err := client.CoreV1().Secrets("default").Create(context.Background(), sec, metav1.CreateOptions{}) + assert.Nil(t, err) + + <-processCh + + keyAuth := &configv2alpha1.ApisixConsumerKeyAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "abc-key-auth"}, + } + cfg, err := tr.translateConsumerKeyAuthPlugin("default", keyAuth) + assert.Nil(t, err) + assert.Equal(t, cfg.Key, "abc") + + cfg, err = tr.translateConsumerKeyAuthPlugin("default2", keyAuth) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "not found") + + delete(sec.Data, "key") + _, err = client.CoreV1().Secrets("default").Update(context.Background(), sec, metav1.UpdateOptions{}) + assert.Nil(t, err) + <-processCh + + cfg, err = tr.translateConsumerKeyAuthPlugin("default", keyAuth) + assert.Nil(t, cfg) + assert.Equal(t, err, _errKeyNotFoundOrInvalid) + + close(processCh) + close(stopCh) +} + +func TestTranslateConsumerBasicAuthPluginWithInPlaceValue(t *testing.T) { + basicAuth := &configv2alpha1.ApisixConsumerBasicAuth{ + Value: &configv2alpha1.ApisixConsumerBasicAuthValue{ + Username: "jack", + Password: "jacknice", + }, + } + cfg, err := (&translator{}).translateConsumerBasicAuthPlugin("default", basicAuth) + assert.Nil(t, err) + assert.Equal(t, cfg.Username, "jack") + assert.Equal(t, cfg.Password, "jacknice") +} + +func TestTranslateConsumerBasicAuthWithSecretRef(t *testing.T) { + sec := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "jack-basic-auth", + }, + Data: map[string][]byte{ + "username": []byte("jack"), + "password": []byte("jacknice"), + }, + } + client := fake.NewSimpleClientset() + informersFactory := informers.NewSharedInformerFactory(client, 0) + secretInformer := informersFactory.Core().V1().Secrets().Informer() + secretLister := informersFactory.Core().V1().Secrets().Lister() + processCh := make(chan struct{}) + stopCh := make(chan struct{}) + secretInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(_ interface{}) { + processCh <- struct{}{} + }, + UpdateFunc: func(_, _ interface{}) { + processCh <- struct{}{} + }, + }) + go secretInformer.Run(stopCh) + + tr := &translator{ + &TranslatorOptions{ + SecretLister: secretLister, + }, + } + _, err := client.CoreV1().Secrets("default").Create(context.Background(), sec, metav1.CreateOptions{}) + assert.Nil(t, err) + + <-processCh + + basicAuth := &configv2alpha1.ApisixConsumerBasicAuth{ + SecretRef: &corev1.LocalObjectReference{Name: "jack-basic-auth"}, + } + cfg, err := tr.translateConsumerBasicAuthPlugin("default", basicAuth) + assert.Nil(t, err) + assert.Equal(t, cfg.Username, "jack") + assert.Equal(t, cfg.Password, "jacknice") + + cfg, err = tr.translateConsumerBasicAuthPlugin("default2", basicAuth) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "not found") + + delete(sec.Data, "password") + _, err = client.CoreV1().Secrets("default").Update(context.Background(), sec, metav1.UpdateOptions{}) + assert.Nil(t, err) + <-processCh + + cfg, err = tr.translateConsumerBasicAuthPlugin("default", basicAuth) + assert.Nil(t, cfg) + assert.Equal(t, err, _errPasswordNotFoundOrInvalid) + + delete(sec.Data, "username") + _, err = client.CoreV1().Secrets("default").Update(context.Background(), sec, metav1.UpdateOptions{}) + assert.Nil(t, err) + <-processCh + + cfg, err = tr.translateConsumerBasicAuthPlugin("default", basicAuth) + assert.Nil(t, cfg) + assert.Equal(t, err, _errUsernameNotFoundOrInvalid) + + close(processCh) + close(stopCh) +} diff --git a/pkg/kube/translation/translator.go b/pkg/kube/translation/translator.go index 9742d2a8d6..5b6c753bac 100644 --- a/pkg/kube/translation/translator.go +++ b/pkg/kube/translation/translator.go @@ -68,7 +68,10 @@ type Translator interface { TranslateSSL(*configv1.ApisixTls) (*apisixv1.Ssl, error) // TranslateClusterConfig translates the configv2alpha1.ApisixClusterConfig object into the APISIX // Global Rule resource. - TranslateClusterConfig(config *configv2alpha1.ApisixClusterConfig) (*apisixv1.GlobalRule, error) + TranslateClusterConfig(*configv2alpha1.ApisixClusterConfig) (*apisixv1.GlobalRule, error) + // TranslateApisixConsumer translates the configv2alpha1.APisixConsumer object into the APISIX Consumer + // resource. + TranslateApisixConsumer(*configv2alpha1.ApisixConsumer) (*apisixv1.Consumer, error) } // TranslatorOptions contains options to help Translator diff --git a/pkg/types/apisix/v1/plugin_types.go b/pkg/types/apisix/v1/plugin_types.go index a9f505ffd3..11e5788380 100644 --- a/pkg/types/apisix/v1/plugin_types.go +++ b/pkg/types/apisix/v1/plugin_types.go @@ -49,6 +49,32 @@ type CorsConfig struct { AllowHeaders string `json:"allow_headers,omitempty"` } +// KeyAuthConsumerConfig is the rule config for key-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type KeyAuthConsumerConfig struct { + Key string `json:"key"` +} + +// KeyAuthRouteConfig is the rule config for key-auth plugin +// used in Route object. +type KeyAuthRouteConfig struct { + Header string `json:"header,omitempty"` +} + +// BasicAuthConsumerConfig is the rule config for basic-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type BasicAuthConsumerConfig struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// BasicAuthRouteConfig is the rule config for basic-auth plugin +// used in Route object. +// +k8s:deepcopy-gen=true +type BasicAuthRouteConfig struct{} + // RewriteConfig is the rule config for proxy-rewrite plugin. // +k8s:deepcopy-gen=true type RewriteConfig struct { diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go index 153e97d99a..393b461e71 100644 --- a/pkg/types/apisix/v1/types.go +++ b/pkg/types/apisix/v1/types.go @@ -369,6 +369,16 @@ func NewDefaultStreamRoute() *StreamRoute { } } +// NewDefaultConsumer returns an empty Consumer with default values. +func NewDefaultConsumer() *Consumer { + return &Consumer{ + Desc: "Created by apisix-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "apisix-ingress-controller", + }, + } +} + // ComposeUpstreamName uses namespace, name and port info to compose // the upstream name. func ComposeUpstreamName(namespace, name string, port int32) string { @@ -421,3 +431,16 @@ func ComposeStreamRouteName(namespace, name string, rule string) string { return buf.String() } + +// ComposeConsumerName uses namespace and name of ApisixConsumer to compose +// the Consumer name. +func ComposeConsumerName(namespace, name string) string { + p := make([]byte, 0, len(namespace)+len(name)+1) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteString("_") + buf.WriteString(name) + + return buf.String() +} diff --git a/pkg/types/apisix/v1/zz_generated.deepcopy.go b/pkg/types/apisix/v1/zz_generated.deepcopy.go index b75a91de41..c7989222ba 100644 --- a/pkg/types/apisix/v1/zz_generated.deepcopy.go +++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go @@ -1,24 +1,57 @@ // +build !ignore_autogenerated -// Licensed to the Apache Software Foundation (ASF) under one or more -// contributor license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright ownership. -// The ASF licenses this file to You under the Apache License, Version 2.0 -// (the "License"); you may not use this file except in compliance with -// the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ // Code generated by deepcopy-gen. DO NOT EDIT. package v1 +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConsumerConfig) DeepCopyInto(out *BasicAuthConsumerConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConsumerConfig. +func (in *BasicAuthConsumerConfig) DeepCopy() *BasicAuthConsumerConfig { + if in == nil { + return nil + } + out := new(BasicAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthRouteConfig) DeepCopyInto(out *BasicAuthRouteConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthRouteConfig. +func (in *BasicAuthRouteConfig) DeepCopy() *BasicAuthRouteConfig { + if in == nil { + return nil + } + out := new(BasicAuthRouteConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Consumer) DeepCopyInto(out *Consumer) { *out = *in @@ -102,6 +135,22 @@ func (in *IPRestrictConfig) DeepCopy() *IPRestrictConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConsumerConfig) DeepCopyInto(out *KeyAuthConsumerConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConsumerConfig. +func (in *KeyAuthConsumerConfig) DeepCopy() *KeyAuthConsumerConfig { + if in == nil { + return nil + } + out := new(KeyAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metadata) DeepCopyInto(out *Metadata) { *out = *in