Skip to content

Commit 6a05524

Browse files
fix: adding predelete check in konnector agent delete webhook (#1438)
**What problem does this PR solve?**: fix: adding predelete check in konnector agent delete webhook **Which issue(s) this PR fixes**: Fixes # https://jira.nutanix.com/browse/NCN-111450 **How Has This Been Tested?**: <!-- Please describe the tests that you ran to verify your changes. Provide output from the tests and any manual steps needed to replicate the tests. --> **Special notes for your reviewer**: <!-- Use this to provide any additional information to the reviewers. This may include: - Best way to review the PR. - Where the author wants the most review attention on. - etc. -->
1 parent ba01bda commit 6a05524

File tree

4 files changed

+385
-0
lines changed

4 files changed

+385
-0
lines changed

pkg/handlers/lifecycle/konnectoragent/handler.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ import (
1717
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1818
apierrors "k8s.io/apimachinery/pkg/api/errors"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/apimachinery/pkg/types"
2021
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
22+
"sigs.k8s.io/cluster-api/controllers/remote"
2123
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
2224
ctrl "sigs.k8s.io/controller-runtime"
2325
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2426

27+
prismgoclient "github.com/nutanix-cloud-native/prism-go-client"
28+
2529
capxv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
2630
caaphv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
2731
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
@@ -31,6 +35,7 @@ import (
3135
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables"
3236
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/lifecycle/addons"
3337
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/lifecycle/config"
38+
lifecycleutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/lifecycle/utils"
3439
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
3540
handlersutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/utils"
3641
)
@@ -465,6 +470,20 @@ func (n *DefaultKonnectorAgent) BeforeClusterDelete(
465470
return
466471
}
467472

473+
// Check cluster is registered in PC
474+
clusterRegistered, err := isClusterRegisteredInPC(ctx, n.client, cluster, log)
475+
if err != nil {
476+
log.Error(err, "Failed to check if cluster is registered in Prism Central, continuing with deletion anyway")
477+
// setting response status to success to allow cluster deletion to proceed
478+
resp.SetStatus(runtimehooksv1.ResponseStatusSuccess)
479+
return
480+
}
481+
if !clusterRegistered {
482+
log.Info("Cluster is not registered in Prism Central, skipping cleanup")
483+
resp.SetStatus(runtimehooksv1.ResponseStatusSuccess)
484+
return
485+
}
486+
468487
// Check if cleanup is already in progress or completed
469488
cleanupStatus, statusMsg, err := n.checkCleanupStatus(ctx, cluster, log)
470489
if err != nil {
@@ -651,3 +670,112 @@ func (n *DefaultKonnectorAgent) checkCleanupStatus(
651670
log.Info("HelmChartProxy exists, cleanup not started", "name", hcp.Name)
652671
return cleanupStatusNotStarted, "HelmChartProxy exists and needs to be deleted", nil
653672
}
673+
674+
// isClusterRegisteredInPC checks if the cluster is registered in Prism Central by calling
675+
// the Konnector GetClusterRegistration API using the cluster's kube-system namespace UUID.
676+
func isClusterRegisteredInPC(
677+
ctx context.Context,
678+
client ctrlclient.Client,
679+
cluster *clusterv1.Cluster,
680+
log logr.Logger,
681+
) (bool, error) {
682+
// Get cluster config to extract PC endpoint
683+
varMap := variables.ClusterVariablesToVariablesMap(cluster.Spec.Topology.Variables)
684+
clusterConfigVar, err := variables.Get[apivariables.ClusterConfigSpec](
685+
varMap,
686+
v1alpha1.ClusterConfigVariableName,
687+
)
688+
if err != nil {
689+
return false, fmt.Errorf("failed to read clusterConfig variable: %w", err)
690+
}
691+
692+
if clusterConfigVar.Nutanix == nil || clusterConfigVar.Nutanix.PrismCentralEndpoint.URL == "" {
693+
return false, fmt.Errorf("prism central endpoint not configured")
694+
}
695+
696+
prismCentralEndpointSpec := clusterConfigVar.Nutanix.PrismCentralEndpoint
697+
host, port, err := prismCentralEndpointSpec.ParseURL()
698+
if err != nil {
699+
return false, fmt.Errorf("failed to parse prism central endpoint URL: %w", err)
700+
}
701+
702+
// Get konnector agent variable to access its credentials secret
703+
k8sAgentVar, err := variables.Get[apivariables.NutanixKonnectorAgent](
704+
varMap,
705+
v1alpha1.ClusterConfigVariableName,
706+
"addons", v1alpha1.KonnectorAgentVariableName,
707+
)
708+
if err != nil {
709+
return false, fmt.Errorf("failed to read konnector agent variable: %w", err)
710+
}
711+
712+
if k8sAgentVar.Credentials == nil || k8sAgentVar.Credentials.SecretRef.Name == "" {
713+
return false, fmt.Errorf("konnector agent credentials secret not configured")
714+
}
715+
716+
// Get credentials from konnector agent addon Secret
717+
credentialsSecret := &corev1.Secret{}
718+
err = client.Get(ctx, types.NamespacedName{
719+
Namespace: cluster.Namespace,
720+
Name: k8sAgentVar.Credentials.SecretRef.Name,
721+
}, credentialsSecret)
722+
if err != nil {
723+
return false, fmt.Errorf("failed to get credentials secret: %w", err)
724+
}
725+
726+
usernameData, ok := credentialsSecret.Data["username"]
727+
if !ok {
728+
return false, fmt.Errorf("credentials secret does not contain 'username' key")
729+
}
730+
passwordData, ok := credentialsSecret.Data["password"]
731+
if !ok {
732+
return false, fmt.Errorf("credentials secret does not contain 'password' key")
733+
}
734+
735+
// Create credentials struct
736+
credentials := prismgoclient.Credentials{
737+
Endpoint: fmt.Sprintf("%s:%d", host, port),
738+
URL: fmt.Sprintf("https://%s:%d", host, port),
739+
Username: string(usernameData),
740+
Password: string(passwordData),
741+
Insecure: prismCentralEndpointSpec.Insecure,
742+
Port: fmt.Sprintf("%d", port),
743+
}
744+
745+
// Get kube-system namespace UUID from the cluster
746+
clusterKey := ctrlclient.ObjectKeyFromObject(cluster)
747+
remoteClient, err := remote.NewClusterClient(ctx, "", client, clusterKey)
748+
if err != nil {
749+
return false, fmt.Errorf("failed to create remote cluster client: %w", err)
750+
}
751+
752+
kubeSystemNS := &corev1.Namespace{}
753+
err = remoteClient.Get(ctx, types.NamespacedName{Name: "kube-system"}, kubeSystemNS)
754+
if err != nil {
755+
return false, fmt.Errorf("failed to get kube-system namespace from cluster(%s): %w", cluster.Name, err)
756+
}
757+
758+
clusterUUID := string(kubeSystemNS.UID)
759+
760+
// Get trust bundle if insecure is false
761+
var trustBundle string
762+
if !prismCentralEndpointSpec.Insecure {
763+
trustBundle = prismCentralEndpointSpec.AdditionalTrustBundle
764+
}
765+
766+
// Create Prism Central Konnector client
767+
prismCentralKonnectorClient, err := lifecycleutils.NewPrismCentralKonnectorClient(&credentials, trustBundle)
768+
if err != nil {
769+
return false, fmt.Errorf("failed to create prism central konnector client: %w", err)
770+
}
771+
772+
// Call GetClusterRegistration API
773+
_, err = prismCentralKonnectorClient.GetClusterRegistration(ctx, clusterUUID)
774+
if err != nil {
775+
return false, fmt.Errorf("failed to get cluster(%s) registration: %w", clusterUUID, err)
776+
}
777+
778+
// If we got here, the cluster is registered
779+
log.Info("Cluster is registered in Prism Central", "clusterUUID", clusterUUID)
780+
return true, nil
781+
}

pkg/handlers/lifecycle/konnectoragent/variables_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"testing"
99

10+
"github.com/go-logr/logr"
1011
"github.com/spf13/pflag"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
@@ -1165,3 +1166,143 @@ func TestFormatCategoriesFromSlice(t *testing.T) {
11651166
})
11661167
}
11671168
}
1169+
1170+
// Test isClusterRegisteredInPC function
1171+
func TestIsClusterRegisteredInPC_MissingClusterConfig(t *testing.T) {
1172+
client := fake.NewClientBuilder().WithScheme(testScheme).Build()
1173+
cluster := &clusterv1.Cluster{
1174+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
1175+
Spec: clusterv1.ClusterSpec{
1176+
Topology: &clusterv1.Topology{
1177+
Variables: []clusterv1.ClusterVariable{},
1178+
},
1179+
},
1180+
}
1181+
1182+
registered, err := isClusterRegisteredInPC(context.Background(), client, cluster, logr.Discard())
1183+
1184+
assert.Error(t, err)
1185+
assert.False(t, registered)
1186+
assert.Contains(t, err.Error(), "failed to read clusterConfig variable")
1187+
}
1188+
1189+
func TestIsClusterRegisteredInPC_MissingCredentialsSecret(t *testing.T) {
1190+
client := fake.NewClientBuilder().WithScheme(testScheme).Build()
1191+
cluster := &clusterv1.Cluster{
1192+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
1193+
Spec: clusterv1.ClusterSpec{
1194+
Topology: &clusterv1.Topology{
1195+
Variables: []clusterv1.ClusterVariable{{
1196+
Name: v1alpha1.ClusterConfigVariableName,
1197+
Value: apiextensionsv1.JSON{Raw: []byte(`{
1198+
"nutanix": {
1199+
"prismCentralEndpoint": {
1200+
"url": "https://prism-central.example.com:9440",
1201+
"insecure": true
1202+
}
1203+
},
1204+
"addons": {
1205+
"konnectorAgent": {
1206+
"credentials": { "secretRef": {"name":"missing-secret"} }
1207+
}
1208+
}
1209+
}`)},
1210+
}},
1211+
},
1212+
},
1213+
}
1214+
1215+
registered, err := isClusterRegisteredInPC(context.Background(), client, cluster, logr.Discard())
1216+
1217+
assert.Error(t, err)
1218+
assert.False(t, registered)
1219+
assert.Contains(t, err.Error(), "failed to get credentials secret")
1220+
}
1221+
1222+
func TestIsClusterRegisteredInPC_MissingUsernameInSecret(t *testing.T) {
1223+
client := fake.NewClientBuilder().WithScheme(testScheme).Build()
1224+
secret := &corev1.Secret{
1225+
ObjectMeta: metav1.ObjectMeta{
1226+
Name: "test-secret",
1227+
Namespace: "default",
1228+
},
1229+
Data: map[string][]byte{
1230+
"password": []byte("testpass"),
1231+
},
1232+
}
1233+
require.NoError(t, client.Create(context.Background(), secret))
1234+
1235+
cluster := &clusterv1.Cluster{
1236+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
1237+
Spec: clusterv1.ClusterSpec{
1238+
Topology: &clusterv1.Topology{
1239+
Variables: []clusterv1.ClusterVariable{{
1240+
Name: v1alpha1.ClusterConfigVariableName,
1241+
Value: apiextensionsv1.JSON{Raw: []byte(`{
1242+
"nutanix": {
1243+
"prismCentralEndpoint": {
1244+
"url": "https://prism-central.example.com:9440",
1245+
"insecure": true
1246+
}
1247+
},
1248+
"addons": {
1249+
"konnectorAgent": {
1250+
"credentials": { "secretRef": {"name":"test-secret"} }
1251+
}
1252+
}
1253+
}`)},
1254+
}},
1255+
},
1256+
},
1257+
}
1258+
1259+
registered, err := isClusterRegisteredInPC(context.Background(), client, cluster, logr.Discard())
1260+
1261+
assert.Error(t, err)
1262+
assert.False(t, registered)
1263+
assert.Contains(t, err.Error(), "credentials secret does not contain 'username' key")
1264+
}
1265+
1266+
func TestIsClusterRegisteredInPC_MissingPasswordInSecret(t *testing.T) {
1267+
client := fake.NewClientBuilder().WithScheme(testScheme).Build()
1268+
secret := &corev1.Secret{
1269+
ObjectMeta: metav1.ObjectMeta{
1270+
Name: "test-secret",
1271+
Namespace: "default",
1272+
},
1273+
Data: map[string][]byte{
1274+
"username": []byte("testuser"),
1275+
},
1276+
}
1277+
require.NoError(t, client.Create(context.Background(), secret))
1278+
1279+
cluster := &clusterv1.Cluster{
1280+
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
1281+
Spec: clusterv1.ClusterSpec{
1282+
Topology: &clusterv1.Topology{
1283+
Variables: []clusterv1.ClusterVariable{{
1284+
Name: v1alpha1.ClusterConfigVariableName,
1285+
Value: apiextensionsv1.JSON{Raw: []byte(`{
1286+
"nutanix": {
1287+
"prismCentralEndpoint": {
1288+
"url": "https://prism-central.example.com:9440",
1289+
"insecure": true
1290+
}
1291+
},
1292+
"addons": {
1293+
"konnectorAgent": {
1294+
"credentials": { "secretRef": {"name":"test-secret"} }
1295+
}
1296+
}
1297+
}`)},
1298+
}},
1299+
},
1300+
},
1301+
}
1302+
1303+
registered, err := isClusterRegisteredInPC(context.Background(), client, cluster, logr.Discard())
1304+
1305+
assert.Error(t, err)
1306+
assert.False(t, registered)
1307+
assert.Contains(t, err.Error(), "credentials secret does not contain 'password' key")
1308+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2023 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package utils provides utility functions for lifecycle handlers.
5+
package utils
6+
7+
import (
8+
"context"
9+
"encoding/base64"
10+
"fmt"
11+
12+
prismgoclient "github.com/nutanix-cloud-native/prism-go-client"
13+
konnectorprismgoclient "github.com/nutanix-cloud-native/prism-go-client/karbon"
14+
)
15+
16+
// NewPrismCentralKonnectorClient creates a new Prism Konnector client that is used to call the Konnector APIs.
17+
func NewPrismCentralKonnectorClient(credentials *prismgoclient.Credentials, additionalTrustBundle string,
18+
clientOpts ...konnectorprismgoclient.ClientOption,
19+
) (*PrismCentralKonnectorClient, error) {
20+
if credentials == nil {
21+
return nil, fmt.Errorf(
22+
" Prism Central credentials cannot be nil, needed to create Prism Central Konnector client",
23+
)
24+
}
25+
26+
if additionalTrustBundle != "" {
27+
certBytes, err := base64.StdEncoding.DecodeString(additionalTrustBundle)
28+
if err != nil {
29+
return nil, fmt.Errorf("failed to decode base64 certificate: %w", err)
30+
}
31+
clientOpts = append(clientOpts, konnectorprismgoclient.WithPEMEncodedCertBundle(certBytes))
32+
// Set insecure to false if trust bundle is provided.
33+
credentials.Insecure = false
34+
}
35+
36+
prismCentralKonnectorClient, err := konnectorprismgoclient.NewKarbonAPIClient(*credentials, clientOpts...)
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to create prism konnector client: %w", err)
39+
}
40+
41+
return &PrismCentralKonnectorClient{prismCentralKonnectorClient: prismCentralKonnectorClient}, nil
42+
}
43+
44+
// PrismCentralKonnectorClient wraps the Prism Central Konnector client.
45+
type PrismCentralKonnectorClient struct {
46+
prismCentralKonnectorClient *konnectorprismgoclient.Client
47+
}
48+
49+
// GetClusterRegistration retrieves the cluster registration from Prism Central.
50+
func (pc *PrismCentralKonnectorClient) GetClusterRegistration(
51+
ctx context.Context,
52+
clusterUUID string,
53+
) (*konnectorprismgoclient.K8sClusterRegistration, error) {
54+
if pc == nil || pc.prismCentralKonnectorClient == nil {
55+
return nil, fmt.Errorf("could not connect to API server on PC: client is nil")
56+
}
57+
clusterRegistration, err := pc.prismCentralKonnectorClient.ClusterRegistrationOperations.GetK8sRegistration(
58+
ctx,
59+
clusterUUID,
60+
)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to get Kubernetes cluster(%s) registration: %w", clusterUUID, err)
63+
}
64+
return clusterRegistration, nil
65+
}

0 commit comments

Comments
 (0)