Skip to content

Commit f040fc4

Browse files
authored
Merge pull request kubernetes-sigs#1303 from mercedes-benz/tobiasgiese/bastion-recreation
✨ Add re-creation of bastion host on change
2 parents 0516ace + 9c9365e commit f040fc4

File tree

7 files changed

+105
-145
lines changed

7 files changed

+105
-145
lines changed

api/v1alpha6/openstackcluster_webhook.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,9 @@ func (r *OpenStackCluster) ValidateUpdate(oldRaw runtime.Object) error {
105105
r.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{}
106106
}
107107

108-
// Allow changes to the bastion spec only if no bastion host is deployed (i.e. Spec.Bastion.Enabled=false).
109-
if old.Status.Bastion == nil {
110-
old.Spec.Bastion = &Bastion{}
111-
r.Spec.Bastion = &Bastion{}
112-
}
113-
114-
// Allow toggling the bastion enabled flag.
115-
if old.Spec.Bastion != nil && r.Spec.Bastion != nil {
116-
old.Spec.Bastion.Enabled = true
117-
r.Spec.Bastion.Enabled = true
118-
}
108+
// Allow changes to the bastion spec.
109+
old.Spec.Bastion = &Bastion{}
110+
r.Spec.Bastion = &Bastion{}
119111

120112
// Allow changes on AllowedCIDRs
121113
if r.Spec.APIServerLoadBalancer.Enabled {

api/v1alpha6/openstackcluster_webhook_test.go

Lines changed: 1 addition & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -112,90 +112,7 @@ func TestOpenStackCluster_ValidateUpdate(t *testing.T) {
112112
wantErr: true,
113113
},
114114
{
115-
name: "Toggle OpenStackCluster.Spec.Bastion.Enabled flag is allowed",
116-
oldTemplate: &OpenStackCluster{
117-
Spec: OpenStackClusterSpec{
118-
CloudName: "foobar",
119-
Bastion: &Bastion{
120-
Instance: OpenStackMachineSpec{
121-
CloudName: "foobar",
122-
Image: "foobar",
123-
Flavor: "minimal",
124-
},
125-
Enabled: false,
126-
},
127-
},
128-
},
129-
newTemplate: &OpenStackCluster{
130-
Spec: OpenStackClusterSpec{
131-
CloudName: "foobar",
132-
Bastion: &Bastion{
133-
Instance: OpenStackMachineSpec{
134-
CloudName: "foobarbaz",
135-
Image: "foobarbaz",
136-
Flavor: "medium",
137-
},
138-
Enabled: true,
139-
},
140-
},
141-
},
142-
wantErr: false,
143-
},
144-
{
145-
name: "Changing empty OpenStackCluster.Spec.Bastion is allowed",
146-
oldTemplate: &OpenStackCluster{
147-
Spec: OpenStackClusterSpec{
148-
CloudName: "foobar",
149-
},
150-
},
151-
newTemplate: &OpenStackCluster{
152-
Spec: OpenStackClusterSpec{
153-
CloudName: "foobar",
154-
Bastion: &Bastion{
155-
Instance: OpenStackMachineSpec{
156-
CloudName: "foobar",
157-
Image: "foobar",
158-
Flavor: "medium",
159-
},
160-
Enabled: true,
161-
},
162-
},
163-
},
164-
wantErr: false,
165-
},
166-
{
167-
name: "Changing OpenStackCluster.Spec.Bastion with no deployed bastion host is allowed",
168-
oldTemplate: &OpenStackCluster{
169-
Spec: OpenStackClusterSpec{
170-
CloudName: "foobar",
171-
Bastion: &Bastion{
172-
Instance: OpenStackMachineSpec{
173-
CloudName: "foobar",
174-
Image: "foobar",
175-
Flavor: "minimal",
176-
},
177-
Enabled: false,
178-
},
179-
},
180-
Status: OpenStackClusterStatus{},
181-
},
182-
newTemplate: &OpenStackCluster{
183-
Spec: OpenStackClusterSpec{
184-
CloudName: "foobar",
185-
Bastion: &Bastion{
186-
Instance: OpenStackMachineSpec{
187-
CloudName: "foobarbaz",
188-
Image: "foobarbaz",
189-
Flavor: "medium",
190-
},
191-
Enabled: true,
192-
},
193-
},
194-
},
195-
wantErr: false,
196-
},
197-
{
198-
name: "Changing OpenStackCluster.Spec.Bastion with deployed bastion host is not allowed",
115+
name: "Changing OpenStackCluster.Spec.Bastion is allowed",
199116
oldTemplate: &OpenStackCluster{
200117
Spec: OpenStackClusterSpec{
201118
CloudName: "foobar",
@@ -227,46 +144,6 @@ func TestOpenStackCluster_ValidateUpdate(t *testing.T) {
227144
},
228145
},
229146
},
230-
wantErr: true,
231-
},
232-
{
233-
name: "Disabling the OpenStackCluster.Spec.Bastion while it's running is allowed",
234-
oldTemplate: &OpenStackCluster{
235-
Spec: OpenStackClusterSpec{
236-
CloudName: "foobar",
237-
Bastion: &Bastion{
238-
Instance: OpenStackMachineSpec{
239-
CloudName: "foobar",
240-
Image: "foobar",
241-
Flavor: "minimal",
242-
},
243-
Enabled: true,
244-
},
245-
},
246-
Status: OpenStackClusterStatus{
247-
Bastion: &Instance{
248-
Name: "foobar",
249-
},
250-
},
251-
},
252-
newTemplate: &OpenStackCluster{
253-
Spec: OpenStackClusterSpec{
254-
CloudName: "foobar",
255-
Bastion: &Bastion{
256-
Instance: OpenStackMachineSpec{
257-
CloudName: "foobar",
258-
Image: "foobar",
259-
Flavor: "minimal",
260-
},
261-
Enabled: false,
262-
},
263-
},
264-
Status: OpenStackClusterStatus{
265-
Bastion: &Instance{
266-
Name: "foobar",
267-
},
268-
},
269-
},
270147
wantErr: false,
271148
},
272149
{

controllers/openstackcluster_controller.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ import (
5151
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
5252
)
5353

54+
const (
55+
BastionInstanceHashAnnotation = "infrastructure.cluster.x-k8s.io/bastion-hash"
56+
)
57+
5458
// OpenStackClusterReconciler reconciles a OpenStackCluster object.
5559
type OpenStackClusterReconciler struct {
5660
Client client.Client
@@ -242,6 +246,8 @@ func deleteBastion(scope *scope.Scope, cluster *clusterv1.Cluster, openStackClus
242246
}
243247
openStackCluster.Status.BastionSecurityGroup = nil
244248

249+
delete(openStackCluster.ObjectMeta.Annotations, BastionInstanceHashAnnotation)
250+
245251
return nil
246252
}
247253

@@ -313,20 +319,35 @@ func reconcileBastion(scope *scope.Scope, cluster *clusterv1.Cluster, openStackC
313319
return err
314320
}
315321

322+
instanceSpec := bastionToInstanceSpec(openStackCluster, cluster.Name)
323+
bastionHash, err := compute.HashInstanceSpec(instanceSpec)
324+
if err != nil {
325+
return errors.Wrap(err, "failed computing bastion hash from instance spec")
326+
}
327+
316328
instanceStatus, err := computeService.GetInstanceStatusByName(openStackCluster, fmt.Sprintf("%s-bastion", cluster.Name))
317329
if err != nil {
318330
return err
319331
}
320332
if instanceStatus != nil {
321-
bastion, err := instanceStatus.APIInstance(openStackCluster)
322-
if err != nil {
333+
if !bastionHashHasChanged(bastionHash, openStackCluster.ObjectMeta.Annotations) {
334+
bastion, err := instanceStatus.APIInstance(openStackCluster)
335+
if err != nil {
336+
return err
337+
}
338+
// Add the current hash if no annotation is set.
339+
if _, ok := openStackCluster.ObjectMeta.Annotations[BastionInstanceHashAnnotation]; !ok {
340+
annotations.AddAnnotations(openStackCluster, map[string]string{BastionInstanceHashAnnotation: bastionHash})
341+
}
342+
openStackCluster.Status.Bastion = bastion
343+
return nil
344+
}
345+
346+
if err := deleteBastion(scope, cluster, openStackCluster); err != nil {
323347
return err
324348
}
325-
openStackCluster.Status.Bastion = bastion
326-
return nil
327349
}
328350

329-
instanceSpec := bastionToInstanceSpec(openStackCluster, cluster.Name)
330351
instanceStatus, err = computeService.CreateInstance(openStackCluster, openStackCluster, instanceSpec, cluster.Name)
331352
if err != nil {
332353
return errors.Errorf("failed to reconcile bastion: %v", err)
@@ -360,6 +381,7 @@ func reconcileBastion(scope *scope.Scope, cluster *clusterv1.Cluster, openStackC
360381
}
361382
bastion.FloatingIP = fp.FloatingIP
362383
openStackCluster.Status.Bastion = bastion
384+
annotations.AddAnnotations(openStackCluster, map[string]string{BastionInstanceHashAnnotation: bastionHash})
363385
return nil
364386
}
365387

@@ -390,6 +412,15 @@ func bastionToInstanceSpec(openStackCluster *infrav1.OpenStackCluster, clusterNa
390412
return instanceSpec
391413
}
392414

415+
// bastionHashHasChanged returns a boolean whether if the latest bastion hash, built from the instance spec, has changed or not.
416+
func bastionHashHasChanged(computeHash string, clusterAnnotations map[string]string) bool {
417+
latestHash, ok := clusterAnnotations[BastionInstanceHashAnnotation]
418+
if !ok {
419+
return false
420+
}
421+
return latestHash != computeHash
422+
}
423+
393424
func reconcileNetworkComponents(scope *scope.Scope, cluster *clusterv1.Cluster, openStackCluster *infrav1.OpenStackCluster) error {
394425
clusterName := fmt.Sprintf("%s-%s", cluster.Namespace, cluster.Name)
395426

docs/book/src/clusteropenstack/configuration.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,9 @@ spec:
475475
sshKeyName: <Key pair name>
476476
```
477477

478-
The `enabled` flag is toggleable. Thus, you're able to save resources while the bastion host is not needed.
479-
All other parameters can be changed via an `OpenStackCluster` update while the bastion host is not running.
480-
481-
> Note: as a rolling update is not ideal during a bastion host session, we prevent changes to a running bastion configuration.
478+
All parameters are mutable during the runtime of the bastion host.
479+
The bastion host will be re-created if it's enabled and the instance spec has been changed.
480+
This is done by a simple checksum validation of the instance spec which is stored in the `OpenStackCluster` annotation `infrastructure.cluster.x-k8s.io/bastion-hash`.
482481

483482
A floating IP is created and associated to the bastion host automatically, but you can add the IP address explicitly:
484483

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.17
44

55
require (
66
github.com/blang/semver v3.5.1+incompatible
7+
github.com/davecgh/go-spew v1.1.1
78
github.com/go-logr/logr v1.2.0
89
github.com/golang/mock v1.6.0
910
github.com/google/gofuzz v1.2.0
@@ -47,7 +48,6 @@ require (
4748
github.com/cespare/xxhash/v2 v2.1.2 // indirect
4849
github.com/coredns/caddy v1.1.0 // indirect
4950
github.com/coredns/corefile-migration v1.0.17 // indirect
50-
github.com/davecgh/go-spew v1.1.1 // indirect
5151
github.com/docker/distribution v2.8.1+incompatible // indirect
5252
github.com/docker/docker v20.10.17+incompatible // indirect
5353
github.com/docker/go-connections v0.4.0 // indirect

pkg/cloud/services/compute/instance.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha6"
3636
"sigs.k8s.io/cluster-api-provider-openstack/pkg/record"
3737
capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
38+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/hash"
3839
)
3940

4041
const (
@@ -722,3 +723,11 @@ func (s *Service) isTrunkExtSupported() (trunknSupported bool, err error) {
722723
}
723724
return true, nil
724725
}
726+
727+
func HashInstanceSpec(computeInstance *InstanceSpec) (string, error) {
728+
instanceHash, err := hash.ComputeSpewHash(computeInstance)
729+
if err != nil {
730+
return "", err
731+
}
732+
return strconv.Itoa(int(instanceHash)), nil
733+
}

pkg/utils/hash/hash.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hash
18+
19+
import (
20+
"fmt"
21+
"hash"
22+
"hash/fnv"
23+
24+
"github.com/davecgh/go-spew/spew"
25+
)
26+
27+
// SpewHashObject writes specified object to hash using the spew library
28+
// which follows pointers and prints actual values of the nested objects
29+
// ensuring the hash does not change when a pointer changes.
30+
func SpewHashObject(hasher hash.Hash, objectToWrite interface{}) error {
31+
hasher.Reset()
32+
printer := spew.ConfigState{
33+
Indent: " ",
34+
SortKeys: true,
35+
DisableMethods: true,
36+
SpewKeys: true,
37+
}
38+
39+
if _, err := printer.Fprintf(hasher, "%#v", objectToWrite); err != nil {
40+
return fmt.Errorf("failed to write object to hasher")
41+
}
42+
return nil
43+
}
44+
45+
// ComputeSpewHash computes the hash of a InstanceSpec using the spew library.
46+
func ComputeSpewHash(objectToWrite interface{}) (uint32, error) {
47+
instanceSpecHasher := fnv.New32a()
48+
if err := SpewHashObject(instanceSpecHasher, objectToWrite); err != nil {
49+
return 0, err
50+
}
51+
return instanceSpecHasher.Sum32(), nil
52+
}

0 commit comments

Comments
 (0)