8000 feat: add support for HTTP01 Challenges on VirtualServer resources (#… · nginx/kubernetes-ingress@56c756d · GitHub
[go: up one dir, main page]

Skip to content

Commit 56c756d

Browse files
authored
feat: add support for HTTP01 Challenges on VirtualServer resources (#2759)
* Treat challenge ingress as VSR * Add unit tests and tidy up
1 parent 816747c commit 56c756d

File tree

5 files changed

+245
-1
lines changed

5 files changed

+245
-1
lines changed

internal/k8s/configuration.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ type Configuration struct {
358358
internalRoutesEnabled bool
359359
isTLSPassthroughEnabled bool
360360
snippetsEnabled bool
361+
isCertManagerEnabled bool
361362

362363
lock sync.RWMutex
363364
}
@@ -374,6 +375,7 @@ func NewConfiguration(
374375
transportServerValidator *validation.TransportServerValidator,
375376
isTLSPassthroughEnabled bool,
376377
snippetsEnabled bool,
378+
isCertManagerEnabled bool,
377379
) *Configuration {
378380
return &Configuration{
379381
hosts: make(map[string]Resource),
@@ -400,6 +402,7 @@ func NewConfiguration(
400402
internalRoutesEnabled: internalRoutesEnabled,
401403
isTLSPassthroughEnabled: isTLSPassthroughEnabled,
402404
snippetsEnabled: snippetsEnabled,
405+
isCertManagerEnabled: isCertManagerEnabled,
403406
}
404407
}
405408

@@ -1279,6 +1282,7 @@ func squashResourceChanges(changes []ResourceChange) []ResourceChange {
12791282
func (c *Configuration) buildHostsAndResources() (newHosts map[string]Resource, newResources map[string]Resource) {
12801283
newHosts = make(map[string]Resource)
12811284
newResources = make(map[string]Resource)
1285+
var challengesVSR []*conf_v1.VirtualServerRoute
12821286

12831287
// Step 1 - Build hosts from Ingress resources
12841288

@@ -1291,6 +1295,14 @@ func (c *Configuration) buildHostsAndResources() (newHosts map[string]Resource,
12911295

12921296
var resource *IngressConfiguration
12931297

1298+
if val := c.isChallengeIngress(ing); val {
1299+
// if using cert-manager with Ingress, the challenge Ingress must be Minion
1300+
// and this code won't be reached. With VS, the challenge Ingress must not be Minion.
1301+
vsr := c.convertIngressToVSR(ing)
1302+
challengesVSR = append(challengesVSR, vsr)
1303+
continue
1304+
}
1305+
12941306
if isMaster(ing) {
12951307
minions, childWarnings := c.buildMinionConfigs(ing.Spec.Rules[0].Host)
12961308
resource = NewMasterIngressConfiguration(ing, minions, childWarnings)
@@ -1324,6 +1336,11 @@ func (c *Configuration) buildHostsAndResources() (newHosts map[string]Resource,
13241336
vs := c.virtualServers[key]
13251337

13261338
vsrs, warnings := c.buildVirtualServerRoutes(vs)
1339+
for _, vsr := range challengesVSR {
1340+
if vs.Spec.Host == vsr.Spec.Host {
1341+
vsrs = append(vsrs, vsr)
1342+
}
1343+
}
13271344
resource := NewVirtualServerConfiguration(vs, vsrs, warnings)
13281345

13291346
newResources[resource.GetKeyWithKind()] = resource
@@ -1377,6 +1394,44 @@ func (c *Configuration) buildHostsAndResources() (newHosts map[string]Resource,
13771394
return newHosts, newResources
13781395
}
13791396

1397+
func (c *Configuration) isChallengeIngress(ing *networking.Ingress) bool {
1398+
if !c.isCertManagerEnabled {
1399+
return false
1400+
}
1401+
return ing.Labels["acme.cert-manager.io/http01-solver"] == "true"
1402+
}
1403+
1404+
func (c *Configuration) convertIngressToVSR(ing *networking.Ingress) *conf_v1.VirtualServerRoute {
1405+
rule := ing.Spec.Rules[0]
1406+
1407+
vs := &conf_v1.VirtualServerRoute{
1408+
ObjectMeta: metav1.ObjectMeta{
1409+
Namespace: ing.Namespace,
1410+
Name: ing.Name,
1411+
},
1412+
Spec: conf_v1.VirtualServerRouteSpec{
1413+
Host: rule.Host,
1414+
Upstreams: []conf_v1.Upstream{
1415+
{
1416+
Name: "challenge",
1417+
Service: rule.HTTP.Paths[0].Backend.Service.Name,
1418+
Port: uint16(rule.HTTP.Paths[0].Backend.Service.Port.Number),
1419+
},
1420+
},
1421+
Subroutes: []conf_v1.Route{
1422+
{
1423+
Path: rule.HTTP.Paths[0].Path,
1424+
Action: &conf_v1.Action{
1425+
Pass: "challenge",
1426+
},
1427+
},
1428+
},
1429+
},
1430+
}
1431+
1432+
return vs
1433+
}
1434+
13801435
func (c *Configuration) buildMinionConfigs(masterHost string) ([]*MinionConfiguration, map[string][]string) {
13811436
var minionConfigs []*MinionConfiguration
13821437
childWarnings := make(map[string][]string)

internal/k8s/configuration_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func createTestConfiguration() *Configuration {
3838
validation.NewTransportServerValidator(isTLSPassthroughEnabled, snippetsEnabled, isPlus),
3939
isTLSPassthroughEnabled,
4040
snippetsEnabled,
41+
certManagerEnabled,
4142
)
4243
}
4344

@@ -2676,6 +2677,88 @@ func TestPortCollisions(t *testing.T) {
26762677
}
26772678
}
26782679

2680+
func TestChallengeIngressToVSR(t *testing.T) {
2681+
configuration := createTestConfiguration()
2682+
2683+
var expectedProblems []ConfigurationProblem
2684+
2685+
// Add a new Ingress
2686+
2687+
vs := createTestVirtualServer("virtualserver", "foo.example.com")
2688+
vsr1 := createTestChallengeVirtualServerRoute("challenge", "foo.example.com", "/.well-known/acme-challenge/test")
2689+
2690+
ing := createTestChallengeIngress("challenge", "foo.example.com", "/.well-known/acme-challenge/test", "cm-acme-http-solver-test")
2691+
2692+
expectedChanges := []ResourceChange{
2693+
{
2694+
Op: AddOrUpdate,
2695+
Resource: &VirtualServerConfiguration{
2696+
VirtualServer: vs,
2697+
VirtualServerRoutes: []*conf_v1.VirtualServerRoute{vsr1},
2698+
Warnings: nil,
2699+
},
2700+
},
2701+
}
2702+
2703+
configuration.AddOrUpdateVirtualServer(vs)
2704+
changes, problems := configuration.AddOrUpdateIngress(ing)
2705+
if diff := cmp.Diff(expectedChanges, changes); diff != "" {
2706+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2707+
}
2708+
if diff := cmp.Diff(expectedProblems, problems); diff != "" {
2709+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2710+
}
2711+
2712+
expectedChanges = nil
2713+
2714+
changes, problems = configuration.DeleteIngress(ing.Name)
2715+
if diff := cmp.Diff(expectedChanges, changes); diff != "" {
2716+
t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff)
2717+
}
2718+
if diff := cmp.Diff(expectedProblems, problems); diff != "" {
2719+
t.Errorf("DeleteIngress() returned unexpected result (-want +got):\n%s", diff)
2720+
}
2721+
2722+
expectedChanges = nil
2723+
ing = createTestIngress("wrong-challenge", "foo.example.com", "bar.example.com")
2724+
ing.Labels = map[string]string{"acme.cert-manager.io/http01-solver": "true"}
2725+
expectedProblems = []ConfigurationProblem{
2726+
{
2727+
Object: ing,
2728+
IsError: true,
2729+
Reason: "Rejected",
2730+
Message: "spec.rules: Forbidden: challenge Ingress must have exactly 1 rule defined",
2731+
},
2732+
}
2733+
2734+
changes, problems = configuration.AddOrUpdateIngress(ing)
2735+
if diff := cmp.Diff(expectedChanges, changes); diff != "" {
2736+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2737+
}
2738+
if diff := cmp.Diff(expectedProblems, problems); diff != "" {
2739+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2740+
}
2741+
2742+
ing = createTestIngress("wrong-challenge", "foo.example.com")
2743+
ing.Labels = map[string]string{"acme.cert-manager.io/http01-solver": "true"}
2744+
expectedProblems = []ConfigurationProblem{
2745+
{
2746+
Object: ing,
2747+
IsError: true,
2748+
Reason: "Rejected",
2749+
Message: "spec.rules.HTTP.Paths: Forbidden: challenge Ingress must have exactly 1 path defined",
2750+
},
2751+
}
2752+
2753+
changes, problems = configuration.AddOrUpdateIngress(ing)
2754+
if diff := cmp.Diff(expectedChanges, changes); diff != "" {
2755+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2756+
}
2757+
if diff := cmp.Diff(expectedProblems, problems); diff != "" {
2758+
t.Errorf("AddOrUpdateIngress() returned unexpected result (-want +got):\n%s", diff)
2759+
}
2760+
}
2761+
26792762
func mustInitGlobalConfiguration(c *Configuration, gc *conf_v1alpha1.GlobalConfiguration) {
26802763
changes, problems, err := c.AddOrUpdateGlobalConfiguration(gc)
26812764

@@ -2740,6 +2823,50 @@ func createTestIngress(name string, hosts ...string) *networking.Ingress {
27402823
}
27412824
}
27422825

2826+
func createTestChallengeIngress(name string, host string, path string, serviceName string) *networking.Ingress {
2827+
var rules []networking.IngressRule
2828+
backend := networking.IngressBackend{
2829+
Service: &networking.IngressServiceBackend{
2830+
Name: serviceName,
2831+
Port: networking.ServiceBackendPort{
2832+
Number: 8089,
2833+
},
2834+
},
2835+
}
2836+
2837+
rules = append(rules, networking.IngressRule{
2838+
Host: host,
2839+
IngressRuleValue: networking.IngressRuleValue{
2840+
HTTP: &networking.HTTPIngressRuleValue{
2841+
Paths: []networking.HTTPIngressPath{
2842+
{
2843+
Path: path,
2844+
Backend: backend,
2845+
},
2846+
},
2847+
},
2848+
},
2849+
},
2850+
)
2851+
2852+
return &networking.Ingress{
2853+
ObjectMeta: metav1.ObjectMeta{
2854+
Name: name,
2855+
Namespace: "default",
2856+
CreationTimestamp: metav1.Now(),
2857+
Annotations: map[string]string{
2858+
"kubernetes.io/ingress.class": "nginx",
2859+
},
2860+
Labels: map[string]string{
2861+
"acme.cert-manager.io/http01-solver": "true",
2862+
},
2863+
},
2864+
Spec: networking.IngressSpec{
2865+
Rules: rules,
2866+
},
2867+
}
2868+
}
2869+
27432870
func createTestVirtualServer(name string, host string) *conf_v1.VirtualServer {
27442871
return &conf_v1.VirtualServer{
27452872
ObjectMeta: metav1.ObjectMeta{
@@ -2783,6 +2910,33 @@ func createTestVirtualServerRoute(name string, host string, path string) *conf_v
27832910
}
27842911
}
27852912

2913+
func createTestChallengeVirtualServerRoute(name string, host string, path string) *conf_v1.VirtualServerRoute {
2914+
return &conf_v1.VirtualServerRoute{
2915+
ObjectMeta: metav1.ObjectMeta{
2916+
Namespace: "default",
2917+
Name: name,
2918+
},
2919+
Spec: conf_v1.VirtualServerRouteSpec{
2920+
Host: host,
2921+
Upstreams: []conf_v1.Upstream{
2922+
{
2923+
Name: "challenge",
2924+
Service: "cm-acme-http-solver-test",
2925+
Port: 8089,
2926+
},
2927+
},
2928+
Subroutes: []conf_v1.Route{
2929+
{
2930+
Path: path,
2931+
Action: &conf_v1.Action{
2932+
Pass: "challenge",
2933+
},
2934+
},
2935+
},
2936+
},
2937+
}
2938+
}
2939+
27862940
func createTestTransportServer(name string, listenerName string, listenerProtocol string) *conf_v1alpha1.TransportServer {
27872941
return &conf_v1alpha1.TransportServer{
27882942
ObjectMeta: metav1.ObjectMeta{

internal/k8s/controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,9 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc
342342
input.GlobalConfigurationValidator,
343343
input.TransportServerValidator,
344344
input.IsTLSPassthroughEnabled,
345-
input.SnippetsEnabled)
345+
input.SnippetsEnabled,
346+
input.CertManagerEnabled,
347+
)
346348

347349
lbc.appProtectConfiguration = appprotect.NewConfiguration()
348350
lbc.dosConfiguration = appprotectdos.NewConfiguration(input.AppProtectDosEnabled)

internal/k8s/utils.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func isMaster(ing *networking.Ingress) bool {
132132
return ing.Annotations["nginx.org/mergeable-ingress-type"] == "master"
133133
}
134134

135+
func isChallengeIngress(ing *networking.Ingress) bool {
136+
return ing.Labels["acme.cert-manager.io/http01-solver"] == "true"
137+
}
138+
135139
// hasChanges determines if current ingress has changes compared to old ingress
136140
func hasChanges(old *networking.Ingress, current *networking.Ingress) bool {
137141
old.Status.LoadBalancer.Ingress = current.Status.LoadBalancer.Ingress

internal/k8s/validation.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,35 @@ func validateIngress(
406406
allErrs = append(allErrs, validateMinionSpec(&ing.Spec, field.NewPath("spec"))...)
407407
}
408408

409+
if isChallengeIngress(ing) {
410+
allErrs = append(allErrs, validateChallengeIngress(&ing.Spec, field.NewPath("spec"))...)
411+
}
412+
413+
return allErrs
414+
}
415+
416+
func validateChallengeIngress(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList {
417+
allErrs := field.ErrorList{}
418+
if spec.Rules == nil || len(spec.Rules) != 1 {
419+
allErrs = append(allErrs, field.Forbidden(fieldPath.Child("rules"), "challenge Ingress must have exactly 1 rule defined"))
420+
return allErrs
421+
}
422+
r := spec.Rules[0]
423+
424+
if r.HTTP == nil || r.HTTP.Paths == nil || len(r.HTTP.Paths) != 1 {
425+
allErrs = append(allErrs, field.Forbidden(fieldPath.Child("rules.HTTP.Paths"), "challenge Ingress must have exactly 1 path defined"))
426+
return allErrs
427+
}
428+
429+
p := r.HTTP.Paths[0]
430+
431+
if p.Backend.Service == nil {
432+
allErrs = append(allErrs, field.Required(fieldPath.Child("rules.HTTP.Paths[0].Backend.Service"), "challenge Ingress must have a Backend Service defined"))
433+
}
434+
435+
if p.Backend.Service.Port.Name != "" {
436+
allErrs = append(allErrs, field.Forbidden(fieldPath.Child("rules.HTTP.Paths[0].Backend.Service.Port.Name"), "challenge Ingress must have a Backend Service Port Number defined, not Name"))
437+
}
409438
return allErrs
410439
}
411440

0 commit comments

Comments
 (0)
0