Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/configuration-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ network:
apiVIP: 192.168.122.100
apiHost: api.cluster01.example.com
apiVIP6: fd12:3456:789a::21
apiVIPMode: managed
```

* `manifests` - Optional; Defines remote Kubernetes manifests to be deployed on the cluster.
Expand All @@ -170,8 +171,9 @@ network:
* `type` - Required; Selects the Kubernetes node type, either server (for control plane nodes) or agent (for worker nodes).
* `init` - Optional; Indicates which node should function as the cluster initializer. The initializer node is the server node which bootstraps the cluster and allows other nodes to join it. If unset, the first server in the node list will be selected as the initializer.
* `network`:
* `apiVIP` - Required for multi-node clusters if not using `apiVIP6`; Specifies the IPv4 address which will serve as the cluster LoadBalancer, backed by MetalLB.
* `apiVIP6` - Required for multi-node clusters if not using `apiVIP`; Specifies the IPv6 address which will serve as the cluster LoadBalancer, backed by MetalLB.
* `apiVIP` - Required for multi-node clusters if not using `apiVIP6`; Specifies the IPv4 address which will serve as the cluster LoadBalancer. By default, setting this field adds MetalLB and endpoint-copier-operator to the image build.
* `apiVIP6` - Required for multi-node clusters if not using `apiVIP`; Specifies the IPv6 address which will serve as the cluster LoadBalancer. By default, setting this field adds MetalLB and endpoint-copier-operator to the image build.
* `apiVIPMode` - Optional; Selects how the API VIP is provided. Omitted or `managed` keeps the default built-in MetalLB and endpoint-copier-operator install. `external` keeps `apiVIP` or `apiVIP6` configured but disables adding those built-in Helm charts at image build time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am wondering if instead of managed shouldn't we name it metallb. If in the future we add support for additional or different load balancers probably it will make more sense to name the stack.

@atanasdinov atanasdinov Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The specific value would read better if we had:

  • loadBalancer: metallb | external

I guess both the above and apiVIPMode: managed | external are fine.

* `apiHost` - Optional; Specifies the domain address for accessing the cluster.

### Kubernetes Directory
Expand Down
2 changes: 1 addition & 1 deletion internal/config/v0/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func parseKubernetes(f vfs.FS, configDir Dir, k *kubernetes.Kubernetes, r *relea
return fmt.Errorf("reading config file: %w", err)
}

if k.Network.APIVIP4 != "" || k.Network.APIVIP6 != "" {
if k.Network.IsHA() && k.Network.APIVIPMode != "external" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On the same idea I'd probably make an explicit match against the network mode rather than filter out the external.

containsChart := func(name string) bool {
return slices.ContainsFunc(r.Components.HelmCharts, func(c release.HelmChart) bool {
return c.Name == name
Expand Down
32 changes: 32 additions & 0 deletions internal/config/v0/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"path/filepath"
"slices"
"strings"
"testing"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -174,6 +175,37 @@ var _ = Describe("Configuration", Label("configuration"), func() {
}))
})

It("Adds managed HA load balancer charts when apiVIPMode is managed", func() {
clusterYAML := strings.Replace(kubernetesClusterYAML, " apiVIP: 192.168.120.100.sslip.io", " apiVIP: 192.168.120.100.sslip.io\n apiVIPMode: managed", 1)
Expect(fs.WriteFile(configDir.ClusterFilepath(), []byte(clusterYAML), 0644)).To(Succeed())

conf, err := Parse(fs, configDir)
Expect(err).ToNot(HaveOccurred())

Expect(containsChart("metallb", conf.Release.Components.HelmCharts)).To(BeTrue())
Expect(containsChart("endpoint-copier-operator", conf.Release.Components.HelmCharts)).To(BeTrue())
})

It("Skips managed HA load balancer charts when apiVIPMode is external", func() {
clusterYAML := strings.Replace(kubernetesClusterYAML, " apiVIP: 192.168.120.100.sslip.io", " apiVIP: 192.168.120.100.sslip.io\n apiVIPMode: external", 1)
Expect(fs.WriteFile(configDir.ClusterFilepath(), []byte(clusterYAML), 0644)).To(Succeed())

conf, err := Parse(fs, configDir)
Expect(err).ToNot(HaveOccurred())

Expect(containsChart("metallb", conf.Release.Components.HelmCharts)).To(BeFalse())
Expect(containsChart("endpoint-copier-operator", conf.Release.Components.HelmCharts)).To(BeFalse())
})

It("Fails to parse an invalid apiVIPMode", func() {
clusterYAML := strings.Replace(kubernetesClusterYAML, " apiVIP: 192.168.120.100.sslip.io", " apiVIP: 192.168.120.100.sslip.io\n apiVIPMode: invalid", 1)
Expect(fs.WriteFile(configDir.ClusterFilepath(), []byte(clusterYAML), 0644)).To(Succeed())

_, err := Parse(fs, configDir)

Expect(err).To(MatchError(`validating configuration: validation failed: field "Configuration.Kubernetes.Network.APIVIPMode" must be one of [managed external], but got "invalid"`))
})

It("Successfully parses relative release manifest URI", func() {
releaseFile := filepath.Join(string(configDir), "release.yaml")
releaseYAML := `manifestURI: file://./release-manifest.yaml`
Expand Down
7 changes: 4 additions & 3 deletions internal/image/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ func FindInitNode(nodes Nodes) (*Node, error) {
}

type Network struct {
APIHost string `yaml:"apiHost"`
APIVIP4 string `yaml:"apiVIP,omitempty" validate:"omitempty"`
APIVIP6 string `yaml:"apiVIP6,omitempty" validate:"omitempty,ipv6"`
APIHost string `yaml:"apiHost"`
APIVIP4 string `yaml:"apiVIP,omitempty" validate:"omitempty"`
APIVIP6 string `yaml:"apiVIP6,omitempty" validate:"omitempty,ipv6"`
APIVIPMode string `yaml:"apiVIPMode,omitempty" validate:"omitempty,oneof=managed external"`
}

func (n Network) IsHA() bool {
Expand Down