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 nodePort and loadbalancerIPAddress as optional fields in WG gateway template #2799

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ spec:
selector:
{{- include "liqo.labelsTemplate" (merge (dict "isService" true) $templateConfig) | nindent 12 }}
type: "{{"{{ .Spec.Endpoint.ServiceType }}"}}"
?loadBalancerIP: "{{"{{ .Spec.Endpoint.LoadBalancerIP }}"}}"
ports:
- port: "{{"{{ .Spec.Endpoint.Port }}"}}"
protocol: UDP
targetPort: "{{"{{ .Spec.Endpoint.Port }}"}}"
?nodePort: "{{"{{ .Spec.Endpoint.NodePort }}"}}"
{{- if .Values.networking.gatewayTemplates.server.service.allocateLoadBalancerNodePorts }}
allocateLoadBalancerNodePorts: {{ .Values.networking.gatewayTemplates.server.service.allocateLoadBalancerNodePorts }}
{{- end }}
Expand Down Expand Up @@ -81,8 +83,8 @@ spec:
{{- if .Values.requirements.kernel.disabled }}
- --disable-kernel-version-check
{{- end }}
volumeMounts:
- name: ipc
volumeMounts:
- name: ipc
mountPath: /ipc
{{- if .Values.metrics.enabled }}
ports:
Expand Down Expand Up @@ -135,7 +137,7 @@ spec:
privileged: true
{{ end }}
volumeMounts:
- name: ipc
- name: ipc
mountPath: /ipc
- name: wireguard-config
mountPath: /etc/wireguard/keys
Expand All @@ -156,8 +158,8 @@ spec:
- --metrics-address=:8084
{{- end }}
- --health-probe-bind-address=:8085
volumeMounts:
- name: ipc
volumeMounts:
- name: ipc
mountPath: /ipc
{{- if .Values.metrics.enabled }}
ports:
Expand All @@ -184,6 +186,6 @@ spec:
- name: wireguard-config
secret:
secretName: "{{"{{ .SecretName }}"}}"
- name: ipc
emptyDir: {}
- name: ipc
emptyDir: {}
{{- end }}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ package utils

import (
"bytes"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"text/template"
)

var variableRegex = regexp.MustCompile(`{{\s*(.\S+)\s*}}`)

type renderOptions struct {
skipIfEmpty bool
}

// RenderTemplate renders a template.
func RenderTemplate(obj, data interface{}, forceString bool) (interface{}, error) {
// if the object is a string, render the template
Expand Down Expand Up @@ -48,12 +57,16 @@ func RenderTemplate(obj, data interface{}, forceString bool) (interface{}, error
// if the object is a map, render the template for each value
if reflect.TypeOf(obj).Kind() == reflect.Map {
for k, v := range obj.(map[string]interface{}) {
res, err := RenderTemplate(v, data, forceString || isLabelsOrAnnotations(obj))
useKey, useValue, options := preProcessOptional(k, v, obj)

res, err := RenderTemplate(useValue, data, forceString || isLabelsOrAnnotations(obj))
if err != nil {
return obj, err
}

obj.(map[string]interface{})[k] = res
if !(reflect.ValueOf(res).IsZero() && options.skipIfEmpty) {
obj.(map[string]interface{})[useKey] = res
}
}

return obj, nil
Expand All @@ -73,6 +86,10 @@ func RenderTemplate(obj, data interface{}, forceString bool) (interface{}, error
return obj, nil
}

if forceString {
return fmt.Sprintf("%v", obj), nil
}

return obj, nil
}

Expand All @@ -87,3 +104,33 @@ func isLabelsOrAnnotations(obj interface{}) bool {

return false
}

// getVariableFromValue given a field value returns the first matched gotmpl variable.
func getVariableFromValue(value string) (string, bool) {
// Look for variables in the value
matches := variableRegex.FindStringSubmatch(value)

// If a variable is found, than get only the first one
if len(matches) > 1 {
return matches[1], true
}

return "", false
}

// preProcessOptional preprocesses the template so that a field is rendered only if it has been provided.
func preProcessOptional(key string, value, obj interface{}) (newKey string, newValue interface{}, options renderOptions) {
newKey = key
newValue = value
if strings.HasPrefix(key, "?") && reflect.TypeOf(key).Kind() == reflect.String {
if variable, match := getVariableFromValue(value.(string)); match {
newKey = key[1:]
newValue = fmt.Sprintf("{{if %s}}%s{{end}}", variable, value)
options.skipIfEmpty = true
// Delete the field with the condition option
delete(obj.(map[string]interface{}), key)
}
}

return
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright 2019-2024 The Liqo 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.
//

package utils_test

import (
"fmt"
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/liqotech/liqo/pkg/liqo-controller-manager/networking/external-network/utils"
)

type sampleDataStructure struct {
Number int
Value string
OptionalString *string
OptionalNumber *int
}

type nestedSampleDataStructure struct {
Number int
Value string
OptionalString *string
OptionalNumber *int
Nested sampleDataStructure
}

type testDataStructure struct {
Name string
Namespace string
Spec nestedSampleDataStructure
}

type templateTestCase struct {
template any
expectedRes any
data testDataStructure
}

var optionalNumber = 10
var optionalString = "Mr. Jack!"
var nestedOptionalString = "optionalVal"

var _ = DescribeTable("Templating tests", func(testCase templateTestCase) {
res, err := utils.RenderTemplate(testCase.template, testCase.data, false)

Expect(err).NotTo(HaveOccurred())
Expect(testCase.expectedRes).To(Equal(res), "Unexpected result returned")
},
Entry("Simple case", templateTestCase{
template: map[string]any{
"Name": "{{ .Name }}",
"Namespace": "{{ .Namespace }}",
},
expectedRes: map[string]any{
"Name": "hello",
"Namespace": "world!",
},
data: testDataStructure{
Name: "hello",
Namespace: "world!",
},
}),
Entry("Labels and annotations should force string", templateTestCase{
template: map[string]any{
"labels": map[string]any{
"hello": "{{ .Namespace }}",
"test": 4,
},
"annotations": map[string]any{
"hello": "{{ .Name }}",
"test": 5,
},
},
expectedRes: map[string]any{
"labels": map[string]any{
"hello": "world!",
"test": "4",
},
"annotations": map[string]any{
"hello": "hello",
"test": "5",
},
},
data: testDataStructure{
Name: "hello",
Namespace: "world!",
},
}),
Entry("Nested variables", templateTestCase{
template: map[string]any{
"Name": "{{ .Name }}",
"Nested": map[string]any{
"NestedVal": map[string]any{
"Value": "{{ .Spec.Nested.Value }}",
"Number": "{{ .Spec.Nested.Number }}",
},
"ListVal": []any{
map[string]any{
"Another": "{{ .Spec.Value }}",
},
map[string]any{
"Number": "{{ .Spec.Number }}",
},
},
},
},
expectedRes: map[string]any{
"Name": "hello",
"Nested": map[string]any{
"NestedVal": map[string]any{
"Value": "world!",
"Number": 1924,
},
"ListVal": []any{
map[string]any{
"Another": "value",
},
map[string]any{
"Number": 10,
},
},
},
},
data: testDataStructure{
Name: "hello",
Spec: nestedSampleDataStructure{
Value: "value",
Number: 10,
Nested: sampleDataStructure{
Value: "world!",
Number: 1924,
},
},
},
}),
Entry("Optional fields", templateTestCase{
template: map[string]any{
"Name": "{{ .Name }}",
"?NotOptional": "This should be kept as is",
"Nested": map[string]any{
"NestedVal": map[string]any{
"Value": "{{ .Spec.Nested.Value }}",
"?Optional": "Some text plus variable {{ .Spec.Nested.OptionalString }}",
"?OptionalNumber": "{{ .Spec.Nested.OptionalNumber }}",
},
"ListVal": []any{
map[string]any{
"Another": "{{ .Spec.Value }}",
"?Hey": "{{ .Spec.OptionalString }}",
},
map[string]any{
"number": "{{ .Spec.Number }}",
"?anotherNumber": "{{ .Spec.OptionalNumber }}",
"?optionalDifferentThanPointer": "{{ .Spec.Value }}",
},
},
},
},
expectedRes: map[string]any{
"Name": "hello",
"?NotOptional": "This should be kept as is",
"Nested": map[string]any{
"NestedVal": map[string]any{
"Value": "world!",
"Optional": fmt.Sprintf("Some text plus variable %s", nestedOptionalString),
"OptionalNumber": optionalNumber,
},
"ListVal": []any{
map[string]any{
"Another": "value",
"Hey": optionalString,
},
map[string]any{
"number": 42,
"optionalDifferentThanPointer": "value",
},
},
},
},
data: testDataStructure{
Name: "hello",
Spec: nestedSampleDataStructure{
Value: "value",
Number: 42,
OptionalString: &optionalString,
Nested: sampleDataStructure{
Value: "world!",
Number: 1924,
OptionalNumber: &optionalNumber,
OptionalString: &nestedOptionalString,
},
},
},
}),
)

func TestLocal(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "External network utils test suite")
}
18 changes: 12 additions & 6 deletions pkg/liqoctl/network/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,11 @@ func (c *Cluster) checkTemplateServerServiceNodePort(template *unstructured.Unst

_, err = maps.GetNestedField(port, "nodePort")
if err != nil {
return fmt.Errorf("unable to get spec.template.spec.service.spec.ports[0].nodePort int the server template, " +
"since you specified the flag \"--server-service-nodeport\" you need to add the \"nodePort\" field in the template")
// If the field is missing, it might be optional (represented by leading ?). If not, raise an error
if _, errOptional := maps.GetNestedField(port, "?nodePort"); errOptional != nil {
return fmt.Errorf("unable to get spec.template.spec.service.spec.ports[0].nodePort int the server template, " +
"since you specified the flag \"--server-service-nodeport\" you need to add the \"nodePort\" field in the template")
}
}

return nil
Expand All @@ -312,11 +315,14 @@ func (c *Cluster) checkTemplateServerServiceLoadBalancer(template *unstructured.
return nil
}

path := "spec.template.spec.service.spec.loadBalancerIP"
_, err := maps.GetNestedField(template.Object, path)
servicePath := "spec.template.spec.service.spec"
_, err := maps.GetNestedField(template.Object, fmt.Sprintf("%s.loadBalancerIP", servicePath))
if err != nil {
return fmt.Errorf("unable to get %s of the server template, "+
"since you specified the flag \"--server-service-loadbalancerip\" you need to add the \"loadBalancerIP\" field in the template", path)
// If the field is missing, it might be optional (represented by leading ?). If not, raise an error
if _, errOptional := maps.GetNestedField(template.Object, fmt.Sprintf("%s.?loadBalancerIP", servicePath)); errOptional != nil {
return fmt.Errorf("unable to get %s of the server template, "+
"since you specified the flag \"--server-service-loadbalancerip\" you need to add the \"loadBalancerIP\" field in the template", servicePath)
}
}
return nil
}
Expand Down