Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/operator/api/v1alpha3/otterize_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ func ServiceIdentityToLabelsForWorkloadSelection(ctx context.Context, k8sClient
return nil, false, errors.Wrap(err)
}
if svc.Spec.Selector == nil {
// ExternalName services don't have selectors as they redirect to external DNS names.
// These services don't select any pods, so we return gracefully without error.
if svc.Spec.Type == v1.ServiceTypeExternalName {
return nil, false, nil
}
return nil, false, errors.Errorf("%w %s/%s", ServiceHasNoSelector, svc.Namespace, svc.Name)
}
return maps.Clone(svc.Spec.Selector), true, nil
Expand Down
5 changes: 5 additions & 0 deletions src/operator/api/v1beta1/otterize_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ func ServiceIdentityToLabelsForWorkloadSelection(ctx context.Context, k8sClient
return nil, false, errors.Wrap(err)
}
if svc.Spec.Selector == nil {
// ExternalName services don't have selectors as they redirect to external DNS names.
// These services don't select any pods, so we return gracefully without error.
if svc.Spec.Type == v1.ServiceTypeExternalName {
return nil, false, nil
}
return nil, false, errors.Errorf("service %s/%s has no selector", svc.Namespace, svc.Name)
}
return maps.Clone(svc.Spec.Selector), true, nil
Expand Down
5 changes: 5 additions & 0 deletions src/operator/api/v2alpha1/otterize_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ func ServiceIdentityToLabelsForWorkloadSelection(ctx context.Context, k8sClient
return nil, false, errors.Wrap(err)
}
if svc.Spec.Selector == nil {
// ExternalName services don't have selectors as they redirect to external DNS names.
// These services don't select any pods, so we return gracefully without error.
if svc.Spec.Type == v1.ServiceTypeExternalName {
return nil, false, nil
}
return nil, false, fmt.Errorf("service %s/%s has no selector", svc.Namespace, svc.Name)
}
return maps.Clone(svc.Spec.Selector), true, nil
Expand Down
5 changes: 5 additions & 0 deletions src/operator/api/v2beta1/otterize_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ func ServiceIdentityToLabelsForWorkloadSelection(ctx context.Context, k8sClient
return nil, false, errors.Wrap(err)
}
if svc.Spec.Selector == nil {
// ExternalName services don't have selectors as they redirect to external DNS names.
// These services don't select any pods, so we return gracefully without error.
if svc.Spec.Type == v1.ServiceTypeExternalName {
return nil, false, nil
}
return nil, false, fmt.Errorf("service %s/%s has no selector", svc.Namespace, svc.Name)
}
return maps.Clone(svc.Spec.Selector), true, nil
Expand Down
31 changes: 31 additions & 0 deletions src/shared/serviceidresolver/podownerresolver/podownerresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package podownerresolver
import (
"context"
"flag"
"fmt"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
Expand Down Expand Up @@ -112,6 +113,36 @@ func resolvePodToServiceIdentity(ctx context.Context, k8sClient client.Client, p
otterizeServiceName := strings.ReplaceAll(resourceName, ".", "_")

ownerKind := ownerObj.GetObjectKind().GroupVersionKind().Kind
ownerApiVersion := ownerObj.GetObjectKind().GroupVersionKind().Group
// Defensively extract the first part of the group if it contains a version separator
if strings.Contains(ownerApiVersion, "/") {
ownerApiVersion = strings.Split(ownerApiVersion, "/")[0]
}

// Apply special service name mappings for specific workload types.
// These standardized names provide consistent identity across different instances of these workload types.
switch ownerKind {
case "Execution":
// Canalflow execution pods get a standardized name for consistent policy application
// across different execution instances
otterizeServiceName = "execution"
case "Workflow":
// Argo Workflow pods get a standardized name for consistent policy application
// across different workflow instances
otterizeServiceName = "argo-workflow"
case "SparkApplication":
// Spark application pods (driver and executors) get a standardized name
// to group them under a common service identity
otterizeServiceName = "spark"
case "RunnerDeployment":
// GitHub Actions runner pods get a standardized name for consistent
// policy management across runner instances
otterizeServiceName = "actions-runner"
case "Service":
// Service owners need API group qualification to distinguish them from core Services
// and prevent naming conflicts with other resource types
ownerKind = fmt.Sprintf("%s.%s", ownerKind, ownerApiVersion)
}
return serviceidentity.ServiceIdentity{Name: otterizeServiceName, Namespace: pod.Namespace, OwnerObject: ownerObj, Kind: ownerKind, ResolvedUsingOverrideAnnotation: lo.ToPtr(false)}, nil
}

Expand Down
226 changes: 226 additions & 0 deletions src/shared/serviceidresolver/serviceidresolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,232 @@ func (s *ServiceIdResolverTestSuite) TestServiceIdentityToPodLabelsForWorkloadSe
s.Require().Len(pods, 0)
}

func (s *ServiceIdResolverTestSuite) TestServiceIdentityToPodLabelsForWorkloadSelection_ServiceKind_ExternalNameService() {
serviceName := "external-service"
namespace := "cool-namespace"
service := serviceidentity.ServiceIdentity{Name: serviceName, Namespace: namespace, Kind: serviceidentity.KindService}

// ExternalName services have no selector and should be handled gracefully
serviceObj := corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeExternalName,
ExternalName: "external.example.com",
Selector: nil, // ExternalName services have no selector
},
}
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: serviceName, Namespace: namespace}, &corev1.Service{}).Do(func(_ context.Context, name types.NamespacedName, svc *corev1.Service, _ ...any) {
serviceObj.DeepCopyInto(svc)
})

// Should return no pods with ok=false and no error for ExternalName services
pods, ok, err := s.Resolver.ResolveServiceIdentityToPodSlice(context.Background(), service)
s.Require().NoError(err)
s.Require().False(ok)
s.Require().Len(pods, 0)
}

func (s *ServiceIdResolverTestSuite) TestResolvePodToServiceIdentity_ExecutionOwner() {
podName := "execution-pod-12345"
podNamespace := "canalflow-namespace"
executionName := "my-execution"

myPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
OwnerReferences: []metav1.OwnerReference{
{
Kind: "Execution",
Name: executionName,
APIVersion: "canalflow.io/v1alpha1",
},
},
},
}

executionAsObject := unstructured.Unstructured{}
executionAsObject.SetName(executionName)
executionAsObject.SetNamespace(podNamespace)
executionAsObject.SetKind("Execution")
executionAsObject.SetAPIVersion("canalflow.io/v1alpha1")

emptyObject := &unstructured.Unstructured{}
emptyObject.SetKind("Execution")
emptyObject.SetAPIVersion("canalflow.io/v1alpha1")
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: executionName, Namespace: podNamespace}, emptyObject).Do(
func(_ context.Context, _ types.NamespacedName, obj *unstructured.Unstructured, _ ...any) error {
executionAsObject.DeepCopyInto(obj)
return nil
})

service, err := s.Resolver.ResolvePodToServiceIdentity(context.Background(), &myPod)
s.Require().NoError(err)
s.Require().Equal("execution", service.Name)
s.Require().Equal("Execution", service.Kind)
}

func (s *ServiceIdResolverTestSuite) TestResolvePodToServiceIdentity_WorkflowOwner() {
podName := "workflow-pod-12345"
podNamespace := "argo-namespace"
workflowName := "my-workflow"

myPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
OwnerReferences: []metav1.OwnerReference{
{
Kind: "Workflow",
Name: workflowName,
APIVersion: "argoproj.io/v1alpha1",
},
},
},
}

workflowAsObject := unstructured.Unstructured{}
workflowAsObject.SetName(workflowName)
workflowAsObject.SetNamespace(podNamespace)
workflowAsObject.SetKind("Workflow")
workflowAsObject.SetAPIVersion("argoproj.io/v1alpha1")

emptyObject := &unstructured.Unstructured{}
emptyObject.SetKind("Workflow")
emptyObject.SetAPIVersion("argoproj.io/v1alpha1")
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: workflowName, Namespace: podNamespace}, emptyObject).Do(
func(_ context.Context, _ types.NamespacedName, obj *unstructured.Unstructured, _ ...any) error {
workflowAsObject.DeepCopyInto(obj)
return nil
})

service, err := s.Resolver.ResolvePodToServiceIdentity(context.Background(), &myPod)
s.Require().NoError(err)
s.Require().Equal("argo-workflow", service.Name)
s.Require().Equal("Workflow", service.Kind)
}

func (s *ServiceIdResolverTestSuite) TestResolvePodToServiceIdentity_SparkApplicationOwner() {
podName := "spark-pod-12345"
podNamespace := "spark-namespace"
sparkAppName := "my-spark-app"

myPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
OwnerReferences: []metav1.OwnerReference{
{
Kind: "SparkApplication",
Name: sparkAppName,
APIVersion: "sparkoperator.k8s.io/v1beta2",
},
},
},
}

sparkAsObject := unstructured.Unstructured{}
sparkAsObject.SetName(sparkAppName)
sparkAsObject.SetNamespace(podNamespace)
sparkAsObject.SetKind("SparkApplication")
sparkAsObject.SetAPIVersion("sparkoperator.k8s.io/v1beta2")

emptyObject := &unstructured.Unstructured{}
emptyObject.SetKind("SparkApplication")
emptyObject.SetAPIVersion("sparkoperator.k8s.io/v1beta2")
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: sparkAppName, Namespace: podNamespace}, emptyObject).Do(
func(_ context.Context, _ types.NamespacedName, obj *unstructured.Unstructured, _ ...any) error {
sparkAsObject.DeepCopyInto(obj)
return nil
})

service, err := s.Resolver.ResolvePodToServiceIdentity(context.Background(), &myPod)
s.Require().NoError(err)
s.Require().Equal("spark", service.Name)
s.Require().Equal("SparkApplication", service.Kind)
}

func (s *ServiceIdResolverTestSuite) TestResolvePodToServiceIdentity_RunnerDeploymentOwner() {
podName := "runner-pod-12345"
podNamespace := "actions-namespace"
runnerDeploymentName := "my-runner-deployment"

myPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
OwnerReferences: []metav1.OwnerReference{
{
Kind: "RunnerDeployment",
Name: runnerDeploymentName,
APIVersion: "actions.summerwind.dev/v1alpha1",
},
},
},
}

runnerAsObject := unstructured.Unstructured{}
runnerAsObject.SetName(runnerDeploymentName)
runnerAsObject.SetNamespace(podNamespace)
runnerAsObject.SetKind("RunnerDeployment")
runnerAsObject.SetAPIVersion("actions.summerwind.dev/v1alpha1")

emptyObject := &unstructured.Unstructured{}
emptyObject.SetKind("RunnerDeployment")
emptyObject.SetAPIVersion("actions.summerwind.dev/v1alpha1")
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: runnerDeploymentName, Namespace: podNamespace}, emptyObject).Do(
func(_ context.Context, _ types.NamespacedName, obj *unstructured.Unstructured, _ ...any) error {
runnerAsObject.DeepCopyInto(obj)
return nil
})

service, err := s.Resolver.ResolvePodToServiceIdentity(context.Background(), &myPod)
s.Require().NoError(err)
s.Require().Equal("actions-runner", service.Name)
s.Require().Equal("RunnerDeployment", service.Kind)
}

func (s *ServiceIdResolverTestSuite) TestResolvePodToServiceIdentity_ServiceOwner() {
podName := "service-pod-12345"
podNamespace := "service-namespace"
serviceName := "my-service"

myPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
OwnerReferences: []metav1.OwnerReference{
{
Kind: "Service",
Name: serviceName,
APIVersion: "v1",
},
},
},
}

serviceAsObject := unstructured.Unstructured{}
serviceAsObject.SetName(serviceName)
serviceAsObject.SetNamespace(podNamespace)
serviceAsObject.SetKind("Service")
serviceAsObject.SetAPIVersion("v1")

emptyObject := &unstructured.Unstructured{}
emptyObject.SetKind("Service")
emptyObject.SetAPIVersion("v1")
s.Client.EXPECT().Get(gomock.Any(), types.NamespacedName{Name: serviceName, Namespace: podNamespace}, emptyObject).Do(
func(_ context.Context, _ types.NamespacedName, obj *unstructured.Unstructured, _ ...any) error {
serviceAsObject.DeepCopyInto(obj)
return nil
})

service, err := s.Resolver.ResolvePodToServiceIdentity(context.Background(), &myPod)
s.Require().NoError(err)
s.Require().Equal(serviceName, service.Name)
// Service kind should include API group to disambiguate from core Service
s.Require().Equal("Service.", service.Kind)
}

func (s *ServiceIdResolverTestSuite) TestUserSpecifiedAnnotationForServiceName() {
annotationName := "coolAnnotationName"
expectedEnvVarName := "OTTERIZE_SERVICE_NAME_OVERRIDE_ANNOTATION"
Expand Down
Loading