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

Add permanent-redirect and permanent-redirect-code annotations #165

Merged
merged 9 commits into from
Dec 6, 2023
15 changes: 9 additions & 6 deletions internal/caddy/ingress/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package ingress
import v1 "k8s.io/api/networking/v1"

const (
annotationPrefix = "caddy.ingress.kubernetes.io"
rewriteToAnnotation = "rewrite-to"
rewriteStripPrefixAnnotation = "rewrite-strip-prefix"
disableSSLRedirect = "disable-ssl-redirect"
backendProtocol = "backend-protocol"
insecureSkipVerify = "insecure-skip-verify"
annotationPrefix = "caddy.ingress.kubernetes.io"
rewriteToAnnotation = "rewrite-to"
rewriteStripPrefixAnnotation = "rewrite-strip-prefix"
disableSSLRedirect = "disable-ssl-redirect"
backendProtocol = "backend-protocol"
insecureSkipVerify = "insecure-skip-verify"
permanentRedirectAnnotation = "permanent-redirect"
permanentRedirectCodeAnnotation = "permanent-redirect-code"
temporaryRedirectAnnotation = "temporal-redirect"
)

func getAnnotation(ing *v1.Ingress, rule string) string {
Expand Down
82 changes: 82 additions & 0 deletions internal/caddy/ingress/redirect.go
mavimo marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ingress

import (
"fmt"
"net/http"
"strconv"

"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
)

type RedirectPlugin struct{}

func (p RedirectPlugin) IngressPlugin() converter.PluginInfo {
return converter.PluginInfo{
Name: "ingress.redirect",
Priority: 10,
New: func() converter.Plugin { return new(RedirectPlugin) },
}
}

// IngressHandler Converts redirect annotations to static_response handler
func (p RedirectPlugin) IngressHandler(input converter.IngressMiddlewareInput) (*caddyhttp.Route, error) {
ing := input.Ingress

permanentRedir := getAnnotation(ing, permanentRedirectAnnotation)
temporaryRedir := getAnnotation(ing, temporaryRedirectAnnotation)

var code string = "301"
var redirectTo string = ""

// Don't allow both redirect annotations to be set
if permanentRedir != "" && temporaryRedir != "" {
return nil, fmt.Errorf("cannot use permanent-redirect annotation with temporal-redirect")
}

if permanentRedir != "" {
redirectCode := getAnnotation(ing, permanentRedirectCodeAnnotation)
if redirectCode != "" {
codeInt, err := strconv.Atoi(redirectCode)
if err != nil {
return nil, fmt.Errorf("not a supported redirection code type or not a valid integer: '%s'", redirectCode)
}

if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, fmt.Errorf("redirection code not in the 3xx range or 401: '%v'", codeInt)
}

code = redirectCode
}
redirectTo = permanentRedir
}

if temporaryRedir != "" {
code = "302"
redirectTo = temporaryRedir
}

if redirectTo != "" {
handler := caddyconfig.JSONModuleObject(
caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(code),
Headers: http.Header{"Location": []string{redirectTo}},
},
"handler", "static_response", nil,
)

input.Route.HandlersRaw = append(input.Route.HandlersRaw, handler)
}

return input.Route, nil
}

func init() {
converter.RegisterPlugin(RedirectPlugin{})
}

// Interface guards
var (
_ = converter.IngressMiddleware(RedirectPlugin{})
)
135 changes: 135 additions & 0 deletions internal/caddy/ingress/redirect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package ingress

import (
"encoding/json"
"os"
"testing"

"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/ingress/pkg/converter"
"github.com/stretchr/testify/assert"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestRedirectConvertToCaddyConfig(t *testing.T) {
rp := RedirectPlugin{}

tests := []struct {
name string
expectedConfigPath string
expectedError string
annotations map[string]string
}{
{
name: "Check permanent redirect without any specific redirect code",
expectedConfigPath: "test_data/redirect_default.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
},
},
{
name: "Check permanent redirect with custom redirect code",
expectedConfigPath: "test_data/redirect_custom_code.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "308",
},
},
{
name: "Check permanent redirect with 401 as redirect code",
expectedConfigPath: "test_data/redirect_401.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "401",
},
},
{
name: "Check temporary redirect",
expectedConfigPath: "test_data/redirect_temporary.json",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/temporal-redirect": "http://example.com",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
},
},
Route: &caddyhttp.Route{},
}

route, err := rp.IngressHandler(input)
assert.NoError(t, err, "failed to generate ingress route")

expectedCfg, err := os.ReadFile(test.expectedConfigPath)
assert.NoError(t, err, "failed to find config file for comparison")

cfgJson, err := json.Marshal(&route)
assert.NoError(t, err, "failed to marshal route to JSON")

assert.JSONEq(t, string(cfgJson), string(expectedCfg))
})
}
}

func TestMisconfiguredRedirectConvertToCaddyConfig(t *testing.T) {
rp := RedirectPlugin{}

tests := []struct {
name string
expectedError string
annotations map[string]string
}{
{
name: "Check permanent redirect with invalid custom redirect code",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "502",
},
expectedError: "redirection code not in the 3xx range or 401: '502'",
},
{
name: "Check permanent redirect with invalid custom redirect code string",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/permanent-redirect-code": "randomstring",
},
expectedError: "not a supported redirection code type or not a valid integer: 'randomstring'",
},
{
name: "Check if both permanent and temporary redirection annotations are set",
annotations: map[string]string{
"caddy.ingress.kubernetes.io/permanent-redirect": "http://example.com",
"caddy.ingress.kubernetes.io/temporal-redirect": "http://example2.com",
},
expectedError: "cannot use permanent-redirect annotation with temporal-redirect",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := converter.IngressMiddlewareInput{
Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: test.annotations,
},
},
Route: &caddyhttp.Route{},
}

route, err := rp.IngressHandler(input)
if assert.Error(t, err, "expected an error while generating the ingress route") {
assert.EqualError(t, err, test.expectedError)
}

cfgJson, err := json.Marshal(&route)
assert.NoError(t, err, "failed to marshal route to JSON")

assert.JSONEq(t, string(cfgJson), "null")
})
}
}
13 changes: 13 additions & 0 deletions internal/caddy/ingress/test_data/redirect_401.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 401
}
]
}
13 changes: 13 additions & 0 deletions internal/caddy/ingress/test_data/redirect_custom_code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 308
}
]
}
13 changes: 13 additions & 0 deletions internal/caddy/ingress/test_data/redirect_default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 301
}
]
}
13 changes: 13 additions & 0 deletions internal/caddy/ingress/test_data/redirect_temporary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"http://example.com"
]
},
"status_code": 302
}
]
}