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

✨ Add constant hostnames to bare metal servers #1285

Merged
merged 5 commits into from
May 3, 2024
Merged
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
2 changes: 2 additions & 0 deletions api/v1beta1/hetznercluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
// AllowEmptyControlPlaneAddressAnnotation allows HetznerCluster Webhook
// to skip some validation steps for externally managed control planes.
AllowEmptyControlPlaneAddressAnnotation = "capi.syself.com/allow-empty-control-plane-address"
// ConstantBareMetalHostnameAnnotation makes hostnames of bare metal servers constant.
ConstantBareMetalHostnameAnnotation = "capi.syself.com/constant-bare-metal-hostname"
)

// HetznerClusterSpec defines the desired state of HetznerCluster.
Expand Down
12 changes: 12 additions & 0 deletions controllers/hetznerbaremetalhost_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/conditions"
"sigs.k8s.io/cluster-api/util/predicates"
"sigs.k8s.io/cluster-api/util/record"
Expand Down Expand Up @@ -111,6 +112,15 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
}

log = log.WithValues("HetznerCluster", klog.KObj(hetznerCluster))

// Fetch the Cluster.
cluster, err := util.GetClusterFromMetadata(ctx, r.Client, hetznerCluster.ObjectMeta)
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get Cluster: %w", err)
}

log = log.WithValues("Cluster", klog.KObj(cluster))

ctx = ctrl.LoggerInto(ctx, log)

// Get Hetzner robot api credentials
Expand All @@ -128,11 +138,13 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
if res != emptyResult {
return res, nil
}

// Create the scope.
hostScope, err := scope.NewBareMetalHostScope(scope.BareMetalHostScopeParams{
Logger: log,
Client: r.Client,
HetznerCluster: hetznerCluster,
Cluster: cluster,
HetznerBareMetalHost: bmHost,
RobotClient: r.RobotClientFactory.NewClient(robotCreds),
SSHClientFactory: r.SSHClientFactory,
Expand Down
2 changes: 2 additions & 0 deletions controllers/hetznerbaremetalhost_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ var _ = Describe("HetznerBareMetalHostReconciler", func() {
UID: capiCluster.UID,
},
},
Labels: map[string]string{clusterv1.ClusterNameLabel: capiCluster.Name},
},
Spec: helpers.GetDefaultHetznerClusterSpec(),
}
Expand Down Expand Up @@ -647,6 +648,7 @@ var _ = Describe("HetznerBareMetalHostReconciler - missing secrets", func() {
UID: capiCluster.UID,
},
},
Labels: map[string]string{clusterv1.ClusterNameLabel: capiCluster.Name},
},
Spec: helpers.GetDefaultHetznerClusterSpec(),
}
Expand Down
2 changes: 1 addition & 1 deletion controllers/hetznerbaremetalmachine_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
UID: capiCluster.UID,
},
},
Labels: map[string]string{clusterv1.ClusterNameLabel: capiCluster.Name},
},
Spec: helpers.GetDefaultHetznerClusterSpec(),
}
Expand Down Expand Up @@ -160,7 +161,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
Err: nil,
})
osSSHClient.On("GetCloudInitOutput").Return(sshclient.Output{StdOut: "dummy content of /var/log/cloud-init-output.log"})

})

AfterEach(func() {
Expand Down
1 change: 1 addition & 0 deletions controllers/hetznerbaremetalremediation_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
UID: capiCluster.UID,
},
},
Labels: map[string]string{clusterv1.ClusterNameLabel: capiCluster.Name},
},
Spec: getDefaultHetznerClusterSpec(),
}
Expand Down
10 changes: 10 additions & 0 deletions docs/topics/hetzner-baremetal.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,13 @@ default my-cluster-md-1-cp2fd-7nld7 my-cluster bm-my-cluster-md-1-d75
default my-cluster-md-1-cp2fd-n74sm my-cluster bm-my-cluster-md-1-l5dnr hcloud://bm-2105469 Running 10h v1.27.7
```
Please note that hcloud servers are prefixed with `hcloud://` and baremetal servers are prefixed with `hcloud://bm-`.

## Advanced

### Constant hostnames for bare metal servers

In some cases it has advantages to fix the hostname and with it the names of nodes in your clusters. For cloud servers not so much as for bare metal servers, where there are storage integrations that allow you to use the storage of the bare metal servers and that work with fixed node names.

Therefore, there is the possibility to create a cluster that uses fixed node names for bare metal servers. Please note: this only applies to the bare metal servers and not to Hetzner Cloud servers.

You can trigger this feature by creating a `Cluster` with the annotation `"capi.syself.com/constant-bare-metal-hostname": "true"`. This is still an experimental feature but it should be save to use and to also update existing clusters with this annotation. All new machines will be created with this constant hostname.
22 changes: 22 additions & 0 deletions pkg/scope/baremetalhost.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"

infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
Expand All @@ -39,6 +40,7 @@ type BareMetalHostScopeParams struct {
Logger logr.Logger
HetznerBareMetalHost *infrav1.HetznerBareMetalHost
HetznerCluster *infrav1.HetznerCluster
Cluster *clusterv1.Cluster
RobotClient robotclient.Client
SSHClientFactory sshclient.Factory
OSSSHSecret *corev1.Secret
Expand All @@ -58,6 +60,9 @@ func NewBareMetalHostScope(params BareMetalHostScopeParams) (*BareMetalHostScope
if params.HetznerCluster == nil {
return nil, errors.New("cannot create baremetal host scope without Hetzner cluster")
}
if params.Cluster == nil {
return nil, errors.New("cannot create baremetal host scope without cluster")
}
if params.RobotClient == nil {
return nil, errors.New("cannot create baremetal host scope without robot client")
}
Expand All @@ -79,6 +84,7 @@ func NewBareMetalHostScope(params BareMetalHostScopeParams) (*BareMetalHostScope
RobotClient: params.RobotClient,
SSHClientFactory: params.SSHClientFactory,
HetznerCluster: params.HetznerCluster,
Cluster: params.Cluster,
HetznerBareMetalHost: params.HetznerBareMetalHost,
OSSSHSecret: params.OSSSHSecret,
RescueSSHSecret: params.RescueSSHSecret,
Expand All @@ -95,6 +101,7 @@ type BareMetalHostScope struct {
SSHClientFactory sshclient.Factory
HetznerBareMetalHost *infrav1.HetznerBareMetalHost
HetznerCluster *infrav1.HetznerCluster
Cluster *clusterv1.Cluster
OSSSHSecret *corev1.Secret
RescueSSHSecret *corev1.Secret
}
Expand Down Expand Up @@ -128,3 +135,18 @@ func (s *BareMetalHostScope) GetRawBootstrapData(ctx context.Context) ([]byte, e

return value, nil
}

// Hostname returns the desired host name.
func (s *BareMetalHostScope) Hostname() (hostname string) {
if s.hasConstantHostname() {
hostname = fmt.Sprintf("%s%s-%v", infrav1.BareMetalHostNamePrefix, s.Cluster.Name, s.HetznerBareMetalHost.Spec.ServerID)
} else {
hostname = infrav1.BareMetalHostNamePrefix + s.HetznerBareMetalHost.Spec.ConsumerRef.Name
}

return hostname
}

func (s *BareMetalHostScope) hasConstantHostname() bool {
janiskemper marked this conversation as resolved.
Show resolved Hide resolved
return s.Cluster.GetAnnotations()[infrav1.ConstantBareMetalHostnameAnnotation] == "true"
}
17 changes: 8 additions & 9 deletions pkg/services/baremetal/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -1172,12 +1172,10 @@ func (s *Service) createAutoSetupInput(sshClient sshclient.Client) (autoSetupInp
return autoSetupInput{}, s.recordActionFailure(infrav1.ProvisioningError, msg)
}

hostName := infrav1.BareMetalHostNamePrefix + s.scope.HetznerBareMetalHost.Spec.ConsumerRef.Name

// Create autosetup file
return autoSetupInput{
osDevices: deviceNames,
hostName: hostName,
hostName: s.scope.Hostname(),
image: imagePath,
}, nil
}
Expand Down Expand Up @@ -1206,7 +1204,7 @@ func (s *Service) actionProvisioning() actionResult {
})

// check hostname with sshClient
wantHostName := infrav1.BareMetalHostNamePrefix + host.Spec.ConsumerRef.Name
wantHostName := s.scope.Hostname()

out := sshClient.GetHostName()

Expand Down Expand Up @@ -1263,15 +1261,15 @@ func (s *Service) actionProvisioning() actionResult {
}

// we are in correct boot and can start provisioning
if failedAction := s.provision(sshClient, host.Spec.ConsumerRef.Name); failedAction != nil {
if failedAction := s.provision(sshClient); failedAction != nil {
return failedAction
}

host.ClearError()
return actionComplete{}
}

func (s *Service) provision(sshClient sshclient.Client, machineName string) actionResult {
func (s *Service) provision(sshClient sshclient.Client) actionResult {
{
out := sshClient.EnsureCloudInit()
if err := handleSSHError(out); err != nil {
Expand All @@ -1295,7 +1293,7 @@ func (s *Service) provision(sshClient sshclient.Client, machineName string) acti
return actionError{err: fmt.Errorf("failed to create no cloud directory: %w", err)}
}

if err := handleSSHError(sshClient.CreateMetaData(infrav1.BareMetalHostNamePrefix + machineName)); err != nil {
if err := handleSSHError(sshClient.CreateMetaData(s.scope.Hostname())); err != nil {
return actionError{err: fmt.Errorf("failed to create meta data: %w", err)}
}

Expand Down Expand Up @@ -1415,7 +1413,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
}
}()
// Check hostname with sshClient
wantHostName := infrav1.BareMetalHostNamePrefix + s.scope.HetznerBareMetalHost.Spec.ConsumerRef.Name
wantHostName := s.scope.Hostname()

out := sshClient.GetHostName()
if trimLineBreak(out.StdOut) != wantHostName {
Expand Down Expand Up @@ -1628,7 +1626,8 @@ func (s *Service) actionProvisioned() actionResult {
// Check hostname with sshClient
out := sshClient.GetHostName()

wantHostName := infrav1.BareMetalHostNamePrefix + s.scope.HetznerBareMetalHost.Spec.ConsumerRef.Name
wantHostName := s.scope.Hostname()

if trimLineBreak(out.StdOut) == wantHostName {
// Reboot has been successful
s.scope.HetznerBareMetalHost.Spec.Status.Rebooted = false
Expand Down
4 changes: 4 additions & 0 deletions pkg/services/baremetal/host/host_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2/textlogger"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"

infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
Expand Down Expand Up @@ -88,6 +90,8 @@ func newTestService(
HetznerCluster: &infrav1.HetznerCluster{
Spec: helpers.GetDefaultHetznerClusterSpec(),
},
// Attention: this doesn't make sense if we test with constant node names
Cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster"}},
OSSSHSecret: osSSHSecret,
RescueSSHSecret: rescueSSHSecret,
},
Expand Down
Loading