From 3bee92458f619e7a05330cda0fe39e6dc55a203b Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Fri, 27 Feb 2026 12:38:48 +0100 Subject: [PATCH 01/16] feat: custom SSH port for nodepools (old=22, new=22522) Add sshPort field to NodePool proto message to differentiate between old nodepools (port 22) and new nodepools (port 22522). Both dynamic and static nodepools are supported. - Add SSHPort helper and constants in internal/nodepools - Set SshPort=22522 for all new nodepools in CreateNodepools() - Transfer sshPort as immutable state for existing nodepools - Add ansible_port to all Ansible inventory templates - Add sshPort template function for Go templates - Update healthcheck to use dynamic SSH port instead of hardcoded 22 Co-Authored-By: Claude Opus 4.6 --- internal/api/manifest/utils.go | 3 ++ internal/nodepools/nodepools.go | 38 ++++++++++++++----- internal/templateUtils/templates.go | 3 ++ proto/pb/spec/nodepool.pb.go | 18 +++++++-- proto/spec/nodepool.proto | 4 ++ .../templates/all-node-inventory.goini | 4 +- .../ansibler/templates/k8s-inventory.goini | 8 ++-- .../ansibler/templates/lb-inventory.goini | 4 +- services/ansibler/templates/proxy-envs.goini | 8 ++-- .../internal/service/existing_state.go | 5 +++ .../manager/internal/service/healthcheck.go | 7 +--- 11 files changed, 72 insertions(+), 30 deletions(-) diff --git a/internal/api/manifest/utils.go b/internal/api/manifest/utils.go index 358804285..cf9bc6b27 100644 --- a/internal/api/manifest/utils.go +++ b/internal/api/manifest/utils.go @@ -6,6 +6,7 @@ import ( "math" "slices" + "github.com/berops/claudie/internal/nodepools" "github.com/berops/claudie/proto/pb/spec" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -333,6 +334,7 @@ func (ds *Manifest) CreateNodepools(pools []string, isControl bool) ([]*spec.Nod Labels: nodePool.Labels, Annotations: nodePool.Annotations, Taints: getTaints(nodePool.Taints), + SshPort: nodepools.NewSSHPort, // The nodes are left empty, as the desired state // in the manifest does not specify each of the nodes // individually, just the count of the nodes that the @@ -370,6 +372,7 @@ func (ds *Manifest) CreateNodepools(pools []string, isControl bool) ([]*spec.Nod Labels: nodePool.Labels, Annotations: nodePool.Annotations, Taints: taints, + SshPort: nodepools.NewSSHPort, Type: &spec.NodePool_StaticNodePool{ StaticNodePool: &spec.StaticNodePool{ NodeKeys: keys, diff --git a/internal/nodepools/nodepools.go b/internal/nodepools/nodepools.go index fbad4a4b5..bb3071de3 100644 --- a/internal/nodepools/nodepools.go +++ b/internal/nodepools/nodepools.go @@ -16,6 +16,22 @@ import ( "google.golang.org/protobuf/proto" ) +const ( + // DefaultSSHPort is the standard SSH port used by old/existing nodepools. + DefaultSSHPort = int32(22) + // NewSSHPort is the SSH port assigned to newly created nodepools. + NewSSHPort = int32(22522) +) + +// SSHPort returns the effective SSH port for a nodepool. +// Returns DefaultSSHPort (22) if not set (backwards compatibility with old nodepools). +func SSHPort(np *spec.NodePool) int32 { + if np.GetSshPort() == 0 { + return DefaultSSHPort + } + return np.GetSshPort() +} + type RegionNetwork struct { Region string ExternalNetwork string @@ -493,35 +509,37 @@ func RandomDynamicNode(nodepools iter.Seq[*spec.NodePool]) *spec.Node { return nodes[idx] } -// Returns a random node public Endpoint and a SSH key to connect to it. Nil if there is none. -func RandomNodePublicEndpoint(nps []*spec.NodePool) (string, string, string) { +// Returns a random node public Endpoint, SSH key, and SSH port to connect to it. +// Empty strings and 0 are returned if there are no nodepools/nodes. +func RandomNodePublicEndpoint(nps []*spec.NodePool) (username, endpoint, key string, sshPort int32) { if len(nps) == 0 { - return "", "", "" + return "", "", "", 0 } idx := rand.IntN(len(nps)) np := nps[idx] if len(np.Nodes) == 0 { - return "", "", "" + return "", "", "", 0 } idx = rand.IntN(len(np.Nodes)) node := np.Nodes[idx] - endpoint := node.Public - username := "root" + endpoint = node.Public + username = "root" if node.Username != "" && node.Username != username { username = node.Username } + sshPort = SSHPort(np) - switch np := np.Type.(type) { + switch npt := np.Type.(type) { case *spec.NodePool_DynamicNodePool: - return username, endpoint, np.DynamicNodePool.PrivateKey + return username, endpoint, npt.DynamicNodePool.PrivateKey, sshPort case *spec.NodePool_StaticNodePool: - return username, endpoint, np.StaticNodePool.NodeKeys[node.Public] + return username, endpoint, npt.StaticNodePool.NodeKeys[node.Public], sshPort default: - return "", "", "" + return "", "", "", 0 } } diff --git a/internal/templateUtils/templates.go b/internal/templateUtils/templates.go index bfa6ac161..50c85868f 100644 --- a/internal/templateUtils/templates.go +++ b/internal/templateUtils/templates.go @@ -11,6 +11,8 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" + "github.com/berops/claudie/internal/nodepools" + "github.com/berops/claudie/proto/pb/spec" ) // directory - output directory @@ -65,6 +67,7 @@ func LoadTemplate(tplFile string) (*template.Template, error) { funcMap["replaceAll"] = strings.ReplaceAll funcMap["extractNetmaskFromCIDR"] = ExtractNetmaskFromCIDR funcMap["hasExtension"] = HasExtension + funcMap["sshPort"] = func(np *spec.NodePool) int32 { return nodepools.SSHPort(np) } tpl, err := template.New("").Funcs(funcMap).Parse(tplFile) if err != nil { diff --git a/proto/pb/spec/nodepool.pb.go b/proto/pb/spec/nodepool.pb.go index 8ee1fec30..02f6c0355 100644 --- a/proto/pb/spec/nodepool.pb.go +++ b/proto/pb/spec/nodepool.pb.go @@ -203,7 +203,11 @@ type NodePool struct { // User defined taints. Taints []*Taint `protobuf:"bytes,7,rep,name=taints,proto3" json:"taints,omitempty"` // User definded annotations. - Annotations map[string]string `protobuf:"bytes,8,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Annotations map[string]string `protobuf:"bytes,8,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // SSH port used to connect to nodes in this nodepool. + // 0 means default (port 22) for backwards compatibility with old nodepools. + // New nodepools are assigned port 22522. + SshPort int32 `protobuf:"varint,9,opt,name=sshPort,proto3" json:"sshPort,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -305,6 +309,13 @@ func (x *NodePool) GetAnnotations() map[string]string { return nil } +func (x *NodePool) GetSshPort() int32 { + if x != nil { + return x.SshPort + } + return 0 +} + type isNodePool_Type interface { isNodePool_Type() } @@ -818,7 +829,7 @@ var File_spec_nodepool_proto protoreflect.FileDescriptor const file_spec_nodepool_proto_rawDesc = "" + "\n" + - "\x13spec/nodepool.proto\x12\x04spec\x1a\x13spec/provider.proto\"\x80\x04\n" + + "\x13spec/nodepool.proto\x12\x04spec\x1a\x13spec/provider.proto\"\x9a\x04\n" + "\bNodePool\x12A\n" + "\x0fdynamicNodePool\x18\x01 \x01(\v2\x15.spec.DynamicNodePoolH\x00R\x0fdynamicNodePool\x12>\n" + "\x0estaticNodePool\x18\x02 \x01(\v2\x14.spec.StaticNodePoolH\x00R\x0estaticNodePool\x12\x12\n" + @@ -828,7 +839,8 @@ const file_spec_nodepool_proto_rawDesc = "" + "\tisControl\x18\x05 \x01(\bR\tisControl\x122\n" + "\x06labels\x18\x06 \x03(\v2\x1a.spec.NodePool.LabelsEntryR\x06labels\x12#\n" + "\x06taints\x18\a \x03(\v2\v.spec.TaintR\x06taints\x12A\n" + - "\vannotations\x18\b \x03(\v2\x1f.spec.NodePool.AnnotationsEntryR\vannotations\x1a9\n" + + "\vannotations\x18\b \x03(\v2\x1f.spec.NodePool.AnnotationsEntryR\vannotations\x12\x18\n" + + "\asshPort\x18\t \x01(\x05R\asshPort\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + diff --git a/proto/spec/nodepool.proto b/proto/spec/nodepool.proto index 96747b6d3..3c7adeaca 100644 --- a/proto/spec/nodepool.proto +++ b/proto/spec/nodepool.proto @@ -24,6 +24,10 @@ message NodePool { repeated Taint taints = 7; // User definded annotations. map annotations = 8; + // SSH port used to connect to nodes in this nodepool. + // 0 means default (port 22) for backwards compatibility with old nodepools. + // New nodepools are assigned port 22522. + int32 sshPort = 9; } // Taint defines a custom defined taint for the node pools. diff --git a/services/ansibler/templates/all-node-inventory.goini b/services/ansibler/templates/all-node-inventory.goini index a32eee69a..614f01d36 100644 --- a/services/ansibler/templates/all-node-inventory.goini +++ b/services/ansibler/templates/all-node-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Dynamic }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -11,7 +11,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Static }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/k8s-inventory.goini b/services/ansibler/templates/k8s-inventory.goini index 576c78c3e..b30ae16c7 100644 --- a/services/ansibler/templates/k8s-inventory.goini +++ b/services/ansibler/templates/k8s-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -10,7 +10,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -19,7 +19,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -27,7 +27,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/lb-inventory.goini b/services/ansibler/templates/lb-inventory.goini index dccbf0abe..a1cfce436 100644 --- a/services/ansibler/templates/lb-inventory.goini +++ b/services/ansibler/templates/lb-inventory.goini @@ -2,13 +2,13 @@ {{- range $lbNodepool := $.LBnodepools.Dynamic }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} ansible_port={{ sshPort $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- range $lbNodepool := $.LBnodepools.Static }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} ansible_port={{ sshPort $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} diff --git a/services/ansibler/templates/proxy-envs.goini b/services/ansibler/templates/proxy-envs.goini index c814a9353..7acc7e20e 100644 --- a/services/ansibler/templates/proxy-envs.goini +++ b/services/ansibler/templates/proxy-envs.goini @@ -2,14 +2,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -18,14 +18,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/manager/internal/service/existing_state.go b/services/manager/internal/service/existing_state.go index 06860849a..de993ac11 100644 --- a/services/manager/internal/service/existing_state.go +++ b/services/manager/internal/service/existing_state.go @@ -561,6 +561,11 @@ outer: continue } + // Transfer the SSH port from the current state. For old nodepools + // that predate the SSH port feature, SshPort will be 0 which + // resolves to port 22 at runtime for backwards compatibility. + desired.SshPort = current.SshPort + switch current.GetType().(type) { case *spec.NodePool_DynamicNodePool: if desired.GetDynamicNodePool() == nil { diff --git a/services/manager/internal/service/healthcheck.go b/services/manager/internal/service/healthcheck.go index 7ae9c62f2..1e4efeb23 100644 --- a/services/manager/internal/service/healthcheck.go +++ b/services/manager/internal/service/healthcheck.go @@ -18,9 +18,6 @@ import ( "golang.org/x/crypto/ssh" ) -// Port on which the ssh connection is established when healthchecking -// individual nodes. -const sshPort = "22" // UnreachableNodesMap holds the nodepools and all of the nodes within // that nodepool that are unreachable via a Ping on the IPv4 public endpoint. @@ -161,7 +158,7 @@ func healthCheckVPN(state *spec.Clusters) (bool, error) { nps = append(nps, lb.ClusterInfo.NodePools...) } - username, public, key := nodepools.RandomNodePublicEndpoint(nps) + username, public, key, port := nodepools.RandomNodePublicEndpoint(nps) if key == "" { // If there is no key, than there is no node. assume all is okay. return true, nil @@ -185,7 +182,7 @@ func healthCheckVPN(state *spec.Clusters) (bool, error) { Timeout: 2 * time.Second, } - endpoint := net.JoinHostPort(public, sshPort) + endpoint := net.JoinHostPort(public, fmt.Sprint(port)) client, err := ssh.Dial("tcp", endpoint, &cfg) if err != nil { return false, err From edbdf564cf4f5d04c58c99ad67d025591c4a2f31 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Tue, 3 Mar 2026 10:20:20 +0100 Subject: [PATCH 02/16] feat: propagate custom SSH port to kube-eleven and terraformer templates Add SshPort field to NodepoolInfo in kube-eleven and terraformer so the custom SSH port (22522 for new nodepools, 22 for old) is passed through to the kubeone and terraform templates. Co-Authored-By: Claude Opus 4.6 --- .../worker/service/internal/kube-eleven/kube_eleven.go | 1 + .../internal/worker/service/internal/kube-eleven/types.go | 1 + services/kube-eleven/templates/kubeone.tpl | 2 ++ .../worker/service/internal/cluster-builder/cluster_builder.go | 1 + .../internal/worker/service/internal/templates/structures.go | 3 +++ 5 files changed, 8 insertions(+) diff --git a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go index bb0a31a15..72b0722fb 100644 --- a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go +++ b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go @@ -199,6 +199,7 @@ func (k *KubeEleven) getClusterNodes() []*NodepoolInfo { return strings.TrimPrefix(name, fmt.Sprintf("%s-", k.K8sCluster.ClusterInfo.Id())) }), IsDynamic: true, + SshPort: nodepools.SSHPort(nodepool), } } else if nodepool.GetStaticNodePool() != nil { nodepoolInfo = &NodepoolInfo{ diff --git a/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go b/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go index 15cafda79..c18467cd6 100644 --- a/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go +++ b/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go @@ -21,6 +21,7 @@ type ( Zone string CloudProviderName string ProviderName string + SshPort int32 } // templateData struct holds the data which will be used in creating diff --git a/services/kube-eleven/templates/kubeone.tpl b/services/kube-eleven/templates/kubeone.tpl index c6e97495a..2281f205e 100644 --- a/services/kube-eleven/templates/kubeone.tpl +++ b/services/kube-eleven/templates/kubeone.tpl @@ -39,6 +39,7 @@ controlPlane: {{- if $nodepool.IsDynamic }} sshUsername: root sshPrivateKeyFile: '{{ $nodepool.NodepoolName }}.pem' + sshPort: {{ $nodepool.SshPort }} {{- else }} sshUsername: '{{ $nodeInfo.Node.Username }}' sshPrivateKeyFile: './{{ $nodeInfo.Name }}.pem' @@ -64,6 +65,7 @@ staticWorkers: {{- if $nodepool.IsDynamic }} sshUsername: root sshPrivateKeyFile: '{{ $nodepool.NodepoolName }}.pem' + sshPort: {{ $nodepool.SshPort }} {{- else }} sshUsername: '{{ $nodeInfo.Node.Username }}' sshPrivateKeyFile: './{{ $nodeInfo.Name }}.pem' diff --git a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go index 500b40f14..a74b9dca7 100644 --- a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go +++ b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go @@ -243,6 +243,7 @@ func (c *ClusterBuilder) generateFiles(clusterDir string) error { Nodes: np.Nodes, Details: np.GetDynamicNodePool(), IsControl: np.IsControl, + SshPort: nodepools.SSHPort(np), }) if err := fileutils.CreateKey(dnp.GetPublicKey(), clusterDir, np.GetName()); err != nil { diff --git a/services/terraformer/internal/worker/service/internal/templates/structures.go b/services/terraformer/internal/worker/service/internal/templates/structures.go index 1886e8edc..2862a56d6 100644 --- a/services/terraformer/internal/worker/service/internal/templates/structures.go +++ b/services/terraformer/internal/worker/service/internal/templates/structures.go @@ -46,6 +46,9 @@ type ( // IsControl Specifies whether the nodepools is used as a control or compute nodepool within the cluster. // In the context of LB cluster, nodepools can only be compute or "worker" nodepools. IsControl bool + // SshPort is the SSH port used to connect to nodes in this nodepool. + // Old nodepools use 22, new nodepools use 22522. + SshPort int32 } ) From 9d90d619584955a6d9b6fc5b633291264cc68359 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Tue, 3 Mar 2026 11:52:41 +0100 Subject: [PATCH 03/16] fix: copy SshPort in nodepool shallow-copy functions PartialCopyWithNodeFilter and PartialCopyWithReplacedNodes were not copying the SshPort field, causing Ansible inventory to fall back to port 22 for tasks that use filtered nodepool copies (e.g. task_install_node_requirements). Co-Authored-By: Claude Opus 4.6 --- internal/nodepools/nodepools.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/nodepools/nodepools.go b/internal/nodepools/nodepools.go index bb3071de3..2ea95212a 100644 --- a/internal/nodepools/nodepools.go +++ b/internal/nodepools/nodepools.go @@ -164,6 +164,7 @@ func PartialCopyWithNodeFilter(np *spec.NodePool, nodes []string) *spec.NodePool Labels: np.Labels, Taints: np.Taints, Annotations: np.Annotations, + SshPort: np.SshPort, } for _, n := range np.Nodes { @@ -211,6 +212,7 @@ func PartialCopyWithReplacedNodes(np *spec.NodePool, nodes []*spec.Node, nodeKey Labels: np.Labels, Taints: np.Taints, Annotations: np.Annotations, + SshPort: np.SshPort, } // To avoid issues with possible node counts, deep clone the node type itself. From 25cb3753e14cb517b78f58ba081a36853c78d857 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Wed, 4 Mar 2026 08:11:14 +0100 Subject: [PATCH 04/16] Fixed NodePool initialization inside PartialCopyWithNodeExclusion func --- internal/nodepools/nodepools.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/nodepools/nodepools.go b/internal/nodepools/nodepools.go index 2ea95212a..68c5fd00a 100644 --- a/internal/nodepools/nodepools.go +++ b/internal/nodepools/nodepools.go @@ -118,6 +118,7 @@ func PartialCopyWithNodeExclusion(np *spec.NodePool, nodes []string) *spec.NodeP Labels: np.Labels, Taints: np.Taints, Annotations: np.Annotations, + SshPort: np.SshPort, } for _, n := range np.Nodes { From 7218f6162c9ef8fb66eebb1807fb435daa16f6d9 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Wed, 4 Mar 2026 08:12:39 +0100 Subject: [PATCH 05/16] Move SshPort transfer inside type-compatible branches --- services/manager/internal/service/existing_state.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/manager/internal/service/existing_state.go b/services/manager/internal/service/existing_state.go index 00d125575..37f04d7bd 100644 --- a/services/manager/internal/service/existing_state.go +++ b/services/manager/internal/service/existing_state.go @@ -561,11 +561,6 @@ outer: continue } - // Transfer the SSH port from the current state. For old nodepools - // that predate the SSH port feature, SshPort will be 0 which - // resolves to port 22 at runtime for backwards compatibility. - desired.SshPort = current.SshPort - switch current.GetType().(type) { case *spec.NodePool_DynamicNodePool: if desired.GetDynamicNodePool() == nil { @@ -575,6 +570,10 @@ outer: // name. continue outer } + // Transfer the SSH port from the current state. For old nodepools + // that predate the SSH port feature, SshPort will be 0 which + // resolves to port 22 at runtime for backwards compatibility. + desired.SshPort = current.SshPort transferDynamicNodePool(current, desired) case *spec.NodePool_StaticNodePool: if desired.GetStaticNodePool() == nil { @@ -584,6 +583,7 @@ outer: // name. continue outer } + desired.SshPort = current.SshPort transferStaticNodePool(current, desired) } } From 525e2b8a9a37fa8a2143528a056480da52512444 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Wed, 4 Mar 2026 10:56:13 +0100 Subject: [PATCH 06/16] fix linting --- services/manager/internal/service/healthcheck.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/manager/internal/service/healthcheck.go b/services/manager/internal/service/healthcheck.go index 1e4efeb23..664b87c57 100644 --- a/services/manager/internal/service/healthcheck.go +++ b/services/manager/internal/service/healthcheck.go @@ -18,7 +18,6 @@ import ( "golang.org/x/crypto/ssh" ) - // UnreachableNodesMap holds the nodepools and all of the nodes within // that nodepool that are unreachable via a Ping on the IPv4 public endpoint. type UnreachableIPv4Map = map[string][]string From bc48f66da939c6a8bddffeefb1edf0b27a3d3166 Mon Sep 17 00:00:00 2001 From: CI/CD pipeline Date: Wed, 4 Mar 2026 10:06:22 +0000 Subject: [PATCH 07/16] Auto commit - update kustomization.yaml --- manifests/claudie/kustomization.yaml | 14 +++++++------- manifests/testing-framework/kustomization.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manifests/claudie/kustomization.yaml b/manifests/claudie/kustomization.yaml index d2f690234..0efba5c77 100644 --- a/manifests/claudie/kustomization.yaml +++ b/manifests/claudie/kustomization.yaml @@ -56,16 +56,16 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: ghcr.io/berops/claudie/ansibler - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/autoscaler-adapter - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/claudie-operator - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/kube-eleven - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/kuber - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/manager - newTag: 24da72e-3952 + newTag: dc10aed-3958 - name: ghcr.io/berops/claudie/terraformer - newTag: 24da72e-3952 + newTag: dc10aed-3958 diff --git a/manifests/testing-framework/kustomization.yaml b/manifests/testing-framework/kustomization.yaml index 7c29f355d..cec802e34 100644 --- a/manifests/testing-framework/kustomization.yaml +++ b/manifests/testing-framework/kustomization.yaml @@ -89,4 +89,4 @@ secretGenerator: images: - name: ghcr.io/berops/claudie/testing-framework - newTag: 24da72e-3952 + newTag: dc10aed-3958 From faf4eacab58919d815d402f84ae6429b02de5329 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Wed, 4 Mar 2026 15:37:03 +0100 Subject: [PATCH 08/16] Added missing SshPort propagation to static nodepoolInfo --- .../internal/worker/service/internal/kube-eleven/kube_eleven.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go index 72b0722fb..b7dde792d 100644 --- a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go +++ b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go @@ -210,6 +210,7 @@ func (k *KubeEleven) getClusterNodes() []*NodepoolInfo { ProviderName: sanitise.String(staticProviderName), Nodes: getNodeData(nodepool.Nodes, func(s string) string { return s }), IsDynamic: false, + SshPort: nodepools.SSHPort(nodepool), } } nodepoolInfos = append(nodepoolInfos, nodepoolInfo) From 9554068a85f81378aa2d6410c47ee479b9d148f0 Mon Sep 17 00:00:00 2001 From: CI/CD pipeline Date: Wed, 4 Mar 2026 14:54:26 +0000 Subject: [PATCH 09/16] Auto commit - update kustomization.yaml --- manifests/claudie/kustomization.yaml | 14 +++++++------- manifests/testing-framework/kustomization.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manifests/claudie/kustomization.yaml b/manifests/claudie/kustomization.yaml index 0efba5c77..d2c1eec69 100644 --- a/manifests/claudie/kustomization.yaml +++ b/manifests/claudie/kustomization.yaml @@ -56,16 +56,16 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: ghcr.io/berops/claudie/ansibler - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/autoscaler-adapter - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/claudie-operator - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/kube-eleven - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/kuber - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/manager - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 - name: ghcr.io/berops/claudie/terraformer - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 diff --git a/manifests/testing-framework/kustomization.yaml b/manifests/testing-framework/kustomization.yaml index cec802e34..832fce4bf 100644 --- a/manifests/testing-framework/kustomization.yaml +++ b/manifests/testing-framework/kustomization.yaml @@ -89,4 +89,4 @@ secretGenerator: images: - name: ghcr.io/berops/claudie/testing-framework - newTag: dc10aed-3958 + newTag: dfdf0f9-3961 From 02417e8829710b7d3edb904e4ae950828e9390ad Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 09:54:26 +0100 Subject: [PATCH 10/16] Added sshPort to protobuf --- proto/pb/spec/nodepool.pb.go | 16 +++++++++++++--- proto/spec/nodepool.proto | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/proto/pb/spec/nodepool.pb.go b/proto/pb/spec/nodepool.pb.go index 02f6c0355..5c3209c72 100644 --- a/proto/pb/spec/nodepool.pb.go +++ b/proto/pb/spec/nodepool.pb.go @@ -410,7 +410,9 @@ type Node struct { // Username of a user with root privileges. Also used in SSH connection Username string `protobuf:"bytes,5,opt,name=username,proto3" json:"username,omitempty"` // Status of the node. - Status NodeStatus `protobuf:"varint,6,opt,name=status,proto3,enum=spec.NodeStatus" json:"status,omitempty"` + Status NodeStatus `protobuf:"varint,6,opt,name=status,proto3,enum=spec.NodeStatus" json:"status,omitempty"` + // Ssh port + SshPort string `protobuf:"bytes,7,opt,name=sshPort,proto3" json:"sshPort,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -487,6 +489,13 @@ func (x *Node) GetStatus() NodeStatus { return NodeStatus_Preparing } +func (x *Node) GetSshPort() string { + if x != nil { + return x.SshPort + } + return "" +} + // DynamicNodePool represents dynamic node pool used in cluster. type DynamicNodePool struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -851,14 +860,15 @@ const file_spec_nodepool_proto_rawDesc = "" + "\x05Taint\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\x12\x16\n" + - "\x06effect\x18\x03 \x01(\tR\x06effect\"\xbe\x01\n" + + "\x06effect\x18\x03 \x01(\tR\x06effect\"\xd8\x01\n" + "\x04Node\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\aprivate\x18\x02 \x01(\tR\aprivate\x12\x16\n" + "\x06public\x18\x03 \x01(\tR\x06public\x12*\n" + "\bnodeType\x18\x04 \x01(\x0e2\x0e.spec.NodeTypeR\bnodeType\x12\x1a\n" + "\busername\x18\x05 \x01(\tR\busername\x12(\n" + - "\x06status\x18\x06 \x01(\x0e2\x10.spec.NodeStatusR\x06status\"\xda\x03\n" + + "\x06status\x18\x06 \x01(\x0e2\x10.spec.NodeStatusR\x06status\x12\x18\n" + + "\asshPort\x18\a \x01(\tR\asshPort\"\xda\x03\n" + "\x0fDynamicNodePool\x12\x1e\n" + "\n" + "serverType\x18\x01 \x01(\tR\n" + diff --git a/proto/spec/nodepool.proto b/proto/spec/nodepool.proto index 3c7adeaca..31c89627f 100644 --- a/proto/spec/nodepool.proto +++ b/proto/spec/nodepool.proto @@ -55,6 +55,8 @@ message Node { // Status of the node. NodeStatus status = 6; + // Ssh port + string sshPort = 7; } // NodeType specifies the type of the node. From 0ce2448bfd275fdd36cd1cc5f2deef62be0caf99 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 09:57:07 +0100 Subject: [PATCH 11/16] Modified terraform output parsing to include ssh port and added tests --- .../cluster-builder/cluster_builder.go | 52 +++++++++++--- .../cluster-builder/cluster_builder_test.go | 69 +++++++++++++++++-- .../service/internal/templates/structures.go | 33 ++++----- 3 files changed, 121 insertions(+), 33 deletions(-) diff --git a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go index a74b9dca7..a8dab9312 100644 --- a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go +++ b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go @@ -121,21 +121,26 @@ func (c ClusterBuilder) ReconcileNodePools() error { if err != nil { return fmt.Errorf("error while getting output from tofu for %s : %w", nodepool.Name, err) } - out, err := readIPs(output) + out, err := readNodeOutput(output) if err != nil { return fmt.Errorf("error while reading the tofu output for %s : %w", nodepool.Name, err) } for _, n := range nodepool.Nodes { var found bool - for target, ip := range generics.IterateMapInOrder(out.IPs) { + for target, val := range generics.IterateMapInOrder(out.Nodes) { if target != n.Name { continue } + ip, port, err := parseNodeOutput(val) + if err != nil { + return fmt.Errorf("node %q from nodepool %q: %w", n.Name, nodepool.Name, err) + } if ip == "" { return fmt.Errorf("node %q from nodepool %q has no public address assigned", n.Name, nodepool.Name) } found = true - n.Public = fmt.Sprint(ip) + n.Public = ip + n.SshPort = port break } if !found { @@ -290,11 +295,42 @@ func (c *ClusterBuilder) generateFiles(clusterDir string) error { return nil } -// readIPs reads json output format from tofu and unmarshal it into map[string]map[string]string readable by Go. -func readIPs(data string) (templates.NodepoolIPs, error) { - var result templates.NodepoolIPs - // Unmarshal or Decode the JSON to the interface. - err := json.Unmarshal([]byte(data), &result.IPs) +// parseNodeOutput extracts the IP and optional SSH port from a terraform output value. +// The value can be either a string (just IP) or an array [IP, port]. +func parseNodeOutput(val any) (ip string, port string, err error) { + switch v := val.(type) { + case string: + return v, "", nil + case []any: + if len(v) == 0 { + return "", "", fmt.Errorf("empty output array") + } + ipStr, ok := v[0].(string) + if !ok { + return "", "", fmt.Errorf("expected string IP in output array, got %T", v[0]) + } + if len(v) >= 2 { + portStr, ok := v[1].(string) + if !ok { + // Handle numeric port from JSON (float64). + if portNum, ok := v[1].(float64); ok { + portStr = fmt.Sprintf("%d", int(portNum)) + } else { + return "", "", fmt.Errorf("expected string or number port in output array, got %T", v[1]) + } + } + return ipStr, portStr, nil + } + return ipStr, "", nil + default: + return fmt.Sprint(val), "", nil + } +} + +// readNodeOutput reads json output format from tofu and unmarshals it into a map of node names to their output values. +func readNodeOutput(data string) (templates.NodepoolOutput, error) { + var result templates.NodepoolOutput + err := json.Unmarshal([]byte(data), &result.Nodes) return result, err } diff --git a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder_test.go b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder_test.go index 0925648ca..ead5dc676 100644 --- a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder_test.go +++ b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder_test.go @@ -7,14 +7,14 @@ import ( "github.com/berops/claudie/services/terraformer/internal/worker/service/internal/templates" ) -func Test_readIPs(t *testing.T) { +func Test_readNodeOutput(t *testing.T) { type args struct { data string } tests := []struct { name string args args - want templates.NodepoolIPs + want templates.NodepoolOutput wantErr bool }{ { @@ -22,8 +22,8 @@ func Test_readIPs(t *testing.T) { args: args{ data: "{\"test-cluster-compute1\":\"0.0.0.65\",\n\"test-cluster-compute2\":\"0.0.0.512\", \"test-cluster-control1\":\"0.0.0.72\",\n\"test-cluster-control2\":\"0.0.0.65\"}", }, - want: templates.NodepoolIPs{ - IPs: map[string]any{ + want: templates.NodepoolOutput{ + Nodes: map[string]any{ "test-cluster-compute1": "0.0.0.65", "test-cluster-compute2": "0.0.0.512", "test-cluster-control1": "0.0.0.72", @@ -35,13 +35,68 @@ func Test_readIPs(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := readIPs(tt.args.data) + got, err := readNodeOutput(tt.args.data) if (err != nil) != tt.wantErr { - t.Errorf("readIPs() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("readNodeOutput() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("readIPs() got = %v, want %v", got, tt.want) + t.Errorf("readNodeOutput() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseNodeOutput(t *testing.T) { + tests := []struct { + name string + val any + wantIP string + wantPort string + wantErr bool + }{ + { + name: "string IP only", + val: "18.194.81.150", + wantIP: "18.194.81.150", + wantPort: "", + }, + { + name: "array with IP and port string", + val: []any{"18.194.81.150", "22"}, + wantIP: "18.194.81.150", + wantPort: "22", + }, + { + name: "array with IP and port number", + val: []any{"18.194.81.150", float64(22522)}, + wantIP: "18.194.81.150", + wantPort: "22522", + }, + { + name: "array with IP only", + val: []any{"18.194.81.150"}, + wantIP: "18.194.81.150", + wantPort: "", + }, + { + name: "empty array", + val: []any{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip, port, err := parseNodeOutput(tt.val) + if (err != nil) != tt.wantErr { + t.Errorf("parseNodeOutput() error = %v, wantErr %v", err, tt.wantErr) + return + } + if ip != tt.wantIP { + t.Errorf("parseNodeOutput() ip = %v, want %v", ip, tt.wantIP) + } + if port != tt.wantPort { + t.Errorf("parseNodeOutput() port = %v, want %v", port, tt.wantPort) } }) } diff --git a/services/terraformer/internal/worker/service/internal/templates/structures.go b/services/terraformer/internal/worker/service/internal/templates/structures.go index 2862a56d6..9596c3115 100644 --- a/services/terraformer/internal/worker/service/internal/templates/structures.go +++ b/services/terraformer/internal/worker/service/internal/templates/structures.go @@ -245,24 +245,21 @@ type ( // All the following types grouped are passed in as "Outputs" from // using the generated template files. type ( - // NodepoolIPs wrap the output data that is acquired from using the - // generated template files. - NodepoolIPs struct { - // IPs holds the IPv4 addresses of the spawned VM instances from the - // generated templates files. It is expected that the template files - // in the nodepool directory that spawn the VM instances also - // expose the IP addresses of the Instances. - // For example (in the case of our own hetzner template files): - // - // output "{{ $nodepool.Name }}_{{ $specName }}_{{ $uniqueFingerPrint }}" { - // value = { - // {{- range $node := $nodepool.Nodes }} - // {{- $serverResourceName := printf "%s_%s" $node.Name $resourceSuffix }} - // "${hcloud_server.{{ $serverResourceName }}.name}" = hcloud_server.{{ $serverResourceName }}.ipv4_address - // {{- end }} - // } - //} - IPs map[string]any `json:"-"` + // NodepoolOutput wraps the output data that is acquired from using the + // generated template files. Each entry maps a node name to its output + // value, which can be a plain IP string or a [IP, port] array. + // For example (in the case of our own hetzner template files): + // + // output "{{ $nodepool.Name }}_{{ $specName }}_{{ $uniqueFingerPrint }}" { + // value = { + // {{- range $node := $nodepool.Nodes }} + // {{- $serverResourceName := printf "%s_%s" $node.Name $resourceSuffix }} + // "${hcloud_server.{{ $serverResourceName }}.name}" = hcloud_server.{{ $serverResourceName }}.ipv4_address + // {{- end }} + // } + //} + NodepoolOutput struct { + Nodes map[string]any `json:"-"` } // DNSDomain wrap the output data that is acquired from using the From ec7b21585d3dad638491174a5b57b2f139d78009 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 09:59:38 +0100 Subject: [PATCH 12/16] Modified ansibler service to include ssh port from terraform output --- internal/templateUtils/templates.go | 12 +++++++++++- services/ansibler/templates/all-node-inventory.goini | 4 ++-- services/ansibler/templates/k8s-inventory.goini | 8 ++++---- services/ansibler/templates/lb-inventory.goini | 4 ++-- services/ansibler/templates/proxy-envs.goini | 8 ++++---- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/internal/templateUtils/templates.go b/internal/templateUtils/templates.go index 50c85868f..12042c4fe 100644 --- a/internal/templateUtils/templates.go +++ b/internal/templateUtils/templates.go @@ -67,7 +67,7 @@ func LoadTemplate(tplFile string) (*template.Template, error) { funcMap["replaceAll"] = strings.ReplaceAll funcMap["extractNetmaskFromCIDR"] = ExtractNetmaskFromCIDR funcMap["hasExtension"] = HasExtension - funcMap["sshPort"] = func(np *spec.NodePool) int32 { return nodepools.SSHPort(np) } + funcMap["nodeSshPort"] = NodeSSHPort tpl, err := template.New("").Funcs(funcMap).Parse(tplFile) if err != nil { @@ -76,6 +76,16 @@ func LoadTemplate(tplFile string) (*template.Template, error) { return tpl, nil } +// NodeSSHPort returns the effective SSH port for a node. +// If the node has a port from the terraform output, it is used. +// Otherwise, falls back to the nodepool-level port (default 22). +func NodeSSHPort(node *spec.Node, np *spec.NodePool) string { + if node.SshPort != "" { + return node.SshPort + } + return fmt.Sprintf("%d", nodepools.SSHPort(np)) +} + // ExtractNetmaskFromCIDR extracts the netmask from the CIDR notation. func ExtractNetmaskFromCIDR(cidr string) string { _, n, err := net.ParseCIDR(cidr) diff --git a/services/ansibler/templates/all-node-inventory.goini b/services/ansibler/templates/all-node-inventory.goini index 614f01d36..455b13fc1 100644 --- a/services/ansibler/templates/all-node-inventory.goini +++ b/services/ansibler/templates/all-node-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Dynamic }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -11,7 +11,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Static }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/k8s-inventory.goini b/services/ansibler/templates/k8s-inventory.goini index b30ae16c7..2e7e4aa64 100644 --- a/services/ansibler/templates/k8s-inventory.goini +++ b/services/ansibler/templates/k8s-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -10,7 +10,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -19,7 +19,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -27,7 +27,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/lb-inventory.goini b/services/ansibler/templates/lb-inventory.goini index a1cfce436..34850da08 100644 --- a/services/ansibler/templates/lb-inventory.goini +++ b/services/ansibler/templates/lb-inventory.goini @@ -2,13 +2,13 @@ {{- range $lbNodepool := $.LBnodepools.Dynamic }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} ansible_port={{ sshPort $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- range $lbNodepool := $.LBnodepools.Static }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} ansible_port={{ sshPort $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} diff --git a/services/ansibler/templates/proxy-envs.goini b/services/ansibler/templates/proxy-envs.goini index 7acc7e20e..c4fd0fc28 100644 --- a/services/ansibler/templates/proxy-envs.goini +++ b/services/ansibler/templates/proxy-envs.goini @@ -2,14 +2,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -18,14 +18,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ sshPort $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} From 1f4aac6af712b68a5594c5509b76b80660f0f709 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 12:00:02 +0100 Subject: [PATCH 13/16] Fix retrieve of sshport when terraform output is empty string --- internal/templateUtils/templates.go | 9 ++++----- services/ansibler/templates/all-node-inventory.goini | 4 ++-- services/ansibler/templates/k8s-inventory.goini | 8 ++++---- services/ansibler/templates/lb-inventory.goini | 4 ++-- services/ansibler/templates/proxy-envs.goini | 8 ++++---- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/templateUtils/templates.go b/internal/templateUtils/templates.go index 12042c4fe..16c42c4e4 100644 --- a/internal/templateUtils/templates.go +++ b/internal/templateUtils/templates.go @@ -76,14 +76,13 @@ func LoadTemplate(tplFile string) (*template.Template, error) { return tpl, nil } -// NodeSSHPort returns the effective SSH port for a node. -// If the node has a port from the terraform output, it is used. -// Otherwise, falls back to the nodepool-level port (default 22). -func NodeSSHPort(node *spec.Node, np *spec.NodePool) string { +// NodeSSHPort returns the SSH port for a node. +// Uses the port from terraform output if present, otherwise defaults to DefaultSSHPort -> 22. +func NodeSSHPort(node *spec.Node) string { if node.SshPort != "" { return node.SshPort } - return fmt.Sprintf("%d", nodepools.SSHPort(np)) + return fmt.Sprintf("%d", nodepools.DefaultSSHPort) } // ExtractNetmaskFromCIDR extracts the netmask from the CIDR notation. diff --git a/services/ansibler/templates/all-node-inventory.goini b/services/ansibler/templates/all-node-inventory.goini index 455b13fc1..0e6fadf60 100644 --- a/services/ansibler/templates/all-node-inventory.goini +++ b/services/ansibler/templates/all-node-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Dynamic }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $nodepoolInfo.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -11,7 +11,7 @@ {{- range $nodepoolInfo := .NodepoolsInfo }} {{- range $nodepool := $nodepoolInfo.Nodepools.Static }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} netmask={{ extractNetmaskFromCIDR $nodepoolInfo.ClusterNetwork }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/k8s-inventory.goini b/services/ansibler/templates/k8s-inventory.goini index 2e7e4aa64..0095ae4a7 100644 --- a/services/ansibler/templates/k8s-inventory.goini +++ b/services/ansibler/templates/k8s-inventory.goini @@ -2,7 +2,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -10,7 +10,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -19,7 +19,7 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -27,7 +27,7 @@ {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} diff --git a/services/ansibler/templates/lb-inventory.goini b/services/ansibler/templates/lb-inventory.goini index 34850da08..cdeaddd34 100644 --- a/services/ansibler/templates/lb-inventory.goini +++ b/services/ansibler/templates/lb-inventory.goini @@ -2,13 +2,13 @@ {{- range $lbNodepool := $.LBnodepools.Dynamic }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-%s-" $.Name $.Hash) $lbNode.Name }} ansible_user=root ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- range $lbNodepool := $.LBnodepools.Static }} {{- range $lbNode := $lbNodepool.Nodes }} {{/*key.pem is taken from a directory where ansible-playbook is called, thus it does not need to specify path relative to inventory.ini*/}} -{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode $lbNodepool }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $lbNode.Name }} ansible_user={{ $lbNode.Username }} ansible_host={{ $lbNode.Public }} ansible_port={{ nodeSshPort $lbNode }} private_ip={{ $lbNode.Private }} ansible_ssh_private_key_file={{ $lbNode.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} diff --git a/services/ansibler/templates/proxy-envs.goini b/services/ansibler/templates/proxy-envs.goini index c4fd0fc28..ba3b46c7f 100644 --- a/services/ansibler/templates/proxy-envs.goini +++ b/services/ansibler/templates/proxy-envs.goini @@ -2,14 +2,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} @@ -18,14 +18,14 @@ {{- range $nodepool := .K8sNodepools.Dynamic }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ trimPrefix (printf "%s-" $.ClusterID) $node.Name }} ansible_user=root ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $nodepool.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} {{- range $nodepool := .K8sNodepools.Static }} {{- if not $nodepool.IsControl }} {{- range $node := $nodepool.Nodes }} -{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node $nodepool }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" +{{ $node.Name }} ansible_user={{ $node.Username }} ansible_host={{ $node.Public }} ansible_port={{ nodeSshPort $node }} private_ip={{ $node.Private }} no_proxy_list={{ $.NoProxyList }} http_proxy_url={{ $.HttpProxyUrl }} ansible_ssh_private_key_file={{ $node.Name }}.pem ansible_ssh_extra_args="-o IdentitiesOnly=yes" {{- end }} {{- end }} {{- end }} From 248fde59716babd706fc6476f2fe62d4968f9b20 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 12:00:17 +0100 Subject: [PATCH 14/16] Fix kubeeleven port definition in template --- .../worker/service/internal/kube-eleven/kube_eleven.go | 2 -- .../internal/worker/service/internal/kube-eleven/types.go | 1 - services/kube-eleven/templates/kubeone.tpl | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go index b7dde792d..bb0a31a15 100644 --- a/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go +++ b/services/kube-eleven/internal/worker/service/internal/kube-eleven/kube_eleven.go @@ -199,7 +199,6 @@ func (k *KubeEleven) getClusterNodes() []*NodepoolInfo { return strings.TrimPrefix(name, fmt.Sprintf("%s-", k.K8sCluster.ClusterInfo.Id())) }), IsDynamic: true, - SshPort: nodepools.SSHPort(nodepool), } } else if nodepool.GetStaticNodePool() != nil { nodepoolInfo = &NodepoolInfo{ @@ -210,7 +209,6 @@ func (k *KubeEleven) getClusterNodes() []*NodepoolInfo { ProviderName: sanitise.String(staticProviderName), Nodes: getNodeData(nodepool.Nodes, func(s string) string { return s }), IsDynamic: false, - SshPort: nodepools.SSHPort(nodepool), } } nodepoolInfos = append(nodepoolInfos, nodepoolInfo) diff --git a/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go b/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go index c18467cd6..15cafda79 100644 --- a/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go +++ b/services/kube-eleven/internal/worker/service/internal/kube-eleven/types.go @@ -21,7 +21,6 @@ type ( Zone string CloudProviderName string ProviderName string - SshPort int32 } // templateData struct holds the data which will be used in creating diff --git a/services/kube-eleven/templates/kubeone.tpl b/services/kube-eleven/templates/kubeone.tpl index 2281f205e..bb195a0f7 100644 --- a/services/kube-eleven/templates/kubeone.tpl +++ b/services/kube-eleven/templates/kubeone.tpl @@ -36,10 +36,10 @@ controlPlane: {{- if ge $nodeInfo.Node.NodeType 1}} - publicAddress: '{{ $nodeInfo.Node.Public }}' privateAddress: '{{ $nodeInfo.Node.Private }}' + sshPort: {{ nodeSshPort $nodeInfo.Node }} {{- if $nodepool.IsDynamic }} sshUsername: root sshPrivateKeyFile: '{{ $nodepool.NodepoolName }}.pem' - sshPort: {{ $nodepool.SshPort }} {{- else }} sshUsername: '{{ $nodeInfo.Node.Username }}' sshPrivateKeyFile: './{{ $nodeInfo.Name }}.pem' @@ -62,10 +62,10 @@ staticWorkers: {{- if eq $nodeInfo.Node.NodeType 0}} - publicAddress: '{{ $nodeInfo.Node.Public }}' privateAddress: '{{ $nodeInfo.Node.Private }}' + sshPort: {{ nodeSshPort $nodeInfo.Node }} {{- if $nodepool.IsDynamic }} sshUsername: root sshPrivateKeyFile: '{{ $nodepool.NodepoolName }}.pem' - sshPort: {{ $nodepool.SshPort }} {{- else }} sshUsername: '{{ $nodeInfo.Node.Username }}' sshPrivateKeyFile: './{{ $nodeInfo.Name }}.pem' From ebbfcc5112548ae896382aa62d26b2c115fbfa39 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Thu, 12 Mar 2026 17:16:06 +0100 Subject: [PATCH 15/16] Add sshPorts to dynamicaly populated networking templates with ssh port --- .../internal/cluster-builder/cluster_builder.go | 11 +++++++++++ .../worker/service/internal/templates/structures.go | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go index a8dab9312..e6a40fe83 100644 --- a/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go +++ b/services/terraformer/internal/worker/service/internal/cluster-builder/cluster_builder.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" comm "github.com/berops/claudie/internal/command" "github.com/berops/claudie/internal/fileutils" @@ -271,6 +272,15 @@ func (c *ClusterBuilder) generateFiles(clusterDir string) error { Fingerprint: hex.EncodeToString(hash.Digest128(filepath.Join(info.SpecName, path))), } + // Collect unique SSH ports from all nodepools for this provider. + var sshPorts []int32 + for _, np := range pools { + port := nodepools.SSHPort(np) + if !slices.Contains(sshPorts, port) { + sshPorts = append(sshPorts, port) + } + } + if err := g.GenerateNetworking(&templates.Networking{ ClusterData: clusterData, Provider: p, @@ -282,6 +292,7 @@ func (c *ClusterBuilder) generateFiles(clusterDir string) error { LBData: templates.LBData{ Roles: c.LBInfo.Roles, }, + SshPorts: sshPorts, }); err != nil { return fmt.Errorf("failed to generate networking_common template files: %w", err) } diff --git a/services/terraformer/internal/worker/service/internal/templates/structures.go b/services/terraformer/internal/worker/service/internal/templates/structures.go index 9596c3115..7d80e2214 100644 --- a/services/terraformer/internal/worker/service/internal/templates/structures.go +++ b/services/terraformer/internal/worker/service/internal/templates/structures.go @@ -177,6 +177,9 @@ type ( // for the firewall. // This data will be set if the ClusterType within ClusterData of this object is of type "LB". LBData LBData + // SshPorts holds the unique SSH ports used by the nodepools in this provider. + // Used to dynamically open firewall rules for the correct ports. + SshPorts []int32 } // Nodepools wraps all data related to generating terraform files to spawn VM instances as described @@ -254,7 +257,7 @@ type ( // value = { // {{- range $node := $nodepool.Nodes }} // {{- $serverResourceName := printf "%s_%s" $node.Name $resourceSuffix }} - // "${hcloud_server.{{ $serverResourceName }}.name}" = hcloud_server.{{ $serverResourceName }}.ipv4_address + // "${hcloud_server.{{ $serverResourceName }}.name}" = hcloud_server.{{ $serverResourceName }}.ipv4_address, "{{ $nodepool.SshPort }}" // {{- end }} // } //} From 07b308676068600b4fccf0ad9e94bd88840d39b2 Mon Sep 17 00:00:00 2001 From: Matus Brandys Date: Fri, 13 Mar 2026 08:59:39 +0100 Subject: [PATCH 16/16] Added NodeSSHPort funct for returning port acquired tofu output --- internal/nodepools/nodepools.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/nodepools/nodepools.go b/internal/nodepools/nodepools.go index 68c5fd00a..52bfe1846 100644 --- a/internal/nodepools/nodepools.go +++ b/internal/nodepools/nodepools.go @@ -32,6 +32,18 @@ func SSHPort(np *spec.NodePool) int32 { return np.GetSshPort() } +// NodeSSHPort returns the SSH port for a node based on its terraform output. +// Returns DefaultSSHPort (22) if not set. +func NodeSSHPort(node *spec.Node) int32 { + if node.GetSshPort() != "" { + var port int32 + if _, err := fmt.Sscanf(node.GetSshPort(), "%d", &port); err == nil && port > 0 { + return port + } + } + return DefaultSSHPort +} + type RegionNetwork struct { Region string ExternalNetwork string @@ -534,7 +546,7 @@ func RandomNodePublicEndpoint(nps []*spec.NodePool) (username, endpoint, key str if node.Username != "" && node.Username != username { username = node.Username } - sshPort = SSHPort(np) + sshPort = NodeSSHPort(node) switch npt := np.Type.(type) { case *spec.NodePool_DynamicNodePool: