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 ability to configure network protocol and load balancer class for external service configs #335

Merged
merged 10 commits into from
Dec 14, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Added

- [PR #335](https://github.com/konpyutaika/nifikop/pull/335) - **[Operator/NifiCluster]** Added ability to set port protocol and load balancer class for external services via `ExternalServiceConfig`.
- [PR #333](https://github.com/konpyutaika/nifikop/pull/333) - **[Operator]** Replace Update by Patch on K8S resource to avoid update race conditions.

### Changed
Expand Down
15 changes: 15 additions & 0 deletions api/v1/nificluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,18 @@ type ExternalServiceSpec struct {
// and requires Type to be ExternalName.
// +optional
ExternalName string `json:"externalName,omitempty" protobuf:"bytes,10,opt,name=externalName"`
// loadBalancerClass is the class of the load balancer implementation this Service belongs to.
// If specified, the value of this field must be a label-style identifier, with an optional prefix,
// e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users.
// This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load
// balancer implementation is used, today this is typically done through the cloud provider integration,
// but should apply for any default implementation. If set, it is assumed that a load balancer
// implementation is watching for Services with a matching class. Any default load balancer
// implementation (e.g. cloud providers) should ignore Services that set this field.
// This field can only be set when creating or updating a Service to type 'LoadBalancer'.
// Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type.
// +optional
LoadBalancerClass *string `json:"loadBalancerClass,omitempty" protobuf:"bytes,21,opt,name=loadBalancerClass"`
}

type PortConfig struct {
Expand All @@ -531,6 +543,9 @@ type PortConfig struct {
// The port that will expose this service externally. (Only if the service is of type NodePort)
// +optional
NodePort *int32 `json:"nodePort,omitempty"`
// The network protocol for this port. Options defined here: https://pkg.go.dev/k8s.io/api/core/v1#Protocol
// +kubebuilder:validation:Enum={"TCP", "UDP", "SCTP"}
Protocol corev1.Protocol `json:"protocol,omitempty"`
}

// LdapConfiguration specifies the configuration if you want to use LDAP
Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/v1alpha1/nificluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ func convertPortConfigs(src []PortConfig) []v1.PortConfig {
dstPortConfigs = append(dstPortConfigs, v1.PortConfig{
Port: srcPortConfig.Port,
InternalListenerName: srcPortConfig.InternalListenerName,
Protocol: corev1.ProtocolTCP,
})
}

Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/nificluster_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func externalServicesEqual(es1 []ExternalServiceConfig, es2 []v1.ExternalService
}
for j, pc := range es.Spec.PortConfigs {
if pc.InternalListenerName != es2[i].Spec.PortConfigs[j].InternalListenerName ||
pc.Port != es2[i].Spec.PortConfigs[j].Port {
pc.Port != es2[i].Spec.PortConfigs[j].Port || es2[i].Spec.PortConfigs[j].Protocol != corev1.ProtocolTCP {
return false
}
}
Expand Down
9 changes: 9 additions & 0 deletions config/crd/bases/nifi.konpyutaika.com_nificlusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ spec:
type: array
externalName:
type: string
loadBalancerClass:
type: string
loadBalancerIP:
type: string
loadBalancerSourceRanges:
Expand All @@ -87,6 +89,13 @@ spec:
port:
format: int32
type: integer
protocol:
default: TCP
enum:
- TCP
- UDP
- SCTP
type: string
required:
- internalListenerName
- port
Expand Down
9 changes: 9 additions & 0 deletions helm/nifikop/crds/nifi.konpyutaika.com_nificlusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ spec:
type: array
externalName:
type: string
loadBalancerClass:
type: string
loadBalancerIP:
type: string
loadBalancerSourceRanges:
Expand All @@ -87,6 +89,13 @@ spec:
port:
format: int32
type: integer
protocol:
default: TCP
enum:
- TCP
- UDP
- SCTP
type: string
required:
- internalListenerName
- port
Expand Down
5 changes: 3 additions & 2 deletions pkg/resources/nifi/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (r *Reconciler) externalServices(log zap.Logger) []runtimeClient.Object {
LoadBalancerIP: eService.Spec.LoadBalancerIP,
LoadBalancerSourceRanges: eService.Spec.LoadBalancerSourceRanges,
ExternalName: eService.Spec.ExternalName,
LoadBalancerClass: eService.Spec.LoadBalancerClass,
},
})
}
Expand All @@ -86,7 +87,7 @@ func generateServicePortForInternalListeners(listeners []v1.InternalListenerConf
Name: strings.ReplaceAll(iListeners.Name, "_", ""),
Port: iListeners.ContainerPort,
TargetPort: intstr.FromInt(int(iListeners.ContainerPort)),
Protocol: corev1.ProtocolTCP,
Protocol: iListeners.Protocol,
})
}

Expand All @@ -103,7 +104,7 @@ func (r *Reconciler) generateServicePortForExternalListeners(eService v1.Externa
Name: strings.ReplaceAll(iListener.Name, "_", ""),
Port: port.Port,
TargetPort: intstr.FromInt(int(iListener.ContainerPort)),
Protocol: corev1.ProtocolTCP,
Protocol: port.Protocol,
}

if eService.Spec.Type == corev1.ServiceTypeNodePort && port.NodePort != nil {
Expand Down
112 changes: 112 additions & 0 deletions pkg/resources/nifi/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package nifi

import (
"testing"

v1 "github.com/konpyutaika/nifikop/api/v1"
"github.com/konpyutaika/nifikop/pkg/resources"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestGenerateServicePortForExternalListeners(t *testing.T) {
r := resources.Reconciler{
NifiCluster: &v1.NifiCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
Namespace: "namespace",
},
Spec: v1.NifiClusterSpec{
ListenersConfig: &v1.ListenersConfig{
InternalListeners: []v1.InternalListenerConfig{
{
Name: "foo-listener",
Protocol: corev1.ProtocolTCP,
},
},
},
},
},
}
rec := Reconciler{
Reconciler: r,
}

lbClass := "foo-lb-class"
esconfig := v1.ExternalServiceConfig{
Name: "foo",
Spec: v1.ExternalServiceSpec{
LoadBalancerClass: &lbClass,
PortConfigs: []v1.PortConfig{
{
Port: 5,
InternalListenerName: "foo-listener",
NodePort: new(int32),
Protocol: corev1.ProtocolTCP,
},
},
},
}

servicePorts := rec.generateServicePortForExternalListeners(esconfig)

assert.NotEmpty(t, servicePorts, "servicePorts should not be empty")
assert.Equal(t, 1, len(servicePorts), "there should only be 1 service port")
assert.Equal(t, esconfig.Spec.PortConfigs[0].Protocol, servicePorts[0].Protocol, "service port protocol should be TCP")
assert.Equal(t, esconfig.Spec.PortConfigs[0].InternalListenerName, servicePorts[0].Name, "service port name should be same as listener")
assert.Equal(t, esconfig.Spec.PortConfigs[0].Port, servicePorts[0].Port, "service port has incorrect port")
}

func TestExternalServices(t *testing.T) {
lbClass := "foo-lb-class"
esconfig := v1.ExternalServiceConfig{
Name: "foo",
Spec: v1.ExternalServiceSpec{
LoadBalancerClass: &lbClass,
PortConfigs: []v1.PortConfig{
{
Port: 5,
InternalListenerName: "foo-listener",
NodePort: new(int32),
Protocol: corev1.ProtocolTCP,
},
},
},
}
r := resources.Reconciler{
NifiCluster: &v1.NifiCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
Namespace: "namespace",
},
Spec: v1.NifiClusterSpec{
ListenersConfig: &v1.ListenersConfig{
InternalListeners: []v1.InternalListenerConfig{
{
Name: "foo-listener",
Protocol: corev1.ProtocolTCP,
},
},
},
ExternalServices: []v1.ExternalServiceConfig{
esconfig,
},
},
},
}

rec := Reconciler{
Reconciler: r,
}

externalServices := rec.externalServices(*zap.NewNop())

assert.NotEmpty(t, externalServices, "external services should not be empty")
assert.Equal(t, 1, len(externalServices), "there should only be 1 external service")
assert.Equal(t, esconfig.Spec.LoadBalancerClass, externalServices[0].(*corev1.Service).Spec.LoadBalancerClass, "service load balancer class should be same as external service")
assert.Equal(t, esconfig.Spec.PortConfigs[0].InternalListenerName, externalServices[0].(*corev1.Service).Spec.Ports[0].Name, "service port name should be same as external service")
assert.Equal(t, esconfig.Spec.PortConfigs[0].Protocol, externalServices[0].(*corev1.Service).Spec.Ports[0].Protocol, "service protocol should be same as external service")
assert.Equal(t, esconfig.Spec.PortConfigs[0].Port, externalServices[0].(*corev1.Service).Spec.Ports[0].Port, "service port name should be same as external service")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: External Service Config
sidebar_label: External Service Config
---

ListenersConfig defines the Nifi listener types :
ListenersConfig defines the Nifi listener types:

```yaml
externalServices:
Expand All @@ -14,6 +14,30 @@ ListenersConfig defines the Nifi listener types :
portConfigs:
- port: 8080
internalListenerName: "http"
- port: 7182
internalListenerName: "my-custom-listener"
protocol: TCP
metadata:
annotations:
toto: tata
labels:
titi: tutu
```

Load balancer example:

```yaml
externalServices:
- name: "nlb"
spec:
type: LoadBalancer
loadBalancerClass: "service.k8s.aws/nlb"
portConfigs:
- port: 8080
internalListenerName: "http"
- port: 7890
internalListenerName: "my-custom-udp-listener"
protocol: UDP
metadata:
annotations:
toto: tata
Expand All @@ -40,6 +64,7 @@ Field|Type|Description|Required|Default|
|loadBalancerIP|string| Only applies to Service Type: LoadBalancer. LoadBalancer will get created with the IP specified in this field. | No | - |
|loadBalancerSourceRanges|\[ \]string| If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer will be restricted to the specified client IPs | No | - |
|externalName|string| externalName is the external reference that kubedns or equivalent will return as a CNAME record for this service. No proxying will be involved. | No | - |
|loadBalancerClass|string| loadBalancerClass is the class of the load balancer implementation this Service belongs to. | No | - |

## PortConfig

Expand All @@ -48,6 +73,7 @@ Field|Type|Description|Required|Default|
|port|int32| The port that will be exposed by this service. | Yes | - |
|internalListenerName|string| The name of the listener which will be used as target container. | Yes | - |
|nodePort|int32| The port that will expose this service externally. (Only if the service is of type NodePort) | No | - |
|protocol|[Protocol](https://pkg.go.dev/k8s.io/api/core/v1#Protocol)| the network protocol for this service port. Must be one of the protocol enum values (i.e. TCP, UDP, SCTP). | No | `TCP` |

## Metadata

Expand Down