Skip to content

Commit 14451c8

Browse files
PR helps better tracking of inventory and cluster resources by adding a
mutating webhook in seeder. The webhook extracts userinfo from the authentication info and labels the same in cluster object. The inventory and cluster controllers then ensure that this info is synced to the associated inventory object when a cluster is created/updated and cleaned from inventory when cluster is deleted
1 parent 9cb5a89 commit 14451c8

File tree

9 files changed

+214
-5
lines changed

9 files changed

+214
-5
lines changed

chart/seeder/templates/rbac.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ rules:
9292
- admissionregistration.k8s.io
9393
resources:
9494
- validatingwebhookconfigurations
95+
- mutatingwebhookconfigurations
9596
verbs:
9697
- get
9798
- watch

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ require (
101101
require (
102102
github.com/beorn7/perks v1.0.1 // indirect
103103
github.com/cespare/xxhash/v2 v2.2.0 // indirect
104-
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
104+
github.com/evanphx/json-patch v5.6.0+incompatible
105105
github.com/fsnotify/fsnotify v1.6.0 // indirect
106106
github.com/ghodss/yaml v1.0.0 // indirect
107107
github.com/go-logr/zapr v1.2.4 // indirect

pkg/api/v1alpha1/common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ const (
2727
DefaultEndpointPort = 9090
2828
DefaultSeederDeploymentService = "harvester-seeder-endpoint"
2929
DefaultHegelDeploymentEndpointLookup = "smee"
30+
ClusterOwnerKey = "harvesterhci.io/clusterOwner"
31+
ClusterOwnerDetailsKey = "harvesterhci.io/clusterOwnerDetails"
32+
ExtraFieldKey = "username"
3033
)

pkg/controllers/cluster_controller.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ func (r *ClusterReconciler) patchNodesAndPools(ctx context.Context, cObj *seeder
192192
continue
193193
}
194194

195+
i, err = r.patchInventoryOwnership(ctx, cObj, i)
196+
if err != nil {
197+
return fmt.Errorf("error updating inventory ownership: %v", err)
198+
}
195199
var found bool
196200
var nodeAddress string
197201
for address, nodeDetails := range pool.Status.AddressAllocation {
@@ -659,3 +663,38 @@ func (r *ClusterReconciler) createOrUpdateHardware(ctx context.Context, hardware
659663

660664
return createOrUpdateInventoryConditions(ctx, inventory, seederv1alpha1.TinkHardwareCreated, "tink hardware created", r.Client)
661665
}
666+
667+
// patchInventoryOwnership looks up ownership info from cluster object
668+
// and labels the inventory with same info
669+
func (r *ClusterReconciler) patchInventoryOwnership(ctx context.Context, c *seederv1alpha1.Cluster, i *seederv1alpha1.Inventory) (*seederv1alpha1.Inventory, error) {
670+
if c.Annotations == nil {
671+
return i, nil
672+
}
673+
674+
clusterOwner, ok := c.Labels[seederv1alpha1.ClusterOwnerKey]
675+
// no ownership details found on cluster, so no further action required
676+
if !ok {
677+
return i, nil
678+
}
679+
680+
if i.Labels == nil {
681+
i.Labels = make(map[string]string)
682+
}
683+
// apply user info
684+
i.Labels[seederv1alpha1.ClusterOwnerKey] = clusterOwner
685+
686+
// apply user details
687+
clusterOwnerDetails, ok := c.Labels[seederv1alpha1.ClusterOwnerDetailsKey]
688+
if ok {
689+
i.Labels[seederv1alpha1.ClusterOwnerDetailsKey] = clusterOwnerDetails
690+
}
691+
692+
err := r.Client.Update(ctx, i)
693+
if err != nil {
694+
return i, fmt.Errorf("error updating inventory with ownership details: %v", err)
695+
}
696+
697+
iObj := &seederv1alpha1.Inventory{}
698+
err = r.Client.Get(ctx, types.NamespacedName{Name: i.Name, Namespace: i.Namespace}, iObj)
699+
return iObj, err
700+
}

pkg/controllers/inventory_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ func (r *InventoryReconciler) reconcileBMCJob(ctx context.Context, iObj *seederv
313313
func (r *InventoryReconciler) inventoryFreed(ctx context.Context, iObj *seederv1alpha1.Inventory) error {
314314
i := iObj.DeepCopy()
315315
if util.ConditionExists(i, seederv1alpha1.InventoryFreed) {
316+
if i.Labels != nil {
317+
if _, ok := i.Labels[seederv1alpha1.ClusterOwnerKey]; ok {
318+
delete(i.Labels, seederv1alpha1.ClusterOwnerKey)
319+
delete(i.Labels, seederv1alpha1.ClusterOwnerDetailsKey)
320+
return r.Update(ctx, i)
321+
}
322+
}
316323
j := util.GenerateJob(i.Name, i.Namespace, "shutdown")
317324
if err := r.jobWrapper(ctx, i, j); err != nil {
318325
return err

pkg/data/data.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/webhook/cluster_mutator.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package webhook
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
werror "github.com/harvester/webhook/pkg/error"
8+
"github.com/harvester/webhook/pkg/server/admission"
9+
admissionregv1 "k8s.io/api/admissionregistration/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/manager"
13+
14+
seederv1alpha1 "github.com/harvester/seeder/pkg/api/v1alpha1"
15+
)
16+
17+
type ClusterMutator struct {
18+
client client.Client
19+
ctx context.Context
20+
admission.DefaultMutator
21+
}
22+
23+
func NewClusterMutator(ctx context.Context, mgr manager.Manager) *ClusterMutator {
24+
return &ClusterMutator{
25+
client: mgr.GetClient(),
26+
ctx: ctx,
27+
}
28+
}
29+
30+
func (c *ClusterMutator) Resource() admission.Resource {
31+
return admission.Resource{
32+
Names: []string{"clusters"},
33+
Scope: admissionregv1.NamespacedScope,
34+
APIGroup: seederv1alpha1.GroupVersion.Group,
35+
APIVersion: seederv1alpha1.GroupVersion.Version,
36+
ObjectType: &seederv1alpha1.Cluster{},
37+
OperationTypes: []admissionregv1.OperationType{
38+
admissionregv1.Create,
39+
},
40+
}
41+
}
42+
43+
func (c *ClusterMutator) Create(req *admission.Request, newObj runtime.Object) (admission.Patch, error) {
44+
clusterObj, ok := newObj.(*seederv1alpha1.Cluster)
45+
if !ok {
46+
return nil, werror.NewBadRequest("unable to assert object to Cluster Object")
47+
}
48+
var patchOps admission.Patch
49+
userName := req.UserInfo.Username
50+
51+
// if there is an existing owner reference, then replace it with details in request
52+
labels := clusterObj.GetLabels()
53+
if labels == nil {
54+
labels = make(map[string]string)
55+
}
56+
57+
labels[seederv1alpha1.ClusterOwnerKey] = userName
58+
val, ok := req.UserInfo.Extra[seederv1alpha1.ExtraFieldKey]
59+
if ok {
60+
labels[seederv1alpha1.ClusterOwnerDetailsKey] = strings.Join(val, " ")
61+
}
62+
63+
patchOps = append(patchOps, admission.PatchOp{
64+
Op: admission.PatchOpAdd,
65+
Path: "/metadata/labels",
66+
Value: labels,
67+
})
68+
return patchOps, nil
69+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package webhook
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"testing"
7+
8+
seederv1alpha1 "github.com/harvester/seeder/pkg/api/v1alpha1"
9+
"github.com/harvester/webhook/pkg/server/admission"
10+
"github.com/rancher/wrangler/pkg/webhook"
11+
"github.com/stretchr/testify/require"
12+
v1 "k8s.io/api/admission/v1"
13+
authenticationv1 "k8s.io/api/authentication/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/runtime"
16+
"k8s.io/apimachinery/pkg/types"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
18+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
19+
)
20+
21+
var (
22+
clusterObj = &seederv1alpha1.Cluster{
23+
ObjectMeta: metav1.ObjectMeta{
24+
Name: "cluster",
25+
},
26+
}
27+
28+
userName = "dev"
29+
30+
request = &admission.Request{
31+
Request: &webhook.Request{
32+
AdmissionRequest: v1.AdmissionRequest{
33+
UserInfo: authenticationv1.UserInfo{
34+
Username: userName,
35+
},
36+
},
37+
},
38+
}
39+
)
40+
41+
func Test_ClusterMutation(t *testing.T) {
42+
type testCases struct {
43+
name string
44+
generateClusterObject func(*seederv1alpha1.Cluster) *seederv1alpha1.Cluster
45+
}
46+
47+
var cases = []testCases{
48+
{
49+
name: "no cluster owner defined",
50+
generateClusterObject: func(cluster *seederv1alpha1.Cluster) *seederv1alpha1.Cluster { return cluster },
51+
},
52+
{
53+
name: "cluster object has an owner defined",
54+
generateClusterObject: func(cluster *seederv1alpha1.Cluster) *seederv1alpha1.Cluster {
55+
clusterCopy := cluster.DeepCopy()
56+
clusterCopy.Labels = map[string]string{
57+
seederv1alpha1.ClusterOwnerKey: "demo",
58+
}
59+
return clusterCopy
60+
},
61+
},
62+
}
63+
64+
for _, testCase := range cases {
65+
assert := require.New(t)
66+
scheme := runtime.NewScheme()
67+
err := seederv1alpha1.AddToScheme(scheme)
68+
assert.NoError(err, testCase.name)
69+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(testCase.generateClusterObject(clusterObj)).Build()
70+
c := &ClusterMutator{
71+
client: fakeClient,
72+
ctx: context.TODO(),
73+
}
74+
ops, err := c.Create(request, clusterObj)
75+
assert.NoError(err, testCase.name)
76+
assert.Len(ops, 1, testCase.name)
77+
// test out by patching the object
78+
opsByte, err := json.Marshal(ops)
79+
assert.NoError(err, testCase.name)
80+
err = c.client.Patch(context.TODO(), clusterObj, client.RawPatch(types.JSONPatchType, opsByte))
81+
assert.NoError(err, testCase.name)
82+
updatedObj := &seederv1alpha1.Cluster{}
83+
err = c.client.Get(context.TODO(), types.NamespacedName{Name: clusterObj.Name, Namespace: clusterObj.Namespace}, updatedObj)
84+
assert.NoError(err, testCase.name)
85+
assert.Equal(updatedObj.GetLabels()[seederv1alpha1.ClusterOwnerKey], userName, testCase.name)
86+
}
87+
}

pkg/webhook/setup.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func SetupWebhookServer(ctx context.Context, mgr manager.Manager, namespace stri
3030
return err
3131
}
3232

33+
if err := webhookServer.RegisterMutators(NewClusterMutator(ctx, mgr)); err != nil {
34+
return err
35+
}
3336
// since webhook and manager start run as two go routines, need to wait for caches to sync
3437
// before starting the server
3538
for {

0 commit comments

Comments
 (0)