Skip to content

Commit d7c2895

Browse files
authored
Ensure that pods are replaced if we change the Pod IP family (#2247)
* Ensure that pods are replaced if we change the Pod IP family
1 parent b431f35 commit d7c2895

File tree

9 files changed

+691
-60
lines changed

9 files changed

+691
-60
lines changed

api/v1beta2/foundationdb_labels.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const (
4040
// gets its public IP from.
4141
PublicIPSourceAnnotation = "foundationdb.org/public-ip-source"
4242

43+
// IPFamilyAnnotation is an annotation key that specifies the IP family of the pod.
44+
IPFamilyAnnotation = "foundationdb.org/ip-family"
45+
4346
// PublicIPAnnotation is an annotation key that specifies the current public
4447
// IP for a pod.
4548
PublicIPAnnotation = "foundationdb.org/public-ip"

api/v1beta2/foundationdbcluster_types.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,8 +2138,7 @@ type RoutingConfig struct {
21382138

21392139
// PodIPFamily tells the pod which family of IP addresses to use.
21402140
// You can use 4 to represent IPv4, and 6 to represent IPv6.
2141-
// This feature is only supported in FDB 7.0 or later, and requires
2142-
// dual-stack support in your Kubernetes environment.
2141+
// This feature requires dual-stack support in your Kubernetes environment.
21432142
PodIPFamily *int `json:"podIPFamily,omitempty"`
21442143

21452144
// UseDNSInClusterFile determines whether to use DNS names rather than IP
@@ -2941,6 +2940,13 @@ func (cluster *FoundationDBCluster) Validate() error {
29412940
}
29422941
}
29432942

2943+
if cluster.Spec.Routing.PodIPFamily != nil {
2944+
ipFamily := *cluster.Spec.Routing.PodIPFamily
2945+
if ipFamily != 4 && ipFamily != 6 {
2946+
validations = append(validations, fmt.Sprintf("Pod IP Family %d is not valid, only 4 or 6 are allowed.", ipFamily))
2947+
}
2948+
}
2949+
29442950
if len(validations) == 0 {
29452951
return nil
29462952
}
@@ -3086,9 +3092,16 @@ func (cluster *FoundationDBCluster) GetProcessGroupID(processClass ProcessClass,
30863092
return fmt.Sprintf("%s-%s-%d", cluster.Name, processClass.GetProcessClassForPodName(), idNum), processGroupID
30873093
}
30883094

3095+
var (
3096+
// PodIPFamilyIPv4 represents the desired IP family as IPv4.
3097+
PodIPFamilyIPv4 = 4
3098+
// PodIPFamilyIPv6 represents the desired IP family as IPv6.
3099+
PodIPFamilyIPv6 = 6
3100+
)
3101+
30893102
// IsPodIPFamily6 determines whether the podIPFamily setting in cluster is set to use the IPv6 family.
30903103
func (cluster *FoundationDBCluster) IsPodIPFamily6() bool {
3091-
return pointer.IntDeref(cluster.Spec.Routing.PodIPFamily, 4) == 6
3104+
return pointer.IntDeref(cluster.Spec.Routing.PodIPFamily, PodIPFamilyIPv4) == PodIPFamilyIPv6
30923105
}
30933106

30943107
// ProcessSharesDC returns true if the process's locality matches the cluster's Datacenter.

docs/cluster_spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ RoutingConfig allows configuring routing to our pods, and services that sit in f
468468
| ----- | ----------- | ------ | -------- |
469469
| headlessService | Headless determines whether we want to run a headless service for the cluster. | *bool | false |
470470
| publicIPSource | PublicIPSource specifies what source a process should use to get its public IPs. This supports the values `pod` and `service`. | *[PublicIPSource](#publicipsource) | false |
471-
| podIPFamily | PodIPFamily tells the pod which family of IP addresses to use. You can use 4 to represent IPv4, and 6 to represent IPv6. This feature is only supported in FDB 7.0 or later, and requires dual-stack support in your Kubernetes environment. | *int | false |
471+
| podIPFamily | PodIPFamily tells the pod which family of IP addresses to use. You can use 4 to represent IPv4, and 6 to represent IPv6. This feature requires dual-stack support in your Kubernetes environment. | *int | false |
472472
| useDNSInClusterFile | UseDNSInClusterFile determines whether to use DNS names rather than IP addresses to identify coordinators in the cluster file. This requires FoundationDB 7.0+. | *bool | false |
473473
| defineDNSLocalityFields | DefineDNSLocalityFields determines whether to define pod DNS names on pod specs and provide them in the locality arguments to fdbserver. This is ignored if UseDNSInCluster is true. | *bool | false |
474474
| dnsDomain | DNSDomain defines the cluster domain used in a DNS name generated for a service. The default is `cluster.local`. | *string | false |

e2e/test_operator/operator_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"fmt"
3737
"log"
3838
"math"
39+
"strconv"
3940
"strings"
4041
"time"
4142

@@ -2737,4 +2738,44 @@ var _ = Describe("Operator", Label("e2e", "pr"), func() {
27372738
})
27382739
})
27392740
})
2741+
2742+
When("the pod IP family is changed", func() {
2743+
var testStartTime time.Time
2744+
// TODO (johscheuer): Make the IP family configurable in the future.
2745+
var podIPFamily = fdbv1beta2.PodIPFamilyIPv4
2746+
2747+
BeforeEach(func() {
2748+
testStartTime = time.Now()
2749+
spec := fdbCluster.GetCluster().Spec.DeepCopy()
2750+
spec.Routing.PodIPFamily = pointer.Int(podIPFamily)
2751+
fdbCluster.UpdateClusterSpecWithSpec(spec)
2752+
Expect(fdbCluster.WaitForReconciliation()).To(Succeed())
2753+
})
2754+
2755+
AfterEach(func() {
2756+
spec := fdbCluster.GetCluster().Spec.DeepCopy()
2757+
spec.Routing.PodIPFamily = nil
2758+
fdbCluster.UpdateClusterSpecWithSpec(spec)
2759+
Expect(fdbCluster.WaitForReconciliation(fixtures.SoftReconcileOption(true))).To(Succeed())
2760+
})
2761+
2762+
It("should replace all pods and configure them properly", func() {
2763+
pods := fdbCluster.GetPods()
2764+
podIPFamilyString := strconv.Itoa(podIPFamily)
2765+
2766+
for _, pod := range pods.Items {
2767+
if !pod.DeletionTimestamp.IsZero() {
2768+
continue
2769+
}
2770+
2771+
if pod.Status.Phase != corev1.PodRunning {
2772+
log.Println("ignoring pod:", pod.Name, "with pod phase", pod.Status.Phase, "message:", pod.Status.Message)
2773+
continue
2774+
}
2775+
2776+
Expect(pod.CreationTimestamp.After(testStartTime)).To(BeTrue())
2777+
Expect(pod.Annotations).To(HaveKeyWithValue(fdbv1beta2.IPFamilyAnnotation, podIPFamilyString))
2778+
}
2779+
})
2780+
})
27402781
})

internal/pod_helper.go

Lines changed: 106 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,72 +25,57 @@ import (
2525
"encoding/hex"
2626
"encoding/json"
2727
"fmt"
28+
monitorapi "github.com/apple/foundationdb/fdbkubernetesmonitor/api"
2829
"net"
2930
"strconv"
3031

31-
"github.com/go-logr/logr"
32-
33-
"k8s.io/utils/pointer"
34-
3532
fdbv1beta2 "github.com/FoundationDB/fdb-kubernetes-operator/v2/api/v1beta2"
33+
"github.com/go-logr/logr"
3634
corev1 "k8s.io/api/core/v1"
3735
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36+
"k8s.io/utils/pointer"
3837
"sigs.k8s.io/controller-runtime/pkg/client"
3938
)
4039

4140
// GetPublicIPsForPod returns the public IPs for a Pod
4241
func GetPublicIPsForPod(pod *corev1.Pod, log logr.Logger) []string {
43-
var podIPFamily *int
44-
4542
if pod == nil {
4643
return []string{}
4744
}
4845

49-
for _, container := range pod.Spec.Containers {
50-
if container.Name != fdbv1beta2.SidecarContainerName {
51-
continue
52-
}
53-
for indexOfArgument, argument := range container.Args {
54-
if argument == "--public-ip-family" && indexOfArgument < len(container.Args)-1 {
55-
familyString := container.Args[indexOfArgument+1]
56-
family, err := strconv.Atoi(familyString)
57-
if err != nil {
58-
log.Error(err, "Error parsing public IP family", "family", familyString)
59-
return nil
60-
}
61-
podIPFamily = &family
62-
break
63-
}
64-
}
46+
podIPFamily, err := GetIPFamily(pod)
47+
if err != nil {
48+
log.Error(err, "Could not parse IP family")
49+
return []string{pod.Status.PodIP}
6550
}
6651

67-
if podIPFamily != nil {
68-
podIPs := pod.Status.PodIPs
69-
matchingIPs := make([]string, 0, len(podIPs))
52+
if podIPFamily == nil {
53+
return []string{pod.Status.PodIP}
54+
}
7055

71-
for _, podIP := range podIPs {
72-
ip := net.ParseIP(podIP.IP)
73-
if ip == nil {
74-
log.Error(nil, "Failed to parse IP from pod", "ip", podIP)
75-
continue
76-
}
77-
matches := false
78-
switch *podIPFamily {
79-
case 4:
80-
matches = ip.To4() != nil
81-
case 6:
82-
matches = ip.To4() == nil
83-
default:
84-
log.Error(nil, "Could not match IP address against IP family", "family", *podIPFamily)
85-
}
86-
if matches {
87-
matchingIPs = append(matchingIPs, podIP.IP)
88-
}
56+
podIPs := pod.Status.PodIPs
57+
matchingIPs := make([]string, 0, len(podIPs))
58+
for _, podIP := range podIPs {
59+
ip := net.ParseIP(podIP.IP)
60+
if ip == nil {
61+
log.Error(nil, "Failed to parse IP from pod", "ip", podIP)
62+
continue
63+
}
64+
matches := false
65+
switch *podIPFamily {
66+
case fdbv1beta2.PodIPFamilyIPv4:
67+
matches = ip.To4() != nil
68+
case fdbv1beta2.PodIPFamilyIPv6:
69+
matches = ip.To4() == nil
70+
default:
71+
log.Error(nil, "Could not match IP address against IP family", "family", *podIPFamily)
72+
}
73+
if matches {
74+
matchingIPs = append(matchingIPs, podIP.IP)
8975
}
90-
return matchingIPs
9176
}
9277

93-
return []string{pod.Status.PodIP}
78+
return matchingIPs
9479
}
9580

9681
// GetProcessGroupIDFromMeta fetches the process group ID from an object's metadata.
@@ -237,6 +222,82 @@ func GetPublicIPSource(pod *corev1.Pod) (fdbv1beta2.PublicIPSource, error) {
237222
return fdbv1beta2.PublicIPSource(source), nil
238223
}
239224

225+
// getIPFamilyFromPod returns the IP family from the pod configuration.
226+
func getIPFamilyFromPod(pod *corev1.Pod) (*int, error) {
227+
if GetImageType(pod) == fdbv1beta2.ImageTypeUnified {
228+
currentData, present := pod.Annotations[monitorapi.CurrentConfigurationAnnotation]
229+
if !present {
230+
return nil, fmt.Errorf("could not read current launcher configuration")
231+
}
232+
233+
currentConfiguration := monitorapi.ProcessConfiguration{}
234+
err := json.Unmarshal([]byte(currentData), &currentConfiguration)
235+
if err != nil {
236+
return nil, fmt.Errorf("could not parse current process configuration: %w", err)
237+
}
238+
239+
for _, argument := range currentConfiguration.Arguments {
240+
if argument.ArgumentType != monitorapi.IPListArgumentType {
241+
continue
242+
}
243+
244+
return validateIPFamily(argument.IPFamily)
245+
}
246+
247+
// No IP List setting is defined.
248+
return nil, nil
249+
}
250+
251+
for _, container := range pod.Spec.Containers {
252+
if container.Name != fdbv1beta2.SidecarContainerName {
253+
continue
254+
}
255+
256+
for indexOfArgument, argument := range container.Args {
257+
if argument == "--public-ip-family" && indexOfArgument < len(container.Args)-1 {
258+
return parseIPFamily(container.Args[indexOfArgument+1])
259+
}
260+
}
261+
}
262+
263+
return nil, nil
264+
}
265+
266+
// validateIPFamily will validate that the IP family is valid and return a pointer.
267+
func validateIPFamily(ipFamily int) (*int, error) {
268+
if ipFamily != fdbv1beta2.PodIPFamilyIPv4 && ipFamily != fdbv1beta2.PodIPFamilyIPv6 {
269+
return nil, fmt.Errorf("unsupported IP family %d", ipFamily)
270+
}
271+
272+
return pointer.Int(ipFamily), nil
273+
}
274+
275+
// parseIPFamily will convert a string into the IP family (int pointer) and validate that the value is a valid IP family.
276+
func parseIPFamily(ipFamilyValue string) (*int, error) {
277+
ipFamily, err := strconv.Atoi(ipFamilyValue)
278+
if err != nil {
279+
return nil, err
280+
}
281+
282+
return validateIPFamily(ipFamily)
283+
}
284+
285+
// GetIPFamily determines the IP family based on the annotation. If the annotation is not present the method will try to
286+
// get the ip family based on the pod spec.
287+
func GetIPFamily(pod *corev1.Pod) (*int, error) {
288+
if pod == nil {
289+
return nil, fmt.Errorf("failed to fetch IP family from nil Pod")
290+
}
291+
292+
ipFamilyValue, ok := pod.ObjectMeta.Annotations[fdbv1beta2.IPFamilyAnnotation]
293+
if ipFamilyValue != "" && ok {
294+
return parseIPFamily(ipFamilyValue)
295+
}
296+
297+
// If the annotation is missing, try to get the pod IP family from the pod spec.
298+
return getIPFamilyFromPod(pod)
299+
}
300+
240301
// PodHasSidecarTLS determines whether a pod currently has TLS enabled for the sidecar process.
241302
// This method should only be used for split images.
242303
func PodHasSidecarTLS(pod *corev1.Pod) bool {

0 commit comments

Comments
 (0)