Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add plugin_config_namespace parameter to ApisixRoute #2137

Merged
merged 16 commits into from
Jan 18, 2024
1 change: 1 addition & 0 deletions docs/en/latest/references/apisix_route_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The table below describes each of the attributes in the spec. The fields `apiVer
| http[].match.exprs[].set | array | Set to compare the subject with. Only used when the operator is `In` or `NotIn`. Can use either this or `http[].match.exprs[].value`. |
| http[].websocket | boolean | When set to `true` enables websocket proxy. |
| http[].plugin_config_name | string | Existing Plugin Config name to use in the Route. |
| http[].plugin_config_namespace | string | Namespace in which to look for `plugin_config_name` Route. |
| http[].backends | object | List of backend services. If there are more than one, a weight based traffic split policy would be applied. |
| http[].backends[].serviceName | string | Name of the backend service. The service and the `ApisixRoute` resource should be created in the same namespace. |
| http[].backends[].servicePort | integer or string | Port number or the name defined in the service object of the backend. |
Expand Down
10 changes: 6 additions & 4 deletions pkg/kube/apisix/apis/config/v2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ type ApisixRouteHTTP struct {
// Upstreams refer to ApisixUpstream CRD
Upstreams []ApisixRouteUpstreamReference `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`

Websocket bool `json:"websocket" yaml:"websocket"`
PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
Authentication ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
Websocket bool `json:"websocket" yaml:"websocket"`
PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
//By default, PluginConfigNamespace will be the same as the namespace of ApisixRoute
PluginConfigNamespace string `json:"plugin_config_namespace,omitempty" yaml:"plugin_config_namespace,omitempty"`
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
Authentication ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
}

// ApisixRouteHTTPBackend represents an HTTP backend (a Kubernetes Service).
Expand Down
10 changes: 7 additions & 3 deletions pkg/providers/apisix/apisix_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,16 +397,20 @@ updateStatus:
func (c *apisixRouteController) checkPluginNameIfNotEmptyV2(ctx context.Context, in *v2.ApisixRoute) error {
for _, v := range in.Spec.HTTP {
if v.PluginConfigName != "" {
_, err := c.APISIX.Cluster(c.Config.APISIX.DefaultClusterName).PluginConfig().Get(ctx, apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName))
ns := in.Namespace
if v.PluginConfigNamespace != "" {
ns = v.PluginConfigNamespace
}
_, err := c.APISIX.Cluster(c.Config.APISIX.DefaultClusterName).PluginConfig().Get(ctx, apisixv1.ComposePluginConfigName(ns, v.PluginConfigName))
if err != nil {
if err == apisixcache.ErrNotFound {
log.Errorw("checkPluginNameIfNotEmptyV2 error: plugin_config not found",
zap.String("name", apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName)),
zap.String("name", apisixv1.ComposePluginConfigName(ns, v.PluginConfigName)),
zap.Any("obj", in),
zap.Error(err))
} else {
log.Errorw("checkPluginNameIfNotEmptyV2 PluginConfig get failed",
zap.String("name", apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName)),
zap.String("name", apisixv1.ComposePluginConfigName(ns, v.PluginConfigName)),
zap.Any("obj", in),
zap.Error(err))
}
Expand Down
12 changes: 10 additions & 2 deletions pkg/providers/apisix/translation/apisix_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,11 @@ func (t *translator) translateHTTPRouteV2(ctx *translation.TranslateContext, ar
route.FilterFunc = part.Match.FilterFunc

if part.PluginConfigName != "" {
route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ar.Namespace, part.PluginConfigName))
ns := ar.Namespace
if part.PluginConfigNamespace != "" {
ns = part.PluginConfigNamespace
}
route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ns, part.PluginConfigName))
}

for k, v := range ar.ObjectMeta.Labels {
Expand Down Expand Up @@ -465,7 +469,11 @@ func (t *translator) generateHTTPRouteV2DeleteMark(ctx *translation.TranslateCon
route.Name = apisixv1.ComposeRouteName(ar.Namespace, ar.Name, part.Name)
route.ID = id.GenID(route.Name)
if part.PluginConfigName != "" {
route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ar.Namespace, part.PluginConfigName))
ns := ar.Namespace
if part.PluginConfigNamespace != "" {
ns = part.PluginConfigNamespace
}
route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ns, part.PluginConfigName))
}

ctx.AddRoute(route)
Expand Down
40 changes: 40 additions & 0 deletions pkg/providers/apisix/translation/apisix_route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,46 @@ func TestTranslateApisixRouteV2WithEmptyPluginConfigName(t *testing.T) {
assert.Equal(t, "", res.Routes[2].PluginConfigId)
}

func TestTranslateApisixRouteV2WithPluginConfigNamespace(t *testing.T) {
tr, processCh := mockTranslatorV2(t)
<-processCh
<-processCh
pluginConfigNamespace := "test-2"
ar := &configv2.ApisixRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "ar",
Namespace: "test",
},
Spec: configv2.ApisixRouteSpec{
HTTP: []configv2.ApisixRouteHTTP{
{
Name: "rule1",
Match: configv2.ApisixRouteHTTPMatch{
Paths: []string{
"/*",
},
},
Backends: []configv2.ApisixRouteHTTPBackend{
{
ServiceName: "svc",
ServicePort: intstr.IntOrString{
IntVal: 80,
},
},
},
PluginConfigName: "test-PluginConfigName-1",
PluginConfigNamespace: pluginConfigNamespace,
},
},
},
}
res, err := tr.TranslateRouteV2(ar)
assert.NoError(t, err)
assert.Len(t, res.PluginConfigs, 0)
expectedPluginId := id.GenID(apisixv1.ComposePluginConfigName(pluginConfigNamespace, ar.Spec.HTTP[0].PluginConfigName))
assert.Equal(t, expectedPluginId, res.Routes[0].PluginConfigId)
}

func TestGenerateApisixRouteV2DeleteMark(t *testing.T) {
tr := &translator{
&TranslatorOptions{},
Expand Down
3 changes: 3 additions & 0 deletions samples/deploy/crd/v1/ApisixRoute.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ spec:
plugin_config_name:
type: string
minLength: 1
plugin_config_namespace:
type: string
minLength: 1
upstreams:
description: Upstreams refer to ApisixUpstream CRD
type: array
Expand Down
90 changes: 90 additions & 0 deletions test/e2e/suite-plugins/suite-plugins-other/plugin_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,3 +594,93 @@ spec:
resp.Status(http.StatusOK)
})
})

var _ = ginkgo.Describe("suite-plugins-other: ApisixPluginConfig cross namespace", func() {
s := scaffold.NewScaffold(&scaffold.Options{
NamespaceSelectorLabel: map[string][]string{
"apisix.ingress.watch": {"test"},
},
})
ginkgo.It("ApisixPluginConfig cross namespace", func() {
testns := `
apiVersion: v1
kind: Namespace
metadata:
name: test
labels:
apisix.ingress.watch: test
`
err := s.CreateResourceFromString(testns)
assert.Nil(ginkgo.GinkgoT(), err, "Creating test namespace")
backendSvc, backendPorts := s.DefaultHTTPBackend()
apc := fmt.Sprintf(`
apiVersion: apisix.apache.org/v2
kind: ApisixPluginConfig
metadata:
name: echo-and-cors-apc
namespace: test
spec:
plugins:
- name: echo
enable: true
config:
before_body: "This is the preface"
after_body: "This is the epilogue"
headers:
X-Foo: v1
X-Foo2: v2
- name: cors
enable: true
`)
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromStringWithNamespace(apc, "test"))

err = s.EnsureNumApisixPluginConfigCreated(1)
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of pluginConfigs")

time.Sleep(time.Second * 3)

ar := fmt.Sprintf(`
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
http:
- name: rule1
match:
hosts:
- httpbin.org
paths:
- /ip
backends:
- serviceName: %s
servicePort: %d
weight: 10
plugin_config_name: echo-and-cors-apc
plugin_config_namespace: test
`, backendSvc, backendPorts[0])
assert.Nil(ginkgo.GinkgoT(), s.CreateVersionedApisixResource(ar))

err = s.EnsureNumApisixRoutesCreated(1)
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of routes")

time.Sleep(3 * time.Second)
pcs, err := s.ListApisixPluginConfig()
assert.Nil(ginkgo.GinkgoT(), err, nil, "listing pluginConfigs")
assert.Len(ginkgo.GinkgoT(), pcs, 1)
assert.Len(ginkgo.GinkgoT(), pcs[0].Plugins, 2)

resp := s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect()
resp.Status(http.StatusOK)
resp.Header("X-Foo").Equal("v1")
resp.Header("X-Foo2").Equal("v2")
resp.Header("Access-Control-Allow-Origin").Equal("*")
resp.Header("Access-Control-Allow-Methods").Equal("*")
resp.Header("Access-Control-Allow-Headers").Equal("*")
resp.Header("Access-Control-Expose-Headers").Equal("*")
resp.Header("Access-Control-Max-Age").Equal("5")
resp.Body().Contains("This is the preface")
resp.Body().Contains("origin")
resp.Body().Contains("This is the epilogue")
})
})
Loading