diff --git a/pkg/builder/interfaces.go b/pkg/builder/interfaces.go index 1c78ca4..4f3d11e 100644 --- a/pkg/builder/interfaces.go +++ b/pkg/builder/interfaces.go @@ -169,7 +169,6 @@ type ServiceBuilder interface { GetObject() *corev1.Service AddPort(port *corev1.ServicePort) GetPorts() []corev1.ServicePort - GetServiceType() corev1.ServiceType } type ServiceAccountBuilder interface { diff --git a/pkg/builder/options.go b/pkg/builder/options.go index a9eb862..1753217 100644 --- a/pkg/builder/options.go +++ b/pkg/builder/options.go @@ -8,6 +8,14 @@ import ( commonsv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/commons/v1alpha1" ) +type Option struct { + ClusterName string + RoleName string + RoleGroupName string + Labels map[string]string + Annotations map[string]string +} + type Options struct { ClusterName string RoleName string diff --git a/pkg/builder/service.go b/pkg/builder/service.go index 40abdbc..bb0233e 100644 --- a/pkg/builder/service.go +++ b/pkg/builder/service.go @@ -3,10 +3,12 @@ package builder import ( "context" - "github.com/zncdatadev/operator-go/pkg/client" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/zncdatadev/operator-go/pkg/client" + "github.com/zncdatadev/operator-go/pkg/constants" ) func ContainerPorts2ServicePorts(port []corev1.ContainerPort) []corev1.ServicePort { @@ -29,33 +31,56 @@ func ContainerPorts2ServicePorts(port []corev1.ContainerPort) []corev1.ServicePo return ports } +// ListenerClass2ServiceType converts listener class to k8s service type +// +// ClusterInternal --> ClusterIP +// ExternalUnstable --> NodePort +// ExternalStable --> LoadBalancer +// Default --> ClusterIP +func ListenerClass2ServiceType(listenerClass constants.ListenerClass) corev1.ServiceType { + switch listenerClass { + case constants.ClusterInternal: + return corev1.ServiceTypeClusterIP + case constants.ExternalUnstable: + return corev1.ServiceTypeNodePort + case constants.ExternalStable: + return corev1.ServiceTypeLoadBalancer + default: + return corev1.ServiceTypeClusterIP + } +} + var _ ServiceBuilder = &BaseServiceBuilder{} type BaseServiceBuilder struct { BaseResourceBuilder - // if you want to get ports, please use GetPorts() method - ports []corev1.ServicePort - - serviceType *corev1.ServiceType - - headless bool + ports []corev1.ServicePort + listenerClass constants.ListenerClass + headless bool + // Setting this parameter will override the default matching labels, generally not needed + matchingLabels map[string]string } func (b *BaseServiceBuilder) GetObject() *corev1.Service { - clusterIp := "" - if b.headless { - clusterIp = corev1.ClusterIPNone + matchingLabels := b.GetMatchingLabels() + if b.matchingLabels != nil { + matchingLabels = b.matchingLabels } - return &corev1.Service{ + obj := &corev1.Service{ ObjectMeta: b.GetObjectMeta(), Spec: corev1.ServiceSpec{ - Ports: b.GetPorts(), - Selector: b.GetMatchingLabels(), - Type: b.GetServiceType(), - ClusterIP: clusterIp, + Ports: b.GetPorts(), + Selector: matchingLabels, + Type: ListenerClass2ServiceType(b.listenerClass), }, } + + if b.headless { + obj.Spec.ClusterIP = corev1.ClusterIPNone + } + + return obj } func (b *BaseServiceBuilder) AddPort(port *corev1.ServicePort) { @@ -66,39 +91,46 @@ func (b *BaseServiceBuilder) GetPorts() []corev1.ServicePort { return b.ports } -func (b *BaseServiceBuilder) GetServiceType() corev1.ServiceType { - if b.serviceType == nil { - return corev1.ServiceTypeClusterIP - } - return *b.serviceType -} - func (b *BaseServiceBuilder) Build(_ context.Context) (ctrlclient.Object, error) { obj := b.GetObject() return obj, nil } +type ServiceBuilderOption struct { + Option + + // If not set, ClusterIP will be used + ListenerClass constants.ListenerClass + Headless bool + MatchingLabels map[string]string +} + +type ServiceBuilderOptions func(*ServiceBuilderOption) + func NewServiceBuilder( client *client.Client, name string, - labels map[string]string, - annotations map[string]string, ports []corev1.ContainerPort, - serviceType *corev1.ServiceType, - headless bool, + options ...ServiceBuilderOptions, ) *BaseServiceBuilder { - servicePorts := ContainerPorts2ServicePorts(ports) + opt := &ServiceBuilderOption{} + + for _, o := range options { + o(opt) + } return &BaseServiceBuilder{ BaseResourceBuilder: BaseResourceBuilder{ - Client: client, - Name: name, - labels: labels, + Client: client, + Name: name, + labels: opt.Labels, + annotations: opt.Annotations, }, - ports: servicePorts, + ports: ContainerPorts2ServicePorts(ports), - serviceType: serviceType, - headless: headless, + headless: opt.Headless, + matchingLabels: opt.MatchingLabels, + listenerClass: opt.ListenerClass, } } diff --git a/pkg/builder/service_test.go b/pkg/builder/service_test.go new file mode 100644 index 0000000..744ec9e --- /dev/null +++ b/pkg/builder/service_test.go @@ -0,0 +1,72 @@ +package builder_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/zncdatadev/operator-go/pkg/builder" + "github.com/zncdatadev/operator-go/pkg/client" + "github.com/zncdatadev/operator-go/pkg/constants" +) + +func TestNewServiceBuilder(t *testing.T) { + fakeOwner := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-owner", + Namespace: "default", + UID: types.UID("fake-uid"), + }, + } + + mockClient := client.NewClient(k8sClient, fakeOwner) + name := "test-service" + ports := []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 80, + Protocol: corev1.ProtocolTCP, + }, + } + + t.Run("default options", func(t *testing.T) { + builder := builder.NewServiceBuilder(mockClient, name, ports) + service := builder.GetObject() + + assert.Equal(t, name, service.Name) + assert.Equal(t, corev1.ServiceTypeClusterIP, service.Spec.Type) + assert.Equal(t, 1, len(service.Spec.Ports)) + assert.Equal(t, "http", service.Spec.Ports[0].Name) + assert.Equal(t, int32(80), service.Spec.Ports[0].Port) + assert.Equal(t, corev1.ProtocolTCP, service.Spec.Ports[0].Protocol) + + obj, err := builder.Build(context.Background()) + assert.Nil(t, err) + assert.NotNil(t, obj) + }) + + t.Run("with options", func(t *testing.T) { + options := []builder.ServiceBuilderOptions{ + func(opt *builder.ServiceBuilderOption) { + opt.ListenerClass = constants.ExternalStable + opt.Headless = true + opt.MatchingLabels = map[string]string{"app": "test"} + }, + } + builder := builder.NewServiceBuilder(mockClient, name, ports, options...) + service := builder.GetObject() + + assert.Equal(t, name, service.Name) + assert.Equal(t, corev1.ServiceTypeLoadBalancer, service.Spec.Type) + assert.Equal(t, corev1.ClusterIPNone, service.Spec.ClusterIP) + assert.Equal(t, map[string]string{"app": "test"}, service.Spec.Selector) + + obj, err := builder.Build(context.Background()) + assert.Nil(t, err) + assert.NotNil(t, obj) + }) +} diff --git a/pkg/constants/listener.go b/pkg/constants/listener.go index d27e3b1..00bde10 100644 --- a/pkg/constants/listener.go +++ b/pkg/constants/listener.go @@ -26,3 +26,16 @@ const ( // If not set, the listener name will be the same as the pod name. AnnotationListenerName string = listenerAPIGroupPrefix + "listenerName" ) + +type ListenerClass string + +const ( + // ClusterInternal is the default listener class. + // cluster-internal --> k8s service with ClusterIP + ClusterInternal ListenerClass = "cluster-internal" + // external-unstable --> k8s service with NodePort + ExternalUnstable ListenerClass = "external-unstable" + // ExternalStable requires a k8s LoadBalancer + // external-stable --> k8s service with LoadBalancer + ExternalStable ListenerClass = "external-stable" +) diff --git a/pkg/reconciler/cluster_test.go b/pkg/reconciler/cluster_test.go index 9c95850..25e821a 100644 --- a/pkg/reconciler/cluster_test.go +++ b/pkg/reconciler/cluster_test.go @@ -8,14 +8,16 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - commonsv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/commons/v1alpha1" - "github.com/zncdatadev/operator-go/pkg/client" - "github.com/zncdatadev/operator-go/pkg/constants" - "github.com/zncdatadev/operator-go/pkg/reconciler" appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + + commonsv1alpha1 "github.com/zncdatadev/operator-go/pkg/apis/commons/v1alpha1" + "github.com/zncdatadev/operator-go/pkg/builder" + "github.com/zncdatadev/operator-go/pkg/client" + "github.com/zncdatadev/operator-go/pkg/constants" + "github.com/zncdatadev/operator-go/pkg/reconciler" ) // ClusterReconciler reconciles a TrinoCluster object @@ -52,16 +54,16 @@ func (r *ClusterReconciler) RegisterResources(ctx context.Context) error { serviceReconciler := reconciler.NewServiceReconciler( r.GetClient(), r.GetName(), - r.ClusterInfo.GetLabels(), - r.ClusterInfo.GetAnnotations(), []corev1.ContainerPort{ { Name: "http", ContainerPort: 3000, }, }, - nil, - false, + func(sbo *builder.ServiceBuilderOption) { + sbo.Annotations = r.ClusterInfo.GetAnnotations() + sbo.Labels = r.ClusterInfo.GetLabels() + }, ) // Register resources r.AddResource(serviceReconciler) diff --git a/pkg/reconciler/role_test.go b/pkg/reconciler/role_test.go index a122bb7..8a385d6 100644 --- a/pkg/reconciler/role_test.go +++ b/pkg/reconciler/role_test.go @@ -143,16 +143,16 @@ func (r *RoleReconciler) getServiceReconciler(info reconciler.RoleGroupInfo) rec return reconciler.NewServiceReconciler( r.GetClient(), info.GetFullName(), - info.GetLabels(), - info.GetAnnotations(), []corev1.ContainerPort{ { Name: "http", ContainerPort: 3000, }, }, - nil, - false, + func(sbo *builder.ServiceBuilderOption) { + sbo.Annotations = info.GetAnnotations() + sbo.Labels = info.GetLabels() + }, ) } diff --git a/pkg/reconciler/service.go b/pkg/reconciler/service.go index f82e915..b8d5c88 100644 --- a/pkg/reconciler/service.go +++ b/pkg/reconciler/service.go @@ -1,9 +1,10 @@ package reconciler import ( + corev1 "k8s.io/api/core/v1" + "github.com/zncdatadev/operator-go/pkg/builder" "github.com/zncdatadev/operator-go/pkg/client" - corev1 "k8s.io/api/core/v1" ) var _ ResourceReconciler[builder.ServiceBuilder] = &Service{} @@ -15,20 +16,14 @@ type Service struct { func NewServiceReconciler( client *client.Client, name string, - labels map[string]string, - annotations map[string]string, ports []corev1.ContainerPort, - serviceType *corev1.ServiceType, - headless bool, + options ...builder.ServiceBuilderOptions, ) *Service { svcBuilder := builder.NewServiceBuilder( client, name, - labels, - annotations, ports, - serviceType, - headless, + options..., ) return &Service{ GenericResourceReconciler: *NewGenericResourceReconciler[builder.ServiceBuilder]( diff --git a/pkg/reconciler/service_test.go b/pkg/reconciler/service_test.go index bc7df67..9cb7476 100644 --- a/pkg/reconciler/service_test.go +++ b/pkg/reconciler/service_test.go @@ -7,11 +7,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/zncdatadev/operator-go/pkg/client" - "github.com/zncdatadev/operator-go/pkg/reconciler" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + + "github.com/zncdatadev/operator-go/pkg/builder" + "github.com/zncdatadev/operator-go/pkg/client" + "github.com/zncdatadev/operator-go/pkg/reconciler" ) var _ = Describe("Service reconciler", func() { @@ -53,8 +55,6 @@ var _ = Describe("Service reconciler", func() { serviceReconciler := reconciler.NewServiceReconciler( resourceClient, name, - map[string]string{"app.kubernetes.io/name": name}, - map[string]string{"app.kubernetes.io/name": name}, []corev1.ContainerPort{ { Name: "http", @@ -62,8 +62,11 @@ var _ = Describe("Service reconciler", func() { Protocol: corev1.ProtocolTCP, }, }, - nil, - false, + func(sbo *builder.ServiceBuilderOption) { + sbo.Annotations = map[string]string{"app.kubernetes.io/name": name} + sbo.Labels = map[string]string{"app.kubernetes.io/name": name} + + }, ) Expect(serviceReconciler).ShouldNot(BeNil())