Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPV6 APIVIP Handling #589

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion pkg/combustion/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func ComponentHelmCharts(ctx *image.Context) ([]image.HelmChart, []image.HelmRep
var charts []image.HelmChart
var repos []image.HelmRepository

if ctx.ImageDefinition.Kubernetes.Network.APIVIP4 != "" || ctx.ImageDefinition.Kubernetes.Network.APIVIP6 != "" {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP != "" {
metalLBChart := image.HelmChart{
Name: ctx.ArtifactSources.MetalLB.Chart,
RepositoryName: metallbRepositoryName,
Expand Down
32 changes: 19 additions & 13 deletions pkg/combustion/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package combustion
import (
_ "embed"
"fmt"
"net/netip"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -147,8 +148,7 @@ func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluste

templateValues := map[string]any{
"installScript": installScript,
"apiVIP4": ctx.ImageDefinition.Kubernetes.Network.APIVIP4,
"apiVIP6": ctx.ImageDefinition.Kubernetes.Network.APIVIP6,
"apiVIP": ctx.ImageDefinition.Kubernetes.Network.APIVIP,
"apiHost": ctx.ImageDefinition.Kubernetes.Network.APIHost,
"binaryPath": binaryPath,
"imagesPath": imagesPath,
Expand All @@ -159,7 +159,7 @@ func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluste

singleNode := len(ctx.ImageDefinition.Kubernetes.Nodes) < 2
if singleNode {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP4 == "" && ctx.ImageDefinition.Kubernetes.Network.APIVIP6 == "" {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP == "" {
zap.S().Info("Virtual IP address for k3s cluster is not provided and will not be configured")
} else {
log.Audit("WARNING: A Virtual IP address for the k3s cluster has been provided. " +
Expand Down Expand Up @@ -243,8 +243,7 @@ func (c *Combustion) configureRKE2(ctx *image.Context, cluster *kubernetes.Clust

templateValues := map[string]any{
"installScript": installScript,
"apiVIP4": ctx.ImageDefinition.Kubernetes.Network.APIVIP4,
"apiVIP6": ctx.ImageDefinition.Kubernetes.Network.APIVIP6,
"apiVIP": ctx.ImageDefinition.Kubernetes.Network.APIVIP,
"apiHost": ctx.ImageDefinition.Kubernetes.Network.APIHost,
"installPath": installPath,
"imagesPath": imagesPath,
Expand All @@ -255,7 +254,7 @@ func (c *Combustion) configureRKE2(ctx *image.Context, cluster *kubernetes.Clust

singleNode := len(ctx.ImageDefinition.Kubernetes.Nodes) < 2
if singleNode {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP4 == "" && ctx.ImageDefinition.Kubernetes.Network.APIVIP6 == "" {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP == "" {
zap.S().Info("Virtual IP address for RKE2 cluster is not provided and will not be configured")
}

Expand Down Expand Up @@ -318,14 +317,21 @@ func (c *Combustion) downloadRKE2Artefacts(ctx *image.Context, cluster *kubernet
}

func kubernetesVIPManifest(k *image.Kubernetes) (string, error) {
ip, err := netip.ParseAddr(k.Network.APIVIP)
if err != nil {
return "", fmt.Errorf("parsing kubernetes APIVIP address: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should read as apiVIP instead of all caps, since that's what the field is named in the definition. But I don't feel particularly strongly if you decide to keep it like this.

}

manifest := struct {
APIAddress4 string
APIAddress6 string
RKE2 bool
APIAddress string
IsIPV4 bool
IsIPV6 bool
RKE2 bool
}{
APIAddress4: k.Network.APIVIP4,
APIAddress6: k.Network.APIVIP6,
RKE2: strings.Contains(k.Version, image.KubernetesDistroRKE2),
APIAddress: k.Network.APIVIP,
IsIPV4: ip.Is4(),
IsIPV6: ip.Is6(),
RKE2: strings.Contains(k.Version, image.KubernetesDistroRKE2),
}

return template.Parse("k8s-vip", k8sVIPManifest, &manifest)
Expand Down Expand Up @@ -371,7 +377,7 @@ func (c *Combustion) configureManifests(ctx *image.Context) (string, error) {
manifestsPath := localKubernetesManifestsPath()
manifestDestDir := filepath.Join(ctx.ArtefactsDir, manifestsPath)

if ctx.ImageDefinition.Kubernetes.Network.APIVIP4 != "" || ctx.ImageDefinition.Kubernetes.Network.APIVIP6 != "" {
if ctx.ImageDefinition.Kubernetes.Network.APIVIP != "" {
if err := os.MkdirAll(manifestDestDir, os.ModePerm); err != nil {
return "", fmt.Errorf("creating manifests destination dir: %w", err)
}
Expand Down
54 changes: 49 additions & 5 deletions pkg/combustion/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func TestConfigureKubernetes_SuccessfulSingleNodeK3sCluster(t *testing.T) {
ctx.ImageDefinition.Kubernetes = image.Kubernetes{
Version: "v1.30.3+k3s1",
Network: image.Network{
APIVIP4: "192.168.122.100",
APIVIP: "192.168.122.100",
APIHost: "api.cluster01.hosted.on.edge.suse.com",
},
}
Expand Down Expand Up @@ -293,7 +293,7 @@ func TestConfigureKubernetes_SuccessfulMultiNodeK3sCluster(t *testing.T) {
Version: "v1.30.3+k3s1",
Network: image.Network{
APIHost: "api.cluster01.hosted.on.edge.suse.com",
APIVIP4: "192.168.122.100",
APIVIP: "192.168.122.100",
},
Nodes: []image.Node{
{
Expand Down Expand Up @@ -423,7 +423,7 @@ func TestConfigureKubernetes_SuccessfulSingleNodeRKE2Cluster(t *testing.T) {
ctx.ImageDefinition.Kubernetes = image.Kubernetes{
Version: "v1.30.3+rke2r1",
Network: image.Network{
APIVIP4: "192.168.122.100",
APIVIP: "192.168.122.100",
APIHost: "api.cluster01.hosted.on.edge.suse.com",
},
}
Expand Down Expand Up @@ -492,7 +492,7 @@ func TestConfigureKubernetes_SuccessfulMultiNodeRKE2Cluster(t *testing.T) {
Version: "v1.30.3+rke2r1",
Network: image.Network{
APIHost: "api.cluster01.hosted.on.edge.suse.com",
APIVIP4: "192.168.122.100",
APIVIP: "192.168.122.100",
},
Nodes: []image.Node{
{
Expand Down Expand Up @@ -734,7 +734,7 @@ func TestConfigureKubernetes_SuccessfulRKE2ServerWithManifests(t *testing.T) {
ctx.ImageDefinition.Kubernetes = image.Kubernetes{
Version: "v1.30.3+rke2r1",
Network: image.Network{
APIVIP4: "192.168.122.100",
APIVIP: "192.168.122.100",
APIHost: "api.cluster01.hosted.on.edge.suse.com",
},
}
Expand Down Expand Up @@ -821,3 +821,47 @@ func TestConfigureKubernetes_SuccessfulRKE2ServerWithManifests(t *testing.T) {
assert.Contains(t, contents, "name: my-nginx")
assert.Contains(t, contents, "image: nginx:1.14.2")
}

func TestKubernetesVIPManifestValidIPV4(t *testing.T) {
Copy link

@mchiappero mchiappero Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-picking, but I'd align these two names from "IPV4"/"IPV6" to IPv4 too, as the rest of the code/prints.

k8s := &image.Kubernetes{
Version: "v1.30.3+rke2r1",
Network: image.Network{
APIVIP: "192.168.1.1",
},
}

manifest, err := kubernetesVIPManifest(k8s)
require.NoError(t, err)

assert.Contains(t, manifest, "- 192.168.1.1/32")
assert.Contains(t, manifest, "- name: rke2-api")
}

func TestKubernetesVIPManifestValidIPV6(t *testing.T) {
k8s := &image.Kubernetes{
Version: "v1.30.3+k3s1",
Network: image.Network{
APIVIP: "fd12:3456:789a::21",
},
}

manifest, err := kubernetesVIPManifest(k8s)
require.NoError(t, err)
fmt.Println(manifest)
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved

assert.Contains(t, manifest, "- fd12:3456:789a::21/128")
assert.Contains(t, manifest, "- name: k8s-api")
assert.NotContains(t, manifest, "rke2")
}

func TestKubernetesVIPManifestInvalidIP(t *testing.T) {
k8s := &image.Kubernetes{
Version: "v1.30.3+k3s1",
Network: image.Network{
APIVIP: "1111",
},
}

_, err := kubernetesVIPManifest(k8s)
require.ErrorContains(t, err, "parsing kubernetes APIVIP address: ParseAddr(\"1111\"): unable to parse IP")
}
8 changes: 2 additions & 6 deletions pkg/combustion/templates/k3s-multi-node-installer.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,8 @@ systemctl enable kubernetes-resources-install.service
{{- end }}
fi

{{- if and .apiVIP4 .apiHost }}
echo "{{ .apiVIP4 }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

{{- if and .apiVIP6 .apiHost }}
echo "{{ .apiVIP6 }} {{ .apiHost }}" >> /etc/hosts
{{- if and .apiVIP .apiHost }}
echo "{{ .apiVIP }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

mkdir -p /etc/rancher/k3s/
Expand Down
8 changes: 2 additions & 6 deletions pkg/combustion/templates/k3s-single-node-installer.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,8 @@ EOF
systemctl enable kubernetes-resources-install.service
{{- end }}

{{- if and .apiVIP4 .apiHost }}
echo "{{ .apiVIP4 }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

{{- if and .apiVIP6 .apiHost }}
echo "{{ .apiVIP6 }} {{ .apiHost }}" >> /etc/hosts
{{- if and .apiVIP .apiHost }}
echo "{{ .apiVIP }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

mkdir -p /etc/rancher/k3s/
Expand Down
8 changes: 4 additions & 4 deletions pkg/combustion/templates/k8s-vip.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ metadata:
namespace: metallb-system
spec:
addresses:
{{- if .APIAddress4 }}
- {{ .APIAddress4 }}/32
{{- if .IsIPV4 }}
- {{ .APIAddress }}/32
{{- end }}
{{- if .APIAddress6 }}
- {{ .APIAddress6 }}/128
{{- if .IsIPV6 }}
- {{ .APIAddress }}/128
{{- end }}
avoidBuggyIPs: true
serviceAllocation:
Expand Down
9 changes: 2 additions & 7 deletions pkg/combustion/templates/rke2-multi-node-installer.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,9 @@ systemctl enable kubernetes-resources-install.service
{{- end }}
fi

{{- if and .apiVIP4 .apiHost }}
echo "{{ .apiVIP4 }} {{ .apiHost }}" >> /etc/hosts
{{- if and .apiVIP .apiHost }}
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
echo "{{ .apiVIP }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

{{- if and .apiVIP6 .apiHost }}
echo "{{ .apiVIP6 }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
mkdir -p /etc/rancher/rke2/
cp $CONFIGFILE /etc/rancher/rke2/config.yaml

Expand Down
8 changes: 2 additions & 6 deletions pkg/combustion/templates/rke2-single-node-installer.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,8 @@ EOF
systemctl enable kubernetes-resources-install.service
{{- end }}

{{- if and .apiVIP4 .apiHost }}
echo "{{ .apiVIP4 }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

{{- if and .apiVIP6 .apiHost }}
echo "{{ .apiVIP6 }} {{ .apiHost }}" >> /etc/hosts
{{- if and .apiVIP .apiHost }}
echo "{{ .apiVIP }} {{ .apiHost }}" >> /etc/hosts
{{- end }}

mkdir -p /etc/rancher/rke2/
Expand Down
3 changes: 1 addition & 2 deletions pkg/image/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ type Kubernetes struct {

type Network struct {
APIHost string `yaml:"apiHost"`
APIVIP4 string `yaml:"apiVIP"`
APIVIP6 string `yaml:"apiVIP6"`
APIVIP string `yaml:"apiVIP"`
}

type Node struct {
Expand Down
3 changes: 1 addition & 2 deletions pkg/image/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,7 @@ func TestParse(t *testing.T) {
assert.Equal(t, "v1.30.3+rke2r1", kubernetes.Version)

// Network
assert.Equal(t, "192.168.122.100", kubernetes.Network.APIVIP4)
assert.Equal(t, "fd12:3456:789a::21", kubernetes.Network.APIVIP6)
assert.Equal(t, "192.168.122.100", kubernetes.Network.APIVIP)
assert.Equal(t, "api.cluster01.hosted.on.edge.suse.com", kubernetes.Network.APIHost)

// Nodes
Expand Down
1 change: 0 additions & 1 deletion pkg/image/testdata/full-valid-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ kubernetes:
version: v1.30.3+rke2r1
network:
apiVIP: 192.168.122.100
apiVIP6: fd12:3456:789a::21
apiHost: api.cluster01.hosted.on.edge.suse.com
nodes:
- hostname: node1.suse.com
Expand Down
45 changes: 10 additions & 35 deletions pkg/image/validation/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,62 +115,37 @@ func validateNodes(k8s *image.Kubernetes) []FailedValidation {

func validateNetwork(k8s *image.Kubernetes) []FailedValidation {
var failures []FailedValidation
ip := k8s.Network.APIVIP
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved

numNodes := len(k8s.Nodes)
if numNodes <= 1 {
// Single node cluster, node configurations are not required
return failures
}

ip4 := k8s.Network.APIVIP4
ip6 := k8s.Network.APIVIP6

if ip4 == "" && ip6 == "" {
if ip == "" {
failures = append(failures, FailedValidation{
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
UserMessage: "The 'apiVIP' or 'apiVIP6' field is required in the 'network' section when defining entries under 'nodes'.",
UserMessage: "The 'apiVIP' field is required in the 'network' section when defining entries under 'nodes'.",
})
}

if ip4 != "" {
ip, err := netip.ParseAddr(ip4)
if err != nil {
failures = append(failures, FailedValidation{
UserMessage: fmt.Sprintf("Invalid IPV4 address %q for field 'apiVIP'.", ip4),
Error: err,
})
}

if !ip.Is4() {
failures = append(failures, FailedValidation{
UserMessage: fmt.Sprintf("%q is not an IPV4 address, only IPV4 addresses are valid for field 'apiVIP'.", ip4),
})
}

if !ip.IsGlobalUnicast() {
msg := fmt.Sprintf("Invalid non-unicast cluster API address (%s) for field 'apiVIP'.", ip4)
failures = append(failures, FailedValidation{
UserMessage: msg,
})
}
}

if ip6 != "" {
ip, err := netip.ParseAddr(ip6)
if ip != "" {
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
parsedIP, err := netip.ParseAddr(ip)
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
failures = append(failures, FailedValidation{
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
UserMessage: fmt.Sprintf("Invalid IPV6 address %q for field 'apiVIP6'.", ip6),
UserMessage: fmt.Sprintf("Invalid APIVIP address %q for field 'apiVIP'.", ip),
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
Error: err,
})
}

if !ip.Is6() {
if !parsedIP.Is4() && !parsedIP.Is6() {
failures = append(failures, FailedValidation{
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
UserMessage: fmt.Sprintf("%q is not an IPV6 address, only IPV6 addresses are valid for field 'apiVIP6'.", ip6),
UserMessage: fmt.Sprintf("%q is not an IPV4 or IPV6 address, only IPV4 and IPV6 addresses are valid for field 'apiVIP'.", ip),
atanasdinov marked this conversation as resolved.
Show resolved Hide resolved
})
}

if !ip.IsGlobalUnicast() {
msg := fmt.Sprintf("Invalid non-unicast cluster API address (%s) for field 'apiVIP6'.", ip6)
if !parsedIP.IsGlobalUnicast() {
msg := fmt.Sprintf("Invalid non-unicast cluster API address (%s) for field 'apiVIP'.", ip)
failures = append(failures, FailedValidation{
UserMessage: msg,
})
Expand Down
Loading