Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
OpenDAS
dynamo
Commits
94876e58
"docs/vscode:/vscode.git/clone" did not exist on "498e6668e798483d3e47ef97199b4394e4d25dfd"
Unverified
Commit
94876e58
authored
Aug 18, 2025
by
julienmancuso
Committed by
GitHub
Aug 18, 2025
Browse files
Fix: Canonicalize PodGangSet to prevent spurious updates (#2469)
parent
78c7e352
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1302 additions
and
48 deletions
+1302
-48
deploy/cloud/operator/go.mod
deploy/cloud/operator/go.mod
+2
-0
deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go
...ternal/controller/dynamocomponentdeployment_controller.go
+1
-1
deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go
...l/controller/dynamocomponentdeployment_controller_test.go
+2
-0
deploy/cloud/operator/internal/controller_common/pod.go
deploy/cloud/operator/internal/controller_common/pod.go
+277
-0
deploy/cloud/operator/internal/controller_common/pod_test.go
deploy/cloud/operator/internal/controller_common/pod_test.go
+881
-0
deploy/cloud/operator/internal/controller_common/podgangset.go
...y/cloud/operator/internal/controller_common/podgangset.go
+19
-0
deploy/cloud/operator/internal/controller_common/resource.go
deploy/cloud/operator/internal/controller_common/resource.go
+30
-0
deploy/cloud/operator/internal/dynamo/component_common.go
deploy/cloud/operator/internal/dynamo/component_common.go
+9
-1
deploy/cloud/operator/internal/dynamo/component_planner.go
deploy/cloud/operator/internal/dynamo/component_planner.go
+2
-3
deploy/cloud/operator/internal/dynamo/graph.go
deploy/cloud/operator/internal/dynamo/graph.go
+25
-21
deploy/cloud/operator/internal/dynamo/graph_test.go
deploy/cloud/operator/internal/dynamo/graph_test.go
+54
-22
No files found.
deploy/cloud/operator/go.mod
View file @
94876e58
...
@@ -13,6 +13,7 @@ require (
...
@@ -13,6 +13,7 @@ require (
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/onsi/gomega v1.37.0
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.2
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.2
github.com/stretchr/testify v1.10.0
go.etcd.io/etcd/client/v3 v3.5.21
go.etcd.io/etcd/client/v3 v3.5.21
istio.io/api v1.23.1
istio.io/api v1.23.1
istio.io/client-go v1.23.1
istio.io/client-go v1.23.1
...
@@ -57,6 +58,7 @@ require (
...
@@ -57,6 +58,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
...
...
deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller.go
View file @
94876e58
...
@@ -1217,7 +1217,7 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex
...
@@ -1217,7 +1217,7 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex
podLabels
[
commonconsts
.
KubeLabelDynamoSelector
]
=
kubeName
podLabels
[
commonconsts
.
KubeLabelDynamoSelector
]
=
kubeName
podSpec
:=
&
basePodSpec
podSpec
:=
basePodSpec
podSpec
.
Containers
=
containers
podSpec
.
Containers
=
containers
extraPodMetadata
:=
opt
.
dynamoComponentDeployment
.
Spec
.
ExtraPodMetadata
extraPodMetadata
:=
opt
.
dynamoComponentDeployment
.
Spec
.
ExtraPodMetadata
...
...
deploy/cloud/operator/internal/controller/dynamocomponentdeployment_controller_test.go
View file @
94876e58
...
@@ -796,6 +796,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
...
@@ -796,6 +796,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
},
},
},
},
},
},
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -895,6 +896,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
...
@@ -895,6 +896,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
},
},
},
},
},
},
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
...
deploy/cloud/operator/internal/controller_common/pod.go
0 → 100644
View file @
94876e58
package
controller_common
import
(
"sort"
corev1
"k8s.io/api/core/v1"
)
// CanonicalizePodSpec sorts the pod spec in a way that is deterministic and easy to reason about.
//
//nolint:gocyclo
func
CanonicalizePodSpec
(
podSpec
*
corev1
.
PodSpec
)
*
corev1
.
PodSpec
{
// Helper function to get EnvFromSource sort key
envFromKey
:=
func
(
e
corev1
.
EnvFromSource
)
string
{
if
e
.
ConfigMapRef
!=
nil
{
return
"cm:"
+
e
.
ConfigMapRef
.
Name
+
":"
+
e
.
Prefix
}
if
e
.
SecretRef
!=
nil
{
return
"sec:"
+
e
.
SecretRef
.
Name
+
":"
+
e
.
Prefix
}
return
"other:"
+
e
.
Prefix
}
// Helper function to sort container-like fields (works for both Container and EphemeralContainer)
sortContainerFields
:=
func
(
env
[]
corev1
.
EnvVar
,
envFrom
[]
corev1
.
EnvFromSource
,
ports
[]
corev1
.
ContainerPort
,
volumeMounts
[]
corev1
.
VolumeMount
,
securityContext
*
corev1
.
SecurityContext
)
{
// Sort env vars by name
if
len
(
env
)
>
1
{
sort
.
Slice
(
env
,
func
(
i
,
j
int
)
bool
{
return
env
[
i
]
.
Name
<
env
[
j
]
.
Name
})
}
// Sort envFrom by referenced source and prefix
if
len
(
envFrom
)
>
1
{
sort
.
Slice
(
envFrom
,
func
(
i
,
j
int
)
bool
{
return
envFromKey
(
envFrom
[
i
])
<
envFromKey
(
envFrom
[
j
])
})
}
// Sort ports by name then port number
if
len
(
ports
)
>
1
{
sort
.
Slice
(
ports
,
func
(
i
,
j
int
)
bool
{
if
ports
[
i
]
.
Name
==
ports
[
j
]
.
Name
{
return
ports
[
i
]
.
ContainerPort
<
ports
[
j
]
.
ContainerPort
}
return
ports
[
i
]
.
Name
<
ports
[
j
]
.
Name
})
}
// Sort volume mounts by name then mount path
if
len
(
volumeMounts
)
>
1
{
sort
.
Slice
(
volumeMounts
,
func
(
i
,
j
int
)
bool
{
if
volumeMounts
[
i
]
.
Name
==
volumeMounts
[
j
]
.
Name
{
return
volumeMounts
[
i
]
.
MountPath
<
volumeMounts
[
j
]
.
MountPath
}
return
volumeMounts
[
i
]
.
Name
<
volumeMounts
[
j
]
.
Name
})
}
// Sort security context capability lists
if
securityContext
!=
nil
&&
securityContext
.
Capabilities
!=
nil
{
if
caps
:=
securityContext
.
Capabilities
.
Add
;
len
(
caps
)
>
1
{
sort
.
Slice
(
caps
,
func
(
i
,
j
int
)
bool
{
return
string
(
caps
[
i
])
<
string
(
caps
[
j
])
})
}
if
caps
:=
securityContext
.
Capabilities
.
Drop
;
len
(
caps
)
>
1
{
sort
.
Slice
(
caps
,
func
(
i
,
j
int
)
bool
{
return
string
(
caps
[
i
])
<
string
(
caps
[
j
])
})
}
}
}
// Sort regular containers
for
i
:=
range
podSpec
.
Containers
{
c
:=
&
podSpec
.
Containers
[
i
]
sortContainerFields
(
c
.
Env
,
c
.
EnvFrom
,
c
.
Ports
,
c
.
VolumeMounts
,
c
.
SecurityContext
)
}
if
len
(
podSpec
.
Containers
)
>
1
{
sort
.
Slice
(
podSpec
.
Containers
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
Containers
[
i
]
.
Name
<
podSpec
.
Containers
[
j
]
.
Name
})
}
// Sort init containers
for
i
:=
range
podSpec
.
InitContainers
{
c
:=
&
podSpec
.
InitContainers
[
i
]
sortContainerFields
(
c
.
Env
,
c
.
EnvFrom
,
c
.
Ports
,
c
.
VolumeMounts
,
c
.
SecurityContext
)
}
if
len
(
podSpec
.
InitContainers
)
>
1
{
sort
.
Slice
(
podSpec
.
InitContainers
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
InitContainers
[
i
]
.
Name
<
podSpec
.
InitContainers
[
j
]
.
Name
})
}
// Sort ephemeral containers
for
i
:=
range
podSpec
.
EphemeralContainers
{
ec
:=
&
podSpec
.
EphemeralContainers
[
i
]
sortContainerFields
(
ec
.
Env
,
ec
.
EnvFrom
,
ec
.
Ports
,
ec
.
VolumeMounts
,
ec
.
SecurityContext
)
}
if
len
(
podSpec
.
EphemeralContainers
)
>
1
{
sort
.
Slice
(
podSpec
.
EphemeralContainers
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
EphemeralContainers
[
i
]
.
Name
<
podSpec
.
EphemeralContainers
[
j
]
.
Name
})
}
// Sort image pull secrets
if
len
(
podSpec
.
ImagePullSecrets
)
>
1
{
sort
.
Slice
(
podSpec
.
ImagePullSecrets
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
ImagePullSecrets
[
i
]
.
Name
<
podSpec
.
ImagePullSecrets
[
j
]
.
Name
})
}
// Sort volumes and their nested items
sortKeyToPathItems
:=
func
(
items
[]
corev1
.
KeyToPath
)
{
if
len
(
items
)
>
1
{
sort
.
Slice
(
items
,
func
(
i
,
j
int
)
bool
{
if
items
[
i
]
.
Key
==
items
[
j
]
.
Key
{
return
items
[
i
]
.
Path
<
items
[
j
]
.
Path
}
return
items
[
i
]
.
Key
<
items
[
j
]
.
Key
})
}
}
for
i
:=
range
podSpec
.
Volumes
{
v
:=
&
podSpec
.
Volumes
[
i
]
// ConfigMap items
if
v
.
ConfigMap
!=
nil
{
sortKeyToPathItems
(
v
.
ConfigMap
.
Items
)
}
// Secret items
if
v
.
Secret
!=
nil
{
sortKeyToPathItems
(
v
.
Secret
.
Items
)
}
// DownwardAPI items
if
v
.
DownwardAPI
!=
nil
&&
len
(
v
.
DownwardAPI
.
Items
)
>
1
{
sort
.
Slice
(
v
.
DownwardAPI
.
Items
,
func
(
i
,
j
int
)
bool
{
return
v
.
DownwardAPI
.
Items
[
i
]
.
Path
<
v
.
DownwardAPI
.
Items
[
j
]
.
Path
})
}
// Projected sources
if
v
.
Projected
!=
nil
{
// Sort projected sources
if
len
(
v
.
Projected
.
Sources
)
>
1
{
sort
.
Slice
(
v
.
Projected
.
Sources
,
func
(
i
,
j
int
)
bool
{
getProjectionKey
:=
func
(
p
corev1
.
VolumeProjection
)
string
{
if
p
.
ConfigMap
!=
nil
{
return
"cm:"
+
p
.
ConfigMap
.
Name
}
if
p
.
Secret
!=
nil
{
return
"sec:"
+
p
.
Secret
.
Name
}
if
p
.
DownwardAPI
!=
nil
{
return
"downward:"
}
if
p
.
ServiceAccountToken
!=
nil
{
return
"sat:"
+
p
.
ServiceAccountToken
.
Audience
}
return
"z:other"
}
return
getProjectionKey
(
v
.
Projected
.
Sources
[
i
])
<
getProjectionKey
(
v
.
Projected
.
Sources
[
j
])
})
}
// Sort nested items for each projection
for
j
:=
range
v
.
Projected
.
Sources
{
p
:=
&
v
.
Projected
.
Sources
[
j
]
if
p
.
ConfigMap
!=
nil
{
sortKeyToPathItems
(
p
.
ConfigMap
.
Items
)
}
if
p
.
Secret
!=
nil
{
sortKeyToPathItems
(
p
.
Secret
.
Items
)
}
if
p
.
DownwardAPI
!=
nil
&&
len
(
p
.
DownwardAPI
.
Items
)
>
1
{
sort
.
Slice
(
p
.
DownwardAPI
.
Items
,
func
(
i
,
j
int
)
bool
{
return
p
.
DownwardAPI
.
Items
[
i
]
.
Path
<
p
.
DownwardAPI
.
Items
[
j
]
.
Path
})
}
}
}
}
// Sort volumes by name
if
len
(
podSpec
.
Volumes
)
>
1
{
sort
.
Slice
(
podSpec
.
Volumes
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
Volumes
[
i
]
.
Name
<
podSpec
.
Volumes
[
j
]
.
Name
})
}
// Sort tolerations
if
len
(
podSpec
.
Tolerations
)
>
1
{
sort
.
Slice
(
podSpec
.
Tolerations
,
func
(
i
,
j
int
)
bool
{
a
,
b
:=
podSpec
.
Tolerations
[
i
],
podSpec
.
Tolerations
[
j
]
if
a
.
Key
!=
b
.
Key
{
return
a
.
Key
<
b
.
Key
}
if
string
(
a
.
Operator
)
!=
string
(
b
.
Operator
)
{
return
string
(
a
.
Operator
)
<
string
(
b
.
Operator
)
}
if
a
.
Value
!=
b
.
Value
{
return
a
.
Value
<
b
.
Value
}
if
string
(
a
.
Effect
)
!=
string
(
b
.
Effect
)
{
return
string
(
a
.
Effect
)
<
string
(
b
.
Effect
)
}
// Handle TolerationSeconds (could be nil)
aSec
,
bSec
:=
int64
(
0
),
int64
(
0
)
if
a
.
TolerationSeconds
!=
nil
{
aSec
=
*
a
.
TolerationSeconds
}
if
b
.
TolerationSeconds
!=
nil
{
bSec
=
*
b
.
TolerationSeconds
}
return
aSec
<
bSec
})
}
// Sort topology spread constraints
if
len
(
podSpec
.
TopologySpreadConstraints
)
>
1
{
sort
.
Slice
(
podSpec
.
TopologySpreadConstraints
,
func
(
i
,
j
int
)
bool
{
a
,
b
:=
podSpec
.
TopologySpreadConstraints
[
i
],
podSpec
.
TopologySpreadConstraints
[
j
]
if
a
.
TopologyKey
!=
b
.
TopologyKey
{
return
a
.
TopologyKey
<
b
.
TopologyKey
}
if
string
(
a
.
WhenUnsatisfiable
)
!=
string
(
b
.
WhenUnsatisfiable
)
{
return
string
(
a
.
WhenUnsatisfiable
)
<
string
(
b
.
WhenUnsatisfiable
)
}
return
a
.
MaxSkew
<
b
.
MaxSkew
})
}
// Sort host aliases
if
len
(
podSpec
.
HostAliases
)
>
1
{
// First sort hostnames within each alias
for
i
:=
range
podSpec
.
HostAliases
{
if
len
(
podSpec
.
HostAliases
[
i
]
.
Hostnames
)
>
1
{
sort
.
Strings
(
podSpec
.
HostAliases
[
i
]
.
Hostnames
)
}
}
// Then sort aliases by IP
sort
.
Slice
(
podSpec
.
HostAliases
,
func
(
i
,
j
int
)
bool
{
return
podSpec
.
HostAliases
[
i
]
.
IP
<
podSpec
.
HostAliases
[
j
]
.
IP
})
}
// Sort DNS config
if
podSpec
.
DNSConfig
!=
nil
{
// Sort DNS options
if
len
(
podSpec
.
DNSConfig
.
Options
)
>
1
{
sort
.
Slice
(
podSpec
.
DNSConfig
.
Options
,
func
(
i
,
j
int
)
bool
{
if
podSpec
.
DNSConfig
.
Options
[
i
]
.
Name
==
podSpec
.
DNSConfig
.
Options
[
j
]
.
Name
{
vi
,
vj
:=
""
,
""
if
podSpec
.
DNSConfig
.
Options
[
i
]
.
Value
!=
nil
{
vi
=
*
podSpec
.
DNSConfig
.
Options
[
i
]
.
Value
}
if
podSpec
.
DNSConfig
.
Options
[
j
]
.
Value
!=
nil
{
vj
=
*
podSpec
.
DNSConfig
.
Options
[
j
]
.
Value
}
return
vi
<
vj
}
return
podSpec
.
DNSConfig
.
Options
[
i
]
.
Name
<
podSpec
.
DNSConfig
.
Options
[
j
]
.
Name
})
}
// Sort nameservers and search domains
if
len
(
podSpec
.
DNSConfig
.
Nameservers
)
>
1
{
sort
.
Strings
(
podSpec
.
DNSConfig
.
Nameservers
)
}
if
len
(
podSpec
.
DNSConfig
.
Searches
)
>
1
{
sort
.
Strings
(
podSpec
.
DNSConfig
.
Searches
)
}
}
return
podSpec
}
deploy/cloud/operator/internal/controller_common/pod_test.go
0 → 100644
View file @
94876e58
package
controller_common
import
(
"testing"
"github.com/stretchr/testify/assert"
corev1
"k8s.io/api/core/v1"
)
func
TestCanonicalizePodSpec
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
input
*
corev1
.
PodSpec
expected
*
corev1
.
PodSpec
}{
{
name
:
"sorts containers by name"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"zebra"
},
{
Name
:
"alpha"
},
{
Name
:
"beta"
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"alpha"
},
{
Name
:
"beta"
},
{
Name
:
"zebra"
},
},
},
},
{
name
:
"sorts init containers by name"
,
input
:
&
corev1
.
PodSpec
{
InitContainers
:
[]
corev1
.
Container
{
{
Name
:
"init-zebra"
},
{
Name
:
"init-alpha"
},
},
},
expected
:
&
corev1
.
PodSpec
{
InitContainers
:
[]
corev1
.
Container
{
{
Name
:
"init-alpha"
},
{
Name
:
"init-zebra"
},
},
},
},
{
name
:
"sorts ephemeral containers by name"
,
input
:
&
corev1
.
PodSpec
{
EphemeralContainers
:
[]
corev1
.
EphemeralContainer
{
{
EphemeralContainerCommon
:
corev1
.
EphemeralContainerCommon
{
Name
:
"debug-zebra"
}},
{
EphemeralContainerCommon
:
corev1
.
EphemeralContainerCommon
{
Name
:
"debug-alpha"
}},
},
},
expected
:
&
corev1
.
PodSpec
{
EphemeralContainers
:
[]
corev1
.
EphemeralContainer
{
{
EphemeralContainerCommon
:
corev1
.
EphemeralContainerCommon
{
Name
:
"debug-alpha"
}},
{
EphemeralContainerCommon
:
corev1
.
EphemeralContainerCommon
{
Name
:
"debug-zebra"
}},
},
},
},
{
name
:
"sorts environment variables by name"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
Env
:
[]
corev1
.
EnvVar
{
{
Name
:
"ZOO"
,
Value
:
"zebra"
},
{
Name
:
"ALPHA"
,
Value
:
"apple"
},
{
Name
:
"BETA"
,
Value
:
"banana"
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
Env
:
[]
corev1
.
EnvVar
{
{
Name
:
"ALPHA"
,
Value
:
"apple"
},
{
Name
:
"BETA"
,
Value
:
"banana"
},
{
Name
:
"ZOO"
,
Value
:
"zebra"
},
},
},
},
},
},
{
name
:
"sorts envFrom by source type and name"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
EnvFrom
:
[]
corev1
.
EnvFromSource
{
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-z"
}}},
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
}}},
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-a"
}}},
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-z"
}}},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
EnvFrom
:
[]
corev1
.
EnvFromSource
{
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
}}},
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-z"
}}},
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-a"
}}},
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-z"
}}},
},
},
},
},
},
{
name
:
"sorts container ports by name then port number"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
Ports
:
[]
corev1
.
ContainerPort
{
{
Name
:
"http"
,
ContainerPort
:
8080
},
{
Name
:
"grpc"
,
ContainerPort
:
9090
},
{
Name
:
"grpc"
,
ContainerPort
:
8080
},
{
Name
:
"debug"
,
ContainerPort
:
8080
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
Ports
:
[]
corev1
.
ContainerPort
{
{
Name
:
"debug"
,
ContainerPort
:
8080
},
{
Name
:
"grpc"
,
ContainerPort
:
8080
},
{
Name
:
"grpc"
,
ContainerPort
:
9090
},
{
Name
:
"http"
,
ContainerPort
:
8080
},
},
},
},
},
},
{
name
:
"sorts volume mounts by name then mount path"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
VolumeMounts
:
[]
corev1
.
VolumeMount
{
{
Name
:
"vol1"
,
MountPath
:
"/data2"
},
{
Name
:
"vol2"
,
MountPath
:
"/data1"
},
{
Name
:
"vol1"
,
MountPath
:
"/data1"
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
VolumeMounts
:
[]
corev1
.
VolumeMount
{
{
Name
:
"vol1"
,
MountPath
:
"/data1"
},
{
Name
:
"vol1"
,
MountPath
:
"/data2"
},
{
Name
:
"vol2"
,
MountPath
:
"/data1"
},
},
},
},
},
},
{
name
:
"sorts security context capabilities"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
SecurityContext
:
&
corev1
.
SecurityContext
{
Capabilities
:
&
corev1
.
Capabilities
{
Add
:
[]
corev1
.
Capability
{
"SYS_ADMIN"
,
"NET_ADMIN"
,
"CHOWN"
},
Drop
:
[]
corev1
.
Capability
{
"ALL"
,
"SETUID"
,
"KILL"
},
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
SecurityContext
:
&
corev1
.
SecurityContext
{
Capabilities
:
&
corev1
.
Capabilities
{
Add
:
[]
corev1
.
Capability
{
"CHOWN"
,
"NET_ADMIN"
,
"SYS_ADMIN"
},
Drop
:
[]
corev1
.
Capability
{
"ALL"
,
"KILL"
,
"SETUID"
},
},
},
},
},
},
},
{
name
:
"sorts image pull secrets by name"
,
input
:
&
corev1
.
PodSpec
{
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{
{
Name
:
"registry-z"
},
{
Name
:
"registry-a"
},
{
Name
:
"registry-b"
},
},
},
expected
:
&
corev1
.
PodSpec
{
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{
{
Name
:
"registry-a"
},
{
Name
:
"registry-b"
},
{
Name
:
"registry-z"
},
},
},
},
{
name
:
"sorts volumes by name"
,
input
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"vol-z"
},
{
Name
:
"vol-a"
},
{
Name
:
"vol-b"
},
},
},
expected
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"vol-a"
},
{
Name
:
"vol-b"
},
{
Name
:
"vol-z"
},
},
},
},
{
name
:
"sorts configmap volume items by key then path"
,
input
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"config"
,
VolumeSource
:
corev1
.
VolumeSource
{
ConfigMap
:
&
corev1
.
ConfigMapVolumeSource
{
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"app.conf"
,
Path
:
"config/app.conf"
},
{
Key
:
"db.conf"
,
Path
:
"config/db.conf"
},
{
Key
:
"app.conf"
,
Path
:
"backup/app.conf"
},
},
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"config"
,
VolumeSource
:
corev1
.
VolumeSource
{
ConfigMap
:
&
corev1
.
ConfigMapVolumeSource
{
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"app.conf"
,
Path
:
"backup/app.conf"
},
{
Key
:
"app.conf"
,
Path
:
"config/app.conf"
},
{
Key
:
"db.conf"
,
Path
:
"config/db.conf"
},
},
},
},
},
},
},
},
{
name
:
"sorts secret volume items by key then path"
,
input
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"secret"
,
VolumeSource
:
corev1
.
VolumeSource
{
Secret
:
&
corev1
.
SecretVolumeSource
{
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"tls.key"
,
Path
:
"tls/server.key"
},
{
Key
:
"tls.crt"
,
Path
:
"tls/server.crt"
},
{
Key
:
"tls.key"
,
Path
:
"backup/server.key"
},
},
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"secret"
,
VolumeSource
:
corev1
.
VolumeSource
{
Secret
:
&
corev1
.
SecretVolumeSource
{
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"tls.crt"
,
Path
:
"tls/server.crt"
},
{
Key
:
"tls.key"
,
Path
:
"backup/server.key"
},
{
Key
:
"tls.key"
,
Path
:
"tls/server.key"
},
},
},
},
},
},
},
},
{
name
:
"sorts downward API items by path"
,
input
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"downward"
,
VolumeSource
:
corev1
.
VolumeSource
{
DownwardAPI
:
&
corev1
.
DownwardAPIVolumeSource
{
Items
:
[]
corev1
.
DownwardAPIVolumeFile
{
{
Path
:
"metadata/name"
},
{
Path
:
"metadata/annotations"
},
{
Path
:
"limits/cpu"
},
},
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"downward"
,
VolumeSource
:
corev1
.
VolumeSource
{
DownwardAPI
:
&
corev1
.
DownwardAPIVolumeSource
{
Items
:
[]
corev1
.
DownwardAPIVolumeFile
{
{
Path
:
"limits/cpu"
},
{
Path
:
"metadata/annotations"
},
{
Path
:
"metadata/name"
},
},
},
},
},
},
},
},
{
name
:
"sorts projected volume sources and their items"
,
input
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"projected"
,
VolumeSource
:
corev1
.
VolumeSource
{
Projected
:
&
corev1
.
ProjectedVolumeSource
{
Sources
:
[]
corev1
.
VolumeProjection
{
{
Secret
:
&
corev1
.
SecretProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-z"
},
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"password"
,
Path
:
"auth/password"
},
{
Key
:
"username"
,
Path
:
"auth/username"
},
},
},
},
{
ConfigMap
:
&
corev1
.
ConfigMapProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
},
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"db.conf"
,
Path
:
"config/db.conf"
},
{
Key
:
"app.conf"
,
Path
:
"config/app.conf"
},
},
},
},
{
DownwardAPI
:
&
corev1
.
DownwardAPIProjection
{
Items
:
[]
corev1
.
DownwardAPIVolumeFile
{
{
Path
:
"metadata/name"
},
{
Path
:
"limits/cpu"
},
},
},
},
{
ServiceAccountToken
:
&
corev1
.
ServiceAccountTokenProjection
{
Audience
:
"api.example.com"
,
Path
:
"tokens/api"
,
},
},
},
},
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"projected"
,
VolumeSource
:
corev1
.
VolumeSource
{
Projected
:
&
corev1
.
ProjectedVolumeSource
{
Sources
:
[]
corev1
.
VolumeProjection
{
{
ConfigMap
:
&
corev1
.
ConfigMapProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
},
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"app.conf"
,
Path
:
"config/app.conf"
},
{
Key
:
"db.conf"
,
Path
:
"config/db.conf"
},
},
},
},
{
DownwardAPI
:
&
corev1
.
DownwardAPIProjection
{
Items
:
[]
corev1
.
DownwardAPIVolumeFile
{
{
Path
:
"limits/cpu"
},
{
Path
:
"metadata/name"
},
},
},
},
{
ServiceAccountToken
:
&
corev1
.
ServiceAccountTokenProjection
{
Audience
:
"api.example.com"
,
Path
:
"tokens/api"
,
},
},
{
Secret
:
&
corev1
.
SecretProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-z"
},
Items
:
[]
corev1
.
KeyToPath
{
{
Key
:
"password"
,
Path
:
"auth/password"
},
{
Key
:
"username"
,
Path
:
"auth/username"
},
},
},
},
},
},
},
},
},
},
},
{
name
:
"sorts tolerations by key, operator, value, effect, seconds"
,
input
:
&
corev1
.
PodSpec
{
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"node-type"
,
Operator
:
corev1
.
TolerationOpEqual
,
Value
:
"gpu"
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
{
Key
:
"node-role"
,
Operator
:
corev1
.
TolerationOpEqual
,
Value
:
"master"
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
{
Key
:
"node-role"
,
Operator
:
corev1
.
TolerationOpExists
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
},
},
expected
:
&
corev1
.
PodSpec
{
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"node-role"
,
Operator
:
corev1
.
TolerationOpEqual
,
Value
:
"master"
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
{
Key
:
"node-role"
,
Operator
:
corev1
.
TolerationOpExists
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
{
Key
:
"node-type"
,
Operator
:
corev1
.
TolerationOpEqual
,
Value
:
"gpu"
,
Effect
:
corev1
.
TaintEffectNoSchedule
,
},
},
},
},
{
name
:
"sorts topology spread constraints by topology key, when unsatisfiable, max skew"
,
input
:
&
corev1
.
PodSpec
{
TopologySpreadConstraints
:
[]
corev1
.
TopologySpreadConstraint
{
{
TopologyKey
:
"kubernetes.io/zone"
,
WhenUnsatisfiable
:
corev1
.
DoNotSchedule
,
MaxSkew
:
2
,
},
{
TopologyKey
:
"kubernetes.io/hostname"
,
WhenUnsatisfiable
:
corev1
.
DoNotSchedule
,
MaxSkew
:
1
,
},
{
TopologyKey
:
"kubernetes.io/hostname"
,
WhenUnsatisfiable
:
corev1
.
ScheduleAnyway
,
MaxSkew
:
1
,
},
},
},
expected
:
&
corev1
.
PodSpec
{
TopologySpreadConstraints
:
[]
corev1
.
TopologySpreadConstraint
{
{
TopologyKey
:
"kubernetes.io/hostname"
,
WhenUnsatisfiable
:
corev1
.
DoNotSchedule
,
MaxSkew
:
1
,
},
{
TopologyKey
:
"kubernetes.io/hostname"
,
WhenUnsatisfiable
:
corev1
.
ScheduleAnyway
,
MaxSkew
:
1
,
},
{
TopologyKey
:
"kubernetes.io/zone"
,
WhenUnsatisfiable
:
corev1
.
DoNotSchedule
,
MaxSkew
:
2
,
},
},
},
},
{
name
:
"sorts host aliases by IP and hostnames within each alias"
,
input
:
&
corev1
.
PodSpec
{
HostAliases
:
[]
corev1
.
HostAlias
{
{
IP
:
"192.168.1.2"
,
Hostnames
:
[]
string
{
"web2.example.com"
,
"api2.example.com"
},
},
{
IP
:
"192.168.1.1"
,
Hostnames
:
[]
string
{
"web1.example.com"
,
"api1.example.com"
,
"admin1.example.com"
},
},
},
},
expected
:
&
corev1
.
PodSpec
{
HostAliases
:
[]
corev1
.
HostAlias
{
{
IP
:
"192.168.1.1"
,
Hostnames
:
[]
string
{
"admin1.example.com"
,
"api1.example.com"
,
"web1.example.com"
},
},
{
IP
:
"192.168.1.2"
,
Hostnames
:
[]
string
{
"api2.example.com"
,
"web2.example.com"
},
},
},
},
},
{
name
:
"sorts DNS config options, nameservers, and searches"
,
input
:
&
corev1
.
PodSpec
{
DNSConfig
:
&
corev1
.
PodDNSConfig
{
Options
:
[]
corev1
.
PodDNSConfigOption
{
{
Name
:
"timeout"
,
Value
:
func
()
*
string
{
s
:=
"5"
;
return
&
s
}()},
{
Name
:
"attempts"
,
Value
:
func
()
*
string
{
s
:=
"3"
;
return
&
s
}()},
{
Name
:
"ndots"
,
Value
:
func
()
*
string
{
s
:=
"2"
;
return
&
s
}()},
},
Nameservers
:
[]
string
{
"8.8.8.8"
,
"1.1.1.1"
,
"8.8.4.4"
},
Searches
:
[]
string
{
"example.com"
,
"cluster.local"
,
"app.local"
},
},
},
expected
:
&
corev1
.
PodSpec
{
DNSConfig
:
&
corev1
.
PodDNSConfig
{
Options
:
[]
corev1
.
PodDNSConfigOption
{
{
Name
:
"attempts"
,
Value
:
func
()
*
string
{
s
:=
"3"
;
return
&
s
}()},
{
Name
:
"ndots"
,
Value
:
func
()
*
string
{
s
:=
"2"
;
return
&
s
}()},
{
Name
:
"timeout"
,
Value
:
func
()
*
string
{
s
:=
"5"
;
return
&
s
}()},
},
Nameservers
:
[]
string
{
"1.1.1.1"
,
"8.8.4.4"
,
"8.8.8.8"
},
Searches
:
[]
string
{
"app.local"
,
"cluster.local"
,
"example.com"
},
},
},
},
{
name
:
"handles nil pointer values gracefully"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
SecurityContext
:
&
corev1
.
SecurityContext
{
Capabilities
:
nil
,
},
},
},
DNSConfig
:
&
corev1
.
PodDNSConfig
{
Options
:
[]
corev1
.
PodDNSConfigOption
{
{
Name
:
"timeout"
,
Value
:
nil
},
{
Name
:
"attempts"
,
Value
:
func
()
*
string
{
s
:=
"3"
;
return
&
s
}()},
},
},
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"test"
,
TolerationSeconds
:
nil
,
},
{
Key
:
"test2"
,
TolerationSeconds
:
func
()
*
int64
{
s
:=
int64
(
300
);
return
&
s
}(),
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
SecurityContext
:
&
corev1
.
SecurityContext
{
Capabilities
:
nil
,
},
},
},
DNSConfig
:
&
corev1
.
PodDNSConfig
{
Options
:
[]
corev1
.
PodDNSConfigOption
{
{
Name
:
"attempts"
,
Value
:
func
()
*
string
{
s
:=
"3"
;
return
&
s
}()},
{
Name
:
"timeout"
,
Value
:
nil
},
},
},
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"test"
,
TolerationSeconds
:
nil
,
},
{
Key
:
"test2"
,
TolerationSeconds
:
func
()
*
int64
{
s
:=
int64
(
300
);
return
&
s
}(),
},
},
},
},
{
name
:
"returns original podspec when already sorted"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"alpha"
,
Env
:
[]
corev1
.
EnvVar
{
{
Name
:
"A"
,
Value
:
"1"
},
{
Name
:
"B"
,
Value
:
"2"
},
},
},
{
Name
:
"beta"
},
},
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{
{
Name
:
"secret-a"
},
{
Name
:
"secret-b"
},
},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"alpha"
,
Env
:
[]
corev1
.
EnvVar
{
{
Name
:
"A"
,
Value
:
"1"
},
{
Name
:
"B"
,
Value
:
"2"
},
},
},
{
Name
:
"beta"
},
},
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{
{
Name
:
"secret-a"
},
{
Name
:
"secret-b"
},
},
},
},
{
name
:
"handles empty slices gracefully"
,
input
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{},
InitContainers
:
[]
corev1
.
Container
{},
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{},
Volumes
:
[]
corev1
.
Volume
{},
Tolerations
:
[]
corev1
.
Toleration
{},
},
expected
:
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{},
InitContainers
:
[]
corev1
.
Container
{},
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{},
Volumes
:
[]
corev1
.
Volume
{},
Tolerations
:
[]
corev1
.
Toleration
{},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
result
:=
CanonicalizePodSpec
(
tt
.
input
)
// Verify the function returns the same instance
assert
.
Same
(
t
,
tt
.
input
,
result
,
"function should return the same PodSpec instance"
)
// Verify the sorting is correct
assert
.
Equal
(
t
,
tt
.
expected
,
result
,
"PodSpec should be sorted correctly"
)
})
}
}
func
TestCanonicalizePodSpec_Idempotent
(
t
*
testing
.
T
)
{
// Create a complex, unsorted PodSpec
podSpec
:=
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"zebra"
,
Env
:
[]
corev1
.
EnvVar
{
{
Name
:
"Z_VAR"
,
Value
:
"z"
},
{
Name
:
"A_VAR"
,
Value
:
"a"
},
},
Ports
:
[]
corev1
.
ContainerPort
{
{
Name
:
"http"
,
ContainerPort
:
8080
},
{
Name
:
"grpc"
,
ContainerPort
:
9090
},
},
VolumeMounts
:
[]
corev1
.
VolumeMount
{
{
Name
:
"vol2"
,
MountPath
:
"/data2"
},
{
Name
:
"vol1"
,
MountPath
:
"/data1"
},
},
},
{
Name
:
"alpha"
},
},
InitContainers
:
[]
corev1
.
Container
{
{
Name
:
"init-zebra"
},
{
Name
:
"init-alpha"
},
},
ImagePullSecrets
:
[]
corev1
.
LocalObjectReference
{
{
Name
:
"secret-z"
},
{
Name
:
"secret-a"
},
},
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"vol-z"
},
{
Name
:
"vol-a"
},
},
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"node-z"
},
{
Key
:
"node-a"
},
},
}
// First canonicalization
result1
:=
CanonicalizePodSpec
(
podSpec
)
// Second canonicalization on the same object
result2
:=
CanonicalizePodSpec
(
result1
)
// Should be identical after second canonicalization
assert
.
Equal
(
t
,
result1
,
result2
,
"CanonicalizePodSpec should be idempotent"
)
// Verify containers are sorted
assert
.
Equal
(
t
,
"alpha"
,
result2
.
Containers
[
0
]
.
Name
)
assert
.
Equal
(
t
,
"zebra"
,
result2
.
Containers
[
1
]
.
Name
)
// Verify env vars within containers are sorted
assert
.
Equal
(
t
,
"A_VAR"
,
result2
.
Containers
[
1
]
.
Env
[
0
]
.
Name
)
assert
.
Equal
(
t
,
"Z_VAR"
,
result2
.
Containers
[
1
]
.
Env
[
1
]
.
Name
)
// Verify ports are sorted
assert
.
Equal
(
t
,
"grpc"
,
result2
.
Containers
[
1
]
.
Ports
[
0
]
.
Name
)
assert
.
Equal
(
t
,
"http"
,
result2
.
Containers
[
1
]
.
Ports
[
1
]
.
Name
)
// Verify volume mounts are sorted
assert
.
Equal
(
t
,
"vol1"
,
result2
.
Containers
[
1
]
.
VolumeMounts
[
0
]
.
Name
)
assert
.
Equal
(
t
,
"vol2"
,
result2
.
Containers
[
1
]
.
VolumeMounts
[
1
]
.
Name
)
}
func
TestCanonicalizePodSpec_EnvFromSortPriority
(
t
*
testing
.
T
)
{
podSpec
:=
&
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{
{
Name
:
"test"
,
EnvFrom
:
[]
corev1
.
EnvFromSource
{
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-b"
}}},
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-b"
}}},
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-a"
}}},
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
}}},
// Test duplicate names for secondary sort
{
ConfigMapRef
:
&
corev1
.
ConfigMapEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
}}},
{
SecretRef
:
&
corev1
.
SecretEnvSource
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-a"
}}},
},
},
},
}
result
:=
CanonicalizePodSpec
(
podSpec
)
// ConfigMaps should come before Secrets (cm: < sec:)
// Within each type, sorted by name
expected
:=
[]
string
{
"cm:config-a:"
,
// ConfigMap config-a
"cm:config-a:"
,
// ConfigMap config-a (duplicate)
"cm:config-b:"
,
// ConfigMap config-b
"sec:secret-a:"
,
// Secret secret-a
"sec:secret-a:"
,
// Secret secret-a (duplicate)
"sec:secret-b:"
,
// Secret secret-b
}
envFromKey
:=
func
(
e
corev1
.
EnvFromSource
)
string
{
if
e
.
ConfigMapRef
!=
nil
{
return
"cm:"
+
e
.
ConfigMapRef
.
Name
+
":"
}
if
e
.
SecretRef
!=
nil
{
return
"sec:"
+
e
.
SecretRef
.
Name
+
":"
}
return
"other:"
}
for
i
,
envFrom
:=
range
result
.
Containers
[
0
]
.
EnvFrom
{
assert
.
Equal
(
t
,
expected
[
i
],
envFromKey
(
envFrom
),
"EnvFrom at index %d should match expected sort order"
,
i
)
}
}
func
TestCanonicalizePodSpec_TolerationSecondsHandling
(
t
*
testing
.
T
)
{
sec300
:=
int64
(
300
)
sec600
:=
int64
(
600
)
podSpec
:=
&
corev1
.
PodSpec
{
Tolerations
:
[]
corev1
.
Toleration
{
{
Key
:
"key1"
,
TolerationSeconds
:
&
sec600
},
{
Key
:
"key1"
,
TolerationSeconds
:
nil
},
{
Key
:
"key1"
,
TolerationSeconds
:
&
sec300
},
},
}
result
:=
CanonicalizePodSpec
(
podSpec
)
// Should be sorted by TolerationSeconds: nil (0) < 300 < 600
assert
.
Nil
(
t
,
result
.
Tolerations
[
0
]
.
TolerationSeconds
)
assert
.
Equal
(
t
,
int64
(
300
),
*
result
.
Tolerations
[
1
]
.
TolerationSeconds
)
assert
.
Equal
(
t
,
int64
(
600
),
*
result
.
Tolerations
[
2
]
.
TolerationSeconds
)
}
func
TestCanonicalizePodSpec_ProjectedVolumeSourcePriority
(
t
*
testing
.
T
)
{
podSpec
:=
&
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
{
Name
:
"projected"
,
VolumeSource
:
corev1
.
VolumeSource
{
Projected
:
&
corev1
.
ProjectedVolumeSource
{
Sources
:
[]
corev1
.
VolumeProjection
{
{
Secret
:
&
corev1
.
SecretProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"secret-a"
}}},
{
ServiceAccountToken
:
&
corev1
.
ServiceAccountTokenProjection
{
Audience
:
"zz.example.com"
}},
{
DownwardAPI
:
&
corev1
.
DownwardAPIProjection
{}},
{
ConfigMap
:
&
corev1
.
ConfigMapProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-z"
}}},
{
ServiceAccountToken
:
&
corev1
.
ServiceAccountTokenProjection
{
Audience
:
"aa.example.com"
}},
{
ConfigMap
:
&
corev1
.
ConfigMapProjection
{
LocalObjectReference
:
corev1
.
LocalObjectReference
{
Name
:
"config-a"
}}},
},
},
},
},
},
}
result
:=
CanonicalizePodSpec
(
podSpec
)
// Expected sort order: cm: < downward: < sat: < sec:
// Within same type, sorted by name/audience
getProjectionKey
:=
func
(
p
corev1
.
VolumeProjection
)
string
{
if
p
.
ConfigMap
!=
nil
{
return
"cm:"
+
p
.
ConfigMap
.
Name
}
if
p
.
Secret
!=
nil
{
return
"sec:"
+
p
.
Secret
.
Name
}
if
p
.
DownwardAPI
!=
nil
{
return
"downward:"
}
if
p
.
ServiceAccountToken
!=
nil
{
return
"sat:"
+
p
.
ServiceAccountToken
.
Audience
}
return
"z:other"
}
expected
:=
[]
string
{
"cm:config-a"
,
"cm:config-z"
,
"downward:"
,
"sat:aa.example.com"
,
"sat:zz.example.com"
,
"sec:secret-a"
,
}
sources
:=
result
.
Volumes
[
0
]
.
VolumeSource
.
Projected
.
Sources
for
i
,
source
:=
range
sources
{
assert
.
Equal
(
t
,
expected
[
i
],
getProjectionKey
(
source
),
"Projected source at index %d should match expected sort order"
,
i
)
}
}
deploy/cloud/operator/internal/controller_common/podgangset.go
0 → 100644
View file @
94876e58
package
controller_common
import
(
"sort"
grovev1alpha1
"github.com/NVIDIA/grove/operator/api/core/v1alpha1"
)
func
CanonicalizePodGangSet
(
gangSet
*
grovev1alpha1
.
PodGangSet
)
*
grovev1alpha1
.
PodGangSet
{
// sort cliques by name
sort
.
Slice
(
gangSet
.
Spec
.
Template
.
Cliques
,
func
(
i
,
j
int
)
bool
{
return
gangSet
.
Spec
.
Template
.
Cliques
[
i
]
.
Name
<
gangSet
.
Spec
.
Template
.
Cliques
[
j
]
.
Name
})
// sort scaling groups by name
sort
.
Slice
(
gangSet
.
Spec
.
Template
.
PodCliqueScalingGroupConfigs
,
func
(
i
,
j
int
)
bool
{
return
gangSet
.
Spec
.
Template
.
PodCliqueScalingGroupConfigs
[
i
]
.
Name
<
gangSet
.
Spec
.
Template
.
PodCliqueScalingGroupConfigs
[
j
]
.
Name
})
return
gangSet
}
deploy/cloud/operator/internal/controller_common/resource.go
View file @
94876e58
...
@@ -27,6 +27,7 @@ import (
...
@@ -27,6 +27,7 @@ import (
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts"
"github.com/google/go-cmp/cmp"
corev1
"k8s.io/api/core/v1"
corev1
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/api/resource"
...
@@ -156,6 +157,14 @@ func SyncResource[T client.Object](ctx context.Context, r Reconciler, parentReso
...
@@ -156,6 +157,14 @@ func SyncResource[T client.Object](ctx context.Context, r Reconciler, parentReso
return
false
,
resource
,
fmt
.
Errorf
(
"failed to check if spec has changed: %w"
,
err
)
return
false
,
resource
,
fmt
.
Errorf
(
"failed to check if spec has changed: %w"
,
err
)
}
}
if
newHash
!=
nil
{
if
newHash
!=
nil
{
// Generate and log diff before updating
diff
,
diffErr
:=
generateSpecDiff
(
oldResource
,
resource
)
if
diffErr
!=
nil
{
logs
.
V
(
1
)
.
Info
(
fmt
.
Sprintf
(
"Failed to generate diff for %s: %v"
,
resourceType
,
diffErr
))
}
else
if
diff
!=
""
{
logs
.
Info
(
fmt
.
Sprintf
(
"%s spec changes detected"
,
resourceType
),
"diff"
,
diff
)
}
// update the spec of the current object with the desired spec
// update the spec of the current object with the desired spec
err
=
CopySpec
(
resource
,
oldResource
)
err
=
CopySpec
(
resource
,
oldResource
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -252,6 +261,27 @@ func IsSpecChanged(current client.Object, desired client.Object) (*string, error
...
@@ -252,6 +261,27 @@ func IsSpecChanged(current client.Object, desired client.Object) (*string, error
return
&
hashStr
,
nil
return
&
hashStr
,
nil
}
}
// generateSpecDiff creates a unified diff showing changes between old and new resource specs
func
generateSpecDiff
(
oldResource
,
newResource
client
.
Object
)
(
string
,
error
)
{
oldSpec
,
err
:=
getSpec
(
oldResource
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to get old spec: %w"
,
err
)
}
newSpec
,
err
:=
getSpec
(
newResource
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to get new spec: %w"
,
err
)
}
// Generate diff using cmp
diff
:=
cmp
.
Diff
(
oldSpec
,
newSpec
)
if
diff
==
""
{
return
""
,
nil
}
return
diff
,
nil
}
func
GetSpecHash
(
obj
client
.
Object
)
(
string
,
error
)
{
func
GetSpecHash
(
obj
client
.
Object
)
(
string
,
error
)
{
spec
,
err
:=
getSpec
(
obj
)
spec
,
err
:=
getSpec
(
obj
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
deploy/cloud/operator/internal/dynamo/component_common.go
View file @
94876e58
...
@@ -8,6 +8,7 @@ package dynamo
...
@@ -8,6 +8,7 @@ package dynamo
import
(
import
(
commonconsts
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts"
commonconsts
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/internal/consts"
corev1
"k8s.io/api/core/v1"
corev1
"k8s.io/api/core/v1"
"k8s.io/utils/ptr"
)
)
// ComponentDefaults interface defines how defaults should be provided
// ComponentDefaults interface defines how defaults should be provided
...
@@ -50,7 +51,14 @@ func (b *BaseComponentDefaults) GetBaseContainer(context ComponentContext) (core
...
@@ -50,7 +51,14 @@ func (b *BaseComponentDefaults) GetBaseContainer(context ComponentContext) (core
}
}
func
(
b
*
BaseComponentDefaults
)
GetBasePodSpec
(
context
ComponentContext
)
(
corev1
.
PodSpec
,
error
)
{
func
(
b
*
BaseComponentDefaults
)
GetBasePodSpec
(
context
ComponentContext
)
(
corev1
.
PodSpec
,
error
)
{
return
corev1
.
PodSpec
{},
nil
return
b
.
getCommonPodSpec
(),
nil
}
func
(
b
*
BaseComponentDefaults
)
getCommonPodSpec
()
corev1
.
PodSpec
{
return
corev1
.
PodSpec
{
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
}
}
}
func
(
b
*
BaseComponentDefaults
)
getCommonContainer
(
context
ComponentContext
)
corev1
.
Container
{
func
(
b
*
BaseComponentDefaults
)
getCommonContainer
(
context
ComponentContext
)
corev1
.
Container
{
...
...
deploy/cloud/operator/internal/dynamo/component_planner.go
View file @
94876e58
...
@@ -39,8 +39,7 @@ func (p *PlannerDefaults) GetBaseContainer(context ComponentContext) (corev1.Con
...
@@ -39,8 +39,7 @@ func (p *PlannerDefaults) GetBaseContainer(context ComponentContext) (corev1.Con
}
}
func
(
p
*
PlannerDefaults
)
GetBasePodSpec
(
context
ComponentContext
)
(
corev1
.
PodSpec
,
error
)
{
func
(
p
*
PlannerDefaults
)
GetBasePodSpec
(
context
ComponentContext
)
(
corev1
.
PodSpec
,
error
)
{
podSpec
:=
corev1
.
PodSpec
{
podSpec
:=
p
.
getCommonPodSpec
()
ServiceAccountName
:
commonconsts
.
PlannerServiceAccountName
,
podSpec
.
ServiceAccountName
=
commonconsts
.
PlannerServiceAccountName
}
return
podSpec
,
nil
return
podSpec
,
nil
}
}
deploy/cloud/operator/internal/dynamo/graph.go
View file @
94876e58
...
@@ -31,6 +31,7 @@ import (
...
@@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/api/resource"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
grovev1alpha1
"github.com/NVIDIA/grove/operator/api/core/v1alpha1"
grovev1alpha1
"github.com/NVIDIA/grove/operator/api/core/v1alpha1"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
"github.com/ai-dynamo/dynamo/deploy/cloud/operator/api/dynamo/common"
...
@@ -687,13 +688,13 @@ func GenerateBasePodSpec(
...
@@ -687,13 +688,13 @@ func GenerateBasePodSpec(
controllerConfig
controller_common
.
Config
,
controllerConfig
controller_common
.
Config
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
serviceName
string
,
serviceName
string
,
)
(
corev1
.
PodSpec
,
error
)
{
)
(
*
corev1
.
PodSpec
,
error
)
{
// Start with base container generated per component type
// Start with base container generated per component type
componentContext
:=
generateComponentContext
(
component
,
parentGraphDeploymentName
,
namespace
,
numberOfNodes
)
componentContext
:=
generateComponentContext
(
component
,
parentGraphDeploymentName
,
namespace
,
numberOfNodes
)
componentDefaults
:=
ComponentDefaultsFactory
(
component
.
ComponentType
)
componentDefaults
:=
ComponentDefaultsFactory
(
component
.
ComponentType
)
container
,
err
:=
componentDefaults
.
GetBaseContainer
(
componentContext
)
container
,
err
:=
componentDefaults
.
GetBaseContainer
(
componentContext
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to get base container: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to get base container: %w"
,
err
)
}
}
if
component
.
ExtraPodSpec
!=
nil
&&
component
.
ExtraPodSpec
.
MainContainer
!=
nil
{
if
component
.
ExtraPodSpec
!=
nil
&&
component
.
ExtraPodSpec
.
MainContainer
!=
nil
{
...
@@ -702,7 +703,7 @@ func GenerateBasePodSpec(
...
@@ -702,7 +703,7 @@ func GenerateBasePodSpec(
// merge the extraPodSpec from the parent deployment with the extraPodSpec from the service
// merge the extraPodSpec from the parent deployment with the extraPodSpec from the service
err
=
mergo
.
Merge
(
&
container
,
*
main
,
mergo
.
WithOverride
)
err
=
mergo
.
Merge
(
&
container
,
*
main
,
mergo
.
WithOverride
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to merge extraPodSpec: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to merge extraPodSpec: %w"
,
err
)
}
}
// main container fields that require special handling
// main container fields that require special handling
...
@@ -725,7 +726,7 @@ func GenerateBasePodSpec(
...
@@ -725,7 +726,7 @@ func GenerateBasePodSpec(
overrideResources
,
err
:=
controller_common
.
GetResourcesConfig
(
component
.
Resources
)
overrideResources
,
err
:=
controller_common
.
GetResourcesConfig
(
component
.
Resources
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to get resources config: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to get resources config: %w"
,
err
)
}
}
// Requests
// Requests
if
overrideResources
!=
nil
&&
len
(
overrideResources
.
Requests
)
>
0
{
if
overrideResources
!=
nil
&&
len
(
overrideResources
.
Requests
)
>
0
{
...
@@ -784,32 +785,32 @@ func GenerateBasePodSpec(
...
@@ -784,32 +785,32 @@ func GenerateBasePodSpec(
// Apply backend-specific container modifications
// Apply backend-specific container modifications
multinodeDeployer
:=
MultinodeDeployerFactory
(
multinodeDeploymentType
)
multinodeDeployer
:=
MultinodeDeployerFactory
(
multinodeDeploymentType
)
if
multinodeDeployer
==
nil
{
if
multinodeDeployer
==
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"unsupported multinode deployment type: %s"
,
multinodeDeploymentType
)
return
nil
,
fmt
.
Errorf
(
"unsupported multinode deployment type: %s"
,
multinodeDeploymentType
)
}
}
backend
:=
BackendFactory
(
backendFramework
)
backend
:=
BackendFactory
(
backendFramework
)
if
backend
==
nil
{
if
backend
==
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"unsupported backend framework: %s"
,
backendFramework
)
return
nil
,
fmt
.
Errorf
(
"unsupported backend framework: %s"
,
backendFramework
)
}
}
backend
.
UpdateContainer
(
&
container
,
numberOfNodes
,
role
,
component
,
serviceName
,
multinodeDeployer
)
backend
.
UpdateContainer
(
&
container
,
numberOfNodes
,
role
,
component
,
serviceName
,
multinodeDeployer
)
// get base podspec from component
// get base podspec from component
podSpec
,
err
:=
componentDefaults
.
GetBasePodSpec
(
componentContext
)
podSpec
,
err
:=
componentDefaults
.
GetBasePodSpec
(
componentContext
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to get base podspec: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to get base podspec: %w"
,
err
)
}
}
if
component
.
ExtraPodSpec
!=
nil
&&
component
.
ExtraPodSpec
.
PodSpec
!=
nil
{
if
component
.
ExtraPodSpec
!=
nil
&&
component
.
ExtraPodSpec
.
PodSpec
!=
nil
{
// merge extraPodSpec PodSpec with base podspec
// merge extraPodSpec PodSpec with base podspec
err
:=
mergo
.
Merge
(
&
podSpec
,
component
.
ExtraPodSpec
.
PodSpec
.
DeepCopy
(),
mergo
.
WithOverride
)
err
:=
mergo
.
Merge
(
&
podSpec
,
component
.
ExtraPodSpec
.
PodSpec
.
DeepCopy
(),
mergo
.
WithOverride
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to merge extraPodSpec: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to merge extraPodSpec: %w"
,
err
)
}
}
}
}
podSpec
.
Containers
=
append
(
podSpec
.
Containers
,
container
)
podSpec
.
Containers
=
append
(
podSpec
.
Containers
,
container
)
podSpec
.
Volumes
=
append
(
podSpec
.
Volumes
,
volumes
...
)
podSpec
.
Volumes
=
append
(
podSpec
.
Volumes
,
volumes
...
)
podSpec
.
ImagePullSecrets
=
append
(
podSpec
.
ImagePullSecrets
,
imagePullSecrets
...
)
podSpec
.
ImagePullSecrets
=
append
(
podSpec
.
ImagePullSecrets
,
imagePullSecrets
...
)
backend
.
UpdatePodSpec
(
&
podSpec
,
numberOfNodes
,
role
,
component
,
serviceName
)
backend
.
UpdatePodSpec
(
&
podSpec
,
numberOfNodes
,
role
,
component
,
serviceName
)
return
podSpec
,
nil
return
controller_common
.
CanonicalizePodSpec
(
&
podSpec
)
,
nil
}
}
func
setMetricsLabels
(
labels
map
[
string
]
string
,
dynamoGraphDeployment
*
v1alpha1
.
DynamoGraphDeployment
)
{
func
setMetricsLabels
(
labels
map
[
string
]
string
,
dynamoGraphDeployment
*
v1alpha1
.
DynamoGraphDeployment
)
{
...
@@ -846,13 +847,13 @@ func GeneratePodSpecForComponent(
...
@@ -846,13 +847,13 @@ func GeneratePodSpecForComponent(
controllerConfig
controller_common
.
Config
,
controllerConfig
controller_common
.
Config
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
serviceName
string
,
serviceName
string
,
)
(
corev1
.
PodSpec
,
error
)
{
)
(
*
corev1
.
PodSpec
,
error
)
{
if
len
(
dynamoDeployment
.
Spec
.
Envs
)
>
0
{
if
len
(
dynamoDeployment
.
Spec
.
Envs
)
>
0
{
component
.
Envs
=
MergeEnvs
(
dynamoDeployment
.
Spec
.
Envs
,
component
.
Envs
)
component
.
Envs
=
MergeEnvs
(
dynamoDeployment
.
Spec
.
Envs
,
component
.
Envs
)
}
}
podSpec
,
err
:=
GenerateBasePodSpec
(
component
,
backendFramework
,
secretsRetriever
,
dynamoDeployment
.
Name
,
dynamoDeployment
.
Namespace
,
role
,
numberOfNodes
,
controllerConfig
,
multinodeDeploymentType
,
serviceName
)
podSpec
,
err
:=
GenerateBasePodSpec
(
component
,
backendFramework
,
secretsRetriever
,
dynamoDeployment
.
Name
,
dynamoDeployment
.
Namespace
,
role
,
numberOfNodes
,
controllerConfig
,
multinodeDeploymentType
,
serviceName
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
err
return
nil
,
err
}
}
return
podSpec
,
nil
return
podSpec
,
nil
}
}
...
@@ -871,6 +872,7 @@ func GenerateGrovePodGangSet(
...
@@ -871,6 +872,7 @@ func GenerateGrovePodGangSet(
gangSet
.
Spec
.
Template
.
HeadlessServiceConfig
=
&
grovev1alpha1
.
HeadlessServiceConfig
{
gangSet
.
Spec
.
Template
.
HeadlessServiceConfig
=
&
grovev1alpha1
.
HeadlessServiceConfig
{
PublishNotReadyAddresses
:
true
,
PublishNotReadyAddresses
:
true
,
}
}
gangSet
.
Spec
.
Template
.
StartupType
=
ptr
.
To
(
grovev1alpha1
.
CliqueStartupTypeAnyOrder
)
if
controllerConfig
.
Grove
.
TerminationDelay
>
0
{
if
controllerConfig
.
Grove
.
TerminationDelay
>
0
{
gangSet
.
Spec
.
Template
.
TerminationDelay
=
&
metav1
.
Duration
{
Duration
:
controllerConfig
.
Grove
.
TerminationDelay
}
gangSet
.
Spec
.
Template
.
TerminationDelay
=
&
metav1
.
Duration
{
Duration
:
controllerConfig
.
Grove
.
TerminationDelay
}
}
}
...
@@ -911,9 +913,10 @@ func GenerateGrovePodGangSet(
...
@@ -911,9 +913,10 @@ func GenerateGrovePodGangSet(
clique
:=
&
grovev1alpha1
.
PodCliqueTemplateSpec
{
clique
:=
&
grovev1alpha1
.
PodCliqueTemplateSpec
{
Name
:
strings
.
ToLower
(
r
.
Name
),
Name
:
strings
.
ToLower
(
r
.
Name
),
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
strings
.
ToLower
(
r
.
Name
),
RoleName
:
strings
.
ToLower
(
r
.
Name
),
Replicas
:
r
.
Replicas
,
Replicas
:
r
.
Replicas
,
PodSpec
:
podSpec
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
*
podSpec
,
},
},
}
}
labels
,
err
:=
generateLabels
(
component
,
dynamoDeployment
,
r
.
Name
)
labels
,
err
:=
generateLabels
(
component
,
dynamoDeployment
,
r
.
Name
)
...
@@ -935,9 +938,10 @@ func GenerateGrovePodGangSet(
...
@@ -935,9 +938,10 @@ func GenerateGrovePodGangSet(
if
isMultinode
{
if
isMultinode
{
scalingGroups
=
append
(
scalingGroups
,
grovev1alpha1
.
PodCliqueScalingGroupConfig
{
scalingGroups
=
append
(
scalingGroups
,
grovev1alpha1
.
PodCliqueScalingGroupConfig
{
Name
:
strings
.
ToLower
(
serviceName
),
Name
:
strings
.
ToLower
(
serviceName
),
CliqueNames
:
cliqueNames
,
CliqueNames
:
cliqueNames
,
Replicas
:
component
.
Replicas
,
Replicas
:
component
.
Replicas
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
})
})
}
}
}
}
...
@@ -945,7 +949,7 @@ func GenerateGrovePodGangSet(
...
@@ -945,7 +949,7 @@ func GenerateGrovePodGangSet(
gangSet
.
Spec
.
Template
.
PodCliqueScalingGroupConfigs
=
scalingGroups
gangSet
.
Spec
.
Template
.
PodCliqueScalingGroupConfigs
=
scalingGroups
}
}
return
gangSet
,
nil
return
controller_common
.
CanonicalizePodGangSet
(
gangSet
)
,
nil
}
}
func
generateLabels
(
component
*
v1alpha1
.
DynamoComponentDeploymentOverridesSpec
,
dynamoDeployment
*
v1alpha1
.
DynamoGraphDeployment
,
componentName
string
)
(
map
[
string
]
string
,
error
)
{
func
generateLabels
(
component
*
v1alpha1
.
DynamoComponentDeploymentOverridesSpec
,
dynamoDeployment
*
v1alpha1
.
DynamoGraphDeployment
,
componentName
string
)
(
map
[
string
]
string
,
error
)
{
...
@@ -1141,7 +1145,7 @@ func GenerateBasePodSpecForController(
...
@@ -1141,7 +1145,7 @@ func GenerateBasePodSpecForController(
controllerConfig
controller_common
.
Config
,
controllerConfig
controller_common
.
Config
,
role
Role
,
role
Role
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
multinodeDeploymentType
commonconsts
.
MultinodeDeploymentType
,
)
(
corev1
.
PodSpec
,
error
)
{
)
(
*
corev1
.
PodSpec
,
error
)
{
// Convert to our interface
// Convert to our interface
componentSpec
:=
ConvertDynamoComponentDeploymentToSpec
(
dynComponent
)
componentSpec
:=
ConvertDynamoComponentDeploymentToSpec
(
dynComponent
)
...
@@ -1150,7 +1154,7 @@ func GenerateBasePodSpecForController(
...
@@ -1150,7 +1154,7 @@ func GenerateBasePodSpecForController(
// Determine backend framework using hybrid approach
// Determine backend framework using hybrid approach
backendFramework
,
err
:=
getBackendFrameworkFromDynamoComponent
(
dynComponent
)
backendFramework
,
err
:=
getBackendFrameworkFromDynamoComponent
(
dynComponent
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
fmt
.
Errorf
(
"failed to determine backend framework: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to determine backend framework: %w"
,
err
)
}
}
// Generate base PodSpec with standard env vars using merged component envs
// Generate base PodSpec with standard env vars using merged component envs
...
@@ -1169,7 +1173,7 @@ func GenerateBasePodSpecForController(
...
@@ -1169,7 +1173,7 @@ func GenerateBasePodSpecForController(
serviceName
,
serviceName
,
)
)
if
err
!=
nil
{
if
err
!=
nil
{
return
corev1
.
PodSpec
{}
,
err
return
nil
,
err
}
}
return
podSpec
,
nil
return
podSpec
,
nil
...
...
deploy/cloud/operator/internal/dynamo/graph_test.go
View file @
94876e58
...
@@ -1205,6 +1205,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1205,6 +1205,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
Spec
:
grovev1alpha1
.
PodGangSetSpec
{
Spec
:
grovev1alpha1
.
PodGangSetSpec
{
Replicas
:
1
,
Replicas
:
1
,
Template
:
grovev1alpha1
.
PodGangSetTemplateSpec
{
Template
:
grovev1alpha1
.
PodGangSetTemplateSpec
{
StartupType
:
ptr
.
To
(
grovev1alpha1
.
CliqueStartupTypeAnyOrder
),
HeadlessServiceConfig
:
&
grovev1alpha1
.
HeadlessServiceConfig
{
HeadlessServiceConfig
:
&
grovev1alpha1
.
HeadlessServiceConfig
{
PublishNotReadyAddresses
:
true
,
PublishNotReadyAddresses
:
true
,
},
},
...
@@ -1224,8 +1225,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1224,8 +1225,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"nvidia.com/annotation2"
:
"annotation2"
,
"nvidia.com/annotation2"
:
"annotation2"
,
},
},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"frontend"
,
RoleName
:
"frontend"
,
Replicas
:
1
,
Replicas
:
1
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
...
@@ -1244,6 +1246,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1244,6 +1246,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
Name
:
"frontend-secret"
,
Name
:
"frontend-secret"
,
},
},
},
},
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -1353,8 +1356,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1353,8 +1356,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
Annotations
:
map
[
string
]
string
{},
Annotations
:
map
[
string
]
string
{},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"planner"
,
RoleName
:
"planner"
,
Replicas
:
2
,
Replicas
:
2
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
...
@@ -1375,6 +1379,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1375,6 +1379,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
},
},
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -1690,10 +1696,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1690,10 +1696,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"worker-ldr"
,
"worker-ldr"
,
"worker-wkr"
,
"worker-wkr"
,
},
},
Replicas
:
ptr
.
To
(
int32
(
5
)),
Replicas
:
ptr
.
To
(
int32
(
5
)),
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
},
},
},
},
// StartupType: ptr.To(grovev1alpha1.CliqueStartupTypeExplicit),
// StartupType: ptr.To(grovev1alpha1.CliqueStartupTypeExplicit),
StartupType
:
ptr
.
To
(
grovev1alpha1
.
CliqueStartupTypeAnyOrder
),
Cliques
:
[]
*
grovev1alpha1
.
PodCliqueTemplateSpec
{
Cliques
:
[]
*
grovev1alpha1
.
PodCliqueTemplateSpec
{
{
{
Name
:
"worker-ldr"
,
Name
:
"worker-ldr"
,
...
@@ -1709,9 +1717,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1709,9 +1717,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"nvidia.com/annotation2"
:
"annotation2"
,
"nvidia.com/annotation2"
:
"annotation2"
,
},
},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"worker-ldr"
,
RoleName
:
"worker-ldr"
,
Replicas
:
1
,
Replicas
:
1
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
Name
:
"shared-memory"
,
Name
:
"shared-memory"
,
...
@@ -1855,10 +1866,13 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1855,10 +1866,13 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"nvidia.com/annotation2"
:
"annotation2"
,
"nvidia.com/annotation2"
:
"annotation2"
,
},
},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"worker-wkr"
,
RoleName
:
"worker-wkr"
,
Replicas
:
2
,
Replicas
:
2
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
// StartsAfter: []string{"worker-ldr"},
// StartsAfter: []string{"worker-ldr"},
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
Name
:
"shared-memory"
,
Name
:
"shared-memory"
,
...
@@ -1961,8 +1975,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1961,8 +1975,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
Annotations
:
map
[
string
]
string
{},
Annotations
:
map
[
string
]
string
{},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"frontend"
,
RoleName
:
"frontend"
,
Replicas
:
1
,
Replicas
:
1
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
...
@@ -1981,6 +1996,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -1981,6 +1996,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
10
)),
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
10
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -2090,9 +2106,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2090,9 +2106,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
Annotations
:
map
[
string
]
string
{},
Annotations
:
map
[
string
]
string
{},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"planner"
,
RoleName
:
"planner"
,
Replicas
:
2
,
Replicas
:
2
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
Name
:
"planner-pvc"
,
Name
:
"planner-pvc"
,
...
@@ -2440,6 +2459,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2440,6 +2459,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
Spec
:
grovev1alpha1
.
PodGangSetSpec
{
Spec
:
grovev1alpha1
.
PodGangSetSpec
{
Replicas
:
1
,
Replicas
:
1
,
Template
:
grovev1alpha1
.
PodGangSetTemplateSpec
{
Template
:
grovev1alpha1
.
PodGangSetTemplateSpec
{
StartupType
:
ptr
.
To
(
grovev1alpha1
.
CliqueStartupTypeAnyOrder
),
HeadlessServiceConfig
:
&
grovev1alpha1
.
HeadlessServiceConfig
{
HeadlessServiceConfig
:
&
grovev1alpha1
.
HeadlessServiceConfig
{
PublishNotReadyAddresses
:
true
,
PublishNotReadyAddresses
:
true
,
},
},
...
@@ -2451,7 +2471,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2451,7 +2471,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"worker-ldr"
,
"worker-ldr"
,
"worker-wkr"
,
"worker-wkr"
,
},
},
Replicas
:
ptr
.
To
(
int32
(
5
)),
Replicas
:
ptr
.
To
(
int32
(
5
)),
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
},
},
},
},
// StartupType: ptr.To(grovev1alpha1.CliqueStartupTypeExplicit),
// StartupType: ptr.To(grovev1alpha1.CliqueStartupTypeExplicit),
...
@@ -2470,8 +2491,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2470,8 +2491,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"nvidia.com/annotation2"
:
"annotation2"
,
"nvidia.com/annotation2"
:
"annotation2"
,
},
},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"worker-ldr"
,
RoleName
:
"worker-ldr"
,
Replicas
:
1
,
Replicas
:
1
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
...
@@ -2484,6 +2506,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2484,6 +2506,8 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
},
},
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -2604,10 +2628,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2604,10 +2628,12 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
"nvidia.com/annotation2"
:
"annotation2"
,
"nvidia.com/annotation2"
:
"annotation2"
,
},
},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"worker-wkr"
,
RoleName
:
"worker-wkr"
,
Replicas
:
2
,
Replicas
:
2
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
// StartsAfter: []string{"worker-ldr"},
// StartsAfter: []string{"worker-ldr"},
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
Name
:
"shared-memory"
,
Name
:
"shared-memory"
,
...
@@ -2619,6 +2645,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2619,6 +2645,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
},
},
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -2710,8 +2737,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2710,8 +2737,9 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
Annotations
:
map
[
string
]
string
{},
Annotations
:
map
[
string
]
string
{},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"frontend"
,
RoleName
:
"frontend"
,
Replicas
:
1
,
Replicas
:
1
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
...
@@ -2730,6 +2758,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2730,6 +2758,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
10
)),
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
10
)),
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
@@ -2839,9 +2868,11 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2839,9 +2868,11 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
Annotations
:
map
[
string
]
string
{},
Annotations
:
map
[
string
]
string
{},
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
Spec
:
grovev1alpha1
.
PodCliqueSpec
{
RoleName
:
"planner"
,
RoleName
:
"planner"
,
Replicas
:
2
,
Replicas
:
2
,
MinAvailable
:
ptr
.
To
(
int32
(
1
)),
PodSpec
:
corev1
.
PodSpec
{
PodSpec
:
corev1
.
PodSpec
{
TerminationGracePeriodSeconds
:
ptr
.
To
(
int64
(
60
)),
Volumes
:
[]
corev1
.
Volume
{
Volumes
:
[]
corev1
.
Volume
{
{
{
Name
:
"planner-pvc"
,
Name
:
"planner-pvc"
,
...
@@ -2861,6 +2892,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
...
@@ -2861,6 +2892,7 @@ func TestGenerateGrovePodGangSet(t *testing.T) {
},
},
},
},
},
},
RestartPolicy
:
corev1
.
RestartPolicyAlways
,
Containers
:
[]
corev1
.
Container
{
Containers
:
[]
corev1
.
Container
{
{
{
Name
:
"main"
,
Name
:
"main"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment