diff --git a/pkg/cluster/checkexpiration/checkexpiration.go b/pkg/cluster/checkexpiration/checkexpiration.go index d348a74af..aa65e3ed9 100644 --- a/pkg/cluster/checkexpiration/checkexpiration.go +++ b/pkg/cluster/checkexpiration/checkexpiration.go @@ -19,15 +19,22 @@ import ( "encoding/pem" "fmt" "log" + "strings" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/k8s/client" ) +const ( + kubeconfigIdentifierSuffix = "-kubeconfig" +) + // CertificateExpirationStore is the customized client store type CertificateExpirationStore struct { Kclient client.Interface @@ -85,6 +92,11 @@ func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecre func (store CertificateExpirationStore) getAllTLSCertificates() (*corev1.SecretList, error) { secretTypeFieldSelector := fmt.Sprintf("type=%s", corev1.SecretTypeTLS) listOptions := metav1.ListOptions{FieldSelector: secretTypeFieldSelector} + return store.getSecrets(listOptions) +} + +// getSecrets returns the secret list based on the listOptions +func (store CertificateExpirationStore) getSecrets(listOptions metav1.ListOptions) (*corev1.SecretList, error) { return store.Kclient.ClientSet().CoreV1().Secrets("").List(listOptions) } @@ -130,3 +142,119 @@ func extractExpirationDateFromCertificate(certData []byte) (time.Time, error) { } return cert.NotAfter, nil } + +// GetExpiringKubeConfigs - fetches all the '-kubeconfig' secrets and identifies expiration +func (store CertificateExpirationStore) GetExpiringKubeConfigs() ([]Kubeconfig, error) { + kubeconfigs, err := store.getKubeconfSecrets() + + if err != nil { + return nil, err + } + + kSecretData := make([]Kubeconfig, 0) + + for _, kubeconfig := range kubeconfigs { + kubecontent, err := clientcmd.Load(kubeconfig.Data["value"]) + if err != nil { + log.Printf("Failed to read kubeconfig from %s in %s-"+ + "it maybe malformed : %v", kubeconfig.Name, kubeconfig.Namespace, err.Error()) + continue + } + + expiringClusters := store.getExpiringClusterCertificates(kubecontent) + + expiringUsers := store.getExpiringUserCertificates(kubecontent) + + if len(expiringClusters) > 0 || len(expiringUsers) > 0 { + kSecretData = append(kSecretData, Kubeconfig{ + SecretName: kubeconfig.Name, + SecretNamespace: kubeconfig.Namespace, + Cluster: expiringClusters, + User: expiringUsers, + }) + } + } + return kSecretData, nil +} + +// filterKubeConfigs identifies the kubeconfig secrets based on the kubeconfigIdentifierSuffix +func filterKubeConfigs(secrets []corev1.Secret) []corev1.Secret { + filteredSecrets := []corev1.Secret{} + for _, secret := range secrets { + if strings.HasSuffix(secret.Name, kubeconfigIdentifierSuffix) { + filteredSecrets = append(filteredSecrets, secret) + } + } + return filteredSecrets +} + +// filterOwners allows only the secrets with Ownerreferences matching ownerKind +func filterOwners(secrets []corev1.Secret, ownerKind string) []corev1.Secret { + filteredSecrets := []corev1.Secret{} + for _, secret := range secrets { + for _, ownerRef := range secret.OwnerReferences { + if ownerRef.Kind == ownerKind { + filteredSecrets = append(filteredSecrets, secret) + } + } + } + return filteredSecrets +} + +func (store CertificateExpirationStore) getExpiringClusterCertificates( + kubeconfig *clientcmdapi.Config) []kubeconfData { + expiringClusterCertificates := make([]kubeconfData, 0) + + // Iterate through each Cluster and identify expiration + for clusterName, clusterData := range kubeconfig.Clusters { + expirationDate, err := extractExpirationDateFromCertificate(clusterData.CertificateAuthorityData) + if err != nil { + log.Printf("Unable to parse certificate for %s : %v", clusterName, err) + continue + } + + if isWithinDuration(expirationDate, store.ExpirationThreshold) { + expiringClusterCertificates = append(expiringClusterCertificates, kubeconfData{ + Name: clusterName, + CertificateName: "CertificateAuthorityData", + ExpirationDate: expirationDate.String(), + }) + } + } + return expiringClusterCertificates +} + +func (store CertificateExpirationStore) getExpiringUserCertificates( + kubeconfig *clientcmdapi.Config) []kubeconfData { + expiringUserCertificates := make([]kubeconfData, 0) + + // Iterate through each User and identify expiration + for userName, userData := range kubeconfig.AuthInfos { + expirationDate, err := extractExpirationDateFromCertificate(userData.ClientCertificateData) + if err != nil { + log.Printf("Unable to parse certificate for %s : %v", userName, err) + continue + } + + if isWithinDuration(expirationDate, store.ExpirationThreshold) { + expiringUserCertificates = append(expiringUserCertificates, kubeconfData{ + Name: userName, + CertificateName: "ClientCertificateData", + ExpirationDate: expirationDate.String(), + }) + } + } + return expiringUserCertificates +} + +// getKubeconfSecrets filters the kubeconf secrets +func (store CertificateExpirationStore) getKubeconfSecrets() ([]corev1.Secret, error) { + secrets, err := store.getSecrets(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + kubeconfigs := filterKubeConfigs(secrets.Items) + kubeconfigs = filterOwners(kubeconfigs, "KubeadmControlPlane") + return kubeconfigs, nil +} diff --git a/pkg/cluster/checkexpiration/command.go b/pkg/cluster/checkexpiration/command.go index 54b1ea37c..524c6e258 100644 --- a/pkg/cluster/checkexpiration/command.go +++ b/pkg/cluster/checkexpiration/command.go @@ -21,6 +21,7 @@ import ( "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/k8s/client" + "opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/util/yaml" ) @@ -39,6 +40,12 @@ type CheckCommand struct { ClientFactory client.Factory } +// ExpirationStore captures expiration information of all expirable entities in the cluster +type ExpirationStore struct { + TLSSecrets []TLSSecret `json:"tlsSecrets,omitempty" yaml:"tlsSecrets,omitempty"` + Kubeconfs []Kubeconfig `json:"kubeconfs,omitempty" yaml:"kubeconfs,omitempty"` +} + // TLSSecret captures expiration information of certificates embedded in TLS secrets type TLSSecret struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` @@ -46,6 +53,21 @@ type TLSSecret struct { ExpiringCertificates map[string]string `json:"certificate,omitempty" yaml:"certificate,omitempty"` } +// Kubeconfig captures expiration information of all kubeconfigs +type Kubeconfig struct { + SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"` + SecretNamespace string `json:"secretNamespace,omitempty" yaml:"secretNamespace,omitempty"` + Cluster []kubeconfData `json:"cluster,omitempty" yaml:"cluster,omitempty"` + User []kubeconfData `json:"user,omitempty" yaml:"user,omitempty"` +} + +// kubeconfData captures cluster ca certificate expiration information and kubeconfig's user's certificate +type kubeconfData struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + CertificateName string `json:"certificateName,omitempty" yaml:"certificateName,omitempty"` + ExpirationDate string `json:"expirationDate,omitempty" yaml:"expirationDate,omitempty"` +} + // RunE is the implementation of check command func (c *CheckCommand) RunE(w io.Writer) error { if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") { @@ -58,10 +80,7 @@ func (c *CheckCommand) RunE(w io.Writer) error { return err } - expirationInfo, err := secretStore.GetExpiringTLSCertificates() - if err != nil { - return err - } + expirationInfo := secretStore.GetExpiringCertificates() if c.Options.FormatType == "yaml" { err = yaml.WriteOut(w, expirationInfo) @@ -80,3 +99,21 @@ func (c *CheckCommand) RunE(w io.Writer) error { } return nil } + +// GetExpiringCertificates encapsulates all the different expirable entities in the cluster +func (store CertificateExpirationStore) GetExpiringCertificates() ExpirationStore { + expiringTLSCertificates, err := store.GetExpiringTLSCertificates() + if err != nil { + log.Printf(err.Error()) + } + + expiringKubeConfCertificates, err := store.GetExpiringKubeConfigs() + if err != nil { + log.Printf(err.Error()) + } + + return ExpirationStore{ + TLSSecrets: expiringTLSCertificates, + Kubeconfs: expiringKubeConfCertificates, + } +} diff --git a/pkg/cluster/checkexpiration/command_test.go b/pkg/cluster/checkexpiration/command_test.go index 03dc0e3af..67aae85f6 100644 --- a/pkg/cluster/checkexpiration/command_test.go +++ b/pkg/cluster/checkexpiration/command_test.go @@ -36,7 +36,8 @@ import ( const ( testThreshold = 5000 - expectedJSONOutput = `[ + expectedJSONOutput = ` { + "tlsSecrets": [ { "name": "test-cluster-etcd", "namespace": "default", @@ -45,10 +46,43 @@ const ( "tls.crt": "2030-08-31 10:12:49 +0000 UTC" } } - ]` + ], + "kubeconfs": [ + { + "secretName": "test-cluster-kubeconfig", + "secretNamespace": "default", + "cluster": [ + { + "name": "workload-cluster", + "certificateName": "CertificateAuthorityData", + "expirationDate": "2030-08-31 10:12:48 +0000 UTC" + } + ], + "user": [ + { + "name": "workload-cluster-admin", + "certificateName": "ClientCertificateData", + "expirationDate": "2021-09-02 10:12:50 +0000 UTC" + } + ] + } + ] + }` expectedYAMLOutput = ` --- +kubeconfs: +- cluster: + - certificateName: CertificateAuthorityData + expirationDate: 2030-08-31 10:12:48 +0000 UTC + name: workload-cluster + secretName: test-cluster-kubeconfig + secretNamespace: default + user: + - certificateName: ClientCertificateData + expirationDate: 2021-09-02 10:12:50 +0000 UTC + name: workload-cluster-admin +tlsSecrets: - certificate: ca.crt: 2030-08-31 10:12:49 +0000 UTC tls.crt: 2030-08-31 10:12:49 +0000 UTC @@ -108,7 +142,10 @@ func TestRunE(t *testing.T) { for _, tt := range tests { t.Run(tt.testCaseName, func(t *testing.T) { - objects := []runtime.Object{getTLSSecret(t)} + objects := []runtime.Object{ + getObject(t, "testdata/tls-secret.yaml"), + getObject(t, "testdata/kubeconfig.yaml"), + } ra := fake.WithTypedObjects(objects...) command := checkexpiration.CheckCommand{ @@ -127,6 +164,7 @@ func TestRunE(t *testing.T) { assert.Contains(t, err.Error(), tt.testErr) } else { require.NoError(t, err) + t.Log(buffer.String()) switch tt.checkFlags.FormatType { case "json": assert.JSONEq(t, tt.expectedOutput, buffer.String()) @@ -138,11 +176,13 @@ func TestRunE(t *testing.T) { } } -func getTLSSecret(t *testing.T) *v1.Secret { +func getObject(t *testing.T, fileName string) *v1.Secret { t.Helper() - object := readObjectFromFile(t, "testdata/tls-secret.yaml") + + object := readObjectFromFile(t, fileName) secret, ok := object.(*v1.Secret) require.True(t, ok) + return secret } diff --git a/pkg/cluster/checkexpiration/testdata/kubeconfig.yaml b/pkg/cluster/checkexpiration/testdata/kubeconfig.yaml new file mode 100644 index 000000000..2b800ef58 --- /dev/null +++ b/pkg/cluster/checkexpiration/testdata/kubeconfig.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +data: + value: YXBpVmVyc2lvbjogdjEKY2x1c3RlcnM6Ci0gY2x1c3RlcjoKICAgIGNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhOiBMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONWVrTkRRV0pQWjBGM1NVSkJaMGxDUVVSQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFRldUVkpOZDBWUldVUldVVkZFUlhkd2NtUlhTbXdLWTIwMWJHUkhWbnBOUWpSWVJGUkpkMDFFYTNkTmFrVjNUVVJqTUU5R2IxaEVWRTEzVFVSbmVrMVVSWGROVkVrd1QwWnZkMFpVUlZSTlFrVkhRVEZWUlFwQmVFMUxZVE5XYVZwWVNuVmFXRkpzWTNwRFEwRlRTWGRFVVZsS1MyOWFTV2gyWTA1QlVVVkNRbEZCUkdkblJWQkJSRU5EUVZGdlEyZG5SVUpCVEVsWkNrb3lUbXRCWnpNeGVqaDRaRGQyUzBWWlIwWnFMMHc0Tm1SaFdEWTJkWGRWVEVRMVozRlJURlJDYVZkSFZqVk9NSEU0UjJwMGNFeGpObTVWVjIxU01Va0tUR0ZNVkdoWVNWTTRWRGhtZWxnNVVqaE5aV1pGZUhOMFVtSXZXbkZGZUVkVmRtVkxkaTlQVjBzNVlXOHlLM0F2T1ZBdmJXUmpVa1p2VG1aalF6Sm5OUXBrYW1GSGJsRlJZMXBPUTBOelRISTRMMWRuVkRSRlNGaERZMlU1UWpGc2NGWjVNU3RXVjNkRFZtWmhaRkpEUkdwWVlWVXZjMkZQT0ZGYU1sQXlWa0phQ2pobE1FSnpUazlaZEVWVWNHVnhZVk15V1ZkdlZuWlFNV3N2Y0dKMVVsTk9XRTFQUkRnNFZYTnNWMUl5UVdOVVZXbG1TWGR2ZUZweU1ISk5NMU55U0ZjS1lVOTFWbm9yWW1kTGQxZHdRVk4xWjJGV1ZrcHJVMkphTTJJd2Eyd3lOV1JxZEVGYU5rdHVhMHhWV21sMVIyeDNMMk5EUjBRdmRHOHdhbEZFTUZaSk1nb3ZhRGxvUlZGS2JFcEZRMmhzVjFkQ0swUk5RMEYzUlVGQllVMXRUVU5SZDBSbldVUldVakJRUVZGSUwwSkJVVVJCWjB0clRVSkpSMEV4VldSRmQwVkNDaTkzVVVsTlFWbENRV1k0UTBGUlFYZEVVVmxLUzI5YVNXaDJZMDVCVVVWTVFsRkJSR2RuUlVKQlNVZzBhSE55ZWpscE1UUTFkR2RQUW5CWldGRTNZakFLU0d0RU9ITkRZVmhTU0ZWU00zaDRRVkJpUVdScFJtSnpPRkJxUkN0VFNVcElTa2x1TW0xS05VcGxibGs1Y0ZwWmRrUlRXbTUyUldkT1UydDJkbWhKS3dwT2JtaGpUbU5sTHk5VlJXdHZOekIxWm5WRk1IaGhMMlJETWtKcVZFSktRakEyY0RKNFVYZ3hWREp2T1RKTVJIVTRhbkJqV1VwbFlWRkZjbTV0YVRWMUNrTTJTaXR1UkhwalpDdENZVk0wTm1ZeWREZG5RekZJTm1WYWJUVjRMMVJOWVVKU1VHNUNVWGxYU25wRU1YSjBjMEZyYTJkRFdVOTJlV2huUjJKSmNYY0tZa2N3U0RaME1WWnJZUzlRY0VGa2JuWXlWa2wwTkdGWmVtRkJTbTFPYXpWcU0xZFdWVU5OVURKaWRFYzFkMkYzWWtaRU1HazJVVUZtZUVVclNWcHVNZ3AwWXk5MFltTjJZelZPZVUxbFpUWTBZM2RhYlZkTlRIVkpWbVpHUlRGRGEyWk1TSEJuYlVSbmVWRXhhVzVtTDNsbmJGbHdUWFpEVWs5MGRIUjJPR3M5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLCiAgICBzZXJ2ZXI6IGh0dHBzOi8vMTcyLjE3LjAuMzo2NDQzCiAgbmFtZTogd29ya2xvYWQtY2x1c3Rlcgpjb250ZXh0czoKLSBjb250ZXh0OgogICAgY2x1c3Rlcjogd29ya2xvYWQtY2x1c3RlcgogICAgdXNlcjogd29ya2xvYWQtY2x1c3Rlci1hZG1pbgogIG5hbWU6IHdvcmtsb2FkLWNsdXN0ZXItYWRtaW5Ad29ya2xvYWQtY2x1c3RlcgpjdXJyZW50LWNvbnRleHQ6IHdvcmtsb2FkLWNsdXN0ZXItYWRtaW5Ad29ya2xvYWQtY2x1c3RlcgpraW5kOiBDb25maWcKcHJlZmVyZW5jZXM6IHt9CnVzZXJzOgotIG5hbWU6IHdvcmtsb2FkLWNsdXN0ZXItYWRtaW4KICB1c2VyOgogICAgY2xpZW50LWNlcnRpZmljYXRlLWRhdGE6IExTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU00YWtORFFXUnhaMEYzU1VKQlowbEpSRUpNVXpneVVVZHhWM2QzUkZGWlNrdHZXa2xvZG1OT1FWRkZURUpSUVhkR1ZFVlVUVUpGUjBFeFZVVUtRWGhOUzJFelZtbGFXRXAxV2xoU2JHTjZRV1ZHZHpCNVRVUkJOVTFFU1hoTlJFRXpUa1JvWVVaM01IbE5WRUUxVFVSSmVFMUVSWGxPVkVKaFRVUlJlQXBHZWtGV1FtZE9Wa0pCYjFSRWJrNDFZek5TYkdKVWNIUlpXRTR3V2xoS2VrMVNhM2RHZDFsRVZsRlJSRVY0UW5Ka1YwcHNZMjAxYkdSSFZucE1WMFpyQ21KWGJIVk5TVWxDU1dwQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSUlVaQlFVOURRVkU0UVUxSlNVSkRaMHREUVZGRlFYZEpURGxKZFROVWRHMXZZMHh5YlcwS1ZERlBkMk0yVFcxeFVVYzNlbFUzT0doVVFVaEpla3BOV0c5Tk1HRktSVkl2YTFoTWJraFBTazB2TkhCNEwwNVhWbWxHUjNWcFkybHJXSFUzZGxFd1VncHJOMnBZTWsxQlUwSXpaazB2S3pNNWVHVk1iakJOWkVsVllsaERTRnBSV1RaVmVrbFJhMFpoVlc4dldrOTNibk15VFhBeGRVdFNVVzlJUkhZNVZFazNDa0ZzVG10U1RYUk9SSHBOTXpGVFFXTXJVRWRGU0VaMGJ5OXRRVVpOUXpsUVdtaHRaMlpJY2twdWJsUkJWR3RZTW1ONlQxcFZaakl6ZUVSdk1XTkZjbklLZWk5aVRYTjBRMGhRWVdjMVRVcE9lWGczUmpCNFlVTlBOSFF2U2pZM2FXWkhORk5tTlVaMVkxVkxTemgxVGs1R1VGbFVSamhIWTI5R1duaGFRelZOTlFwdlEwUkdSVGRVUm5obWRXWTNaMWhpU0RSemVsQTBkMFZhTms5S1NIQTRjVUpHYlU4eVRrVndkR3N3T1VSbVpUSndUVFZxYzJSSWNXcE9jV2t2VVc5QkNrRTVURGhEVVVsRVFWRkJRbTk1WTNkS1ZFRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwSmhRWGRGZDFsRVZsSXdiRUpCZDNkRFoxbEpTM2RaUWtKUlZVZ0tRWGRKZDBSUldVcExiMXBKYUhaalRrRlJSVXhDVVVGRVoyZEZRa0ZDUXpZMlZHVkhaV3RtTlU1VU1sbHJSR0pEWjBsemR6TmpXbTV5UVRCYVdDdDZaQW95WXpRM2VGaFlibkJpUW1VeVJYQjNNSGwwUkZkNFpGWnVZa1JzVkVoaVUydERPRFl2ZEZSalpHNVFhMHB4YUM5MWEwSnpTbGxHV0hjclZVNDNlbXRDQ2pock5HY3pWbmRvWldWeU1raDZTVWxUZVV4MWJVZHJlRFJMVlhOblRGbzRjblZCWVU1VkswVlRjSFpyZWpOclpuY3dMMmhOUzB4MFNGTjRRMEpoWVhjS01YaHNSbk5yVG5ZNVlVRXhNMWxXVXpZMWRYWlBXVzUyUzJOa1YxVkRXamx5VW1SdVREWlNiMUJUVm1OQlFYQnZlU3RPY2xOaU5DOXBXVFU1V2tKSFJncEpkRmQwYm1sdlZURTVhVmxzT1ZoRGVtaDZaakF6VDB4NlVqaGpOVFJFT0VFemFXZzNOR3AwU0hKUksweEpjWFJ0UnpScGNYTk9kbWhCWXpoREwzSmxDa1JQYm5VNVIxSnRSMmR1SzFkbEszbFlkMDVGVDBGTFdVcERZV1l2Y25jM1NYRk1PSEpZZDJOWVltcHBia1ZhUlVZclZUMEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0KICAgIGNsaWVudC1rZXktZGF0YTogTFMwdExTMUNSVWRKVGlCU1UwRWdVRkpKVmtGVVJTQkxSVmt0TFMwdExRcE5TVWxGYjNkSlFrRkJTME5CVVVWQmQwbE1PVWwxTTFSMGJXOWpUSEp0YlZReFQzZGpOazF0Y1ZGSE4zcFZOemhvVkVGSVNYcEtUVmh2VFRCaFNrVlNDaTlyV0V4dVNFOUtUUzgwY0hndlRsZFdhVVpIZFdsamFXdFlkVGQyVVRCU2F6ZHFXREpOUVZOQ00yWk5MeXN6T1hobFRHNHdUV1JKVldKWVEwaGFVVmtLTmxWNlNWRnJSbUZWYnk5YVQzZHVjekpOY0RGMVMxSlJiMGhFZGpsVVNUZEJiRTVyVWsxMFRrUjZUVE14VTBGaksxQkhSVWhHZEc4dmJVRkdUVU01VUFwYWFHMW5aa2h5U201dVZFRlVhMWd5WTNwUFdsVm1Nak40Ukc4eFkwVnljbm92WWsxemRFTklVR0ZuTlUxS1RubDROMFl3ZUdGRFR6UjBMMG8yTjJsbUNrYzBVMlkxUm5WalZVdExPSFZPVGtaUVdWUkdPRWRqYjBaYWVGcEROVTAxYjBORVJrVTNWRVo0Wm5WbU4yZFlZa2cwYzNwUU5IZEZXalpQU2tod09IRUtRa1p0VHpKT1JYQjBhekE1UkdabE1uQk5OV3B6WkVoeGFrNXhhUzlSYjBGQk9VdzRRMUZKUkVGUlFVSkJiMGxDUVVaTGJYRnNiVnAwUVVOdlRTdDBXQXB3TTBJNVdrMDVkelUwVFN0RVJYaE5UV2g0V1dveVpuTkRNU3N3WjNkaE1UWm1lVFpMU0ROSFIwMTZjWFpVVG5OYVRTOWlkalJ5YVdwSVVHSndURWMwQ2pjMlpqRm1lVFFyTkVRMk1tbzFkamx4UzNWVFJFdGlWazlHV210aE5pOWFVMjk1TTJVeU55dHpaa2R4WTNOQ2JrNVliWEZEY25FMFUxTnNiemcwZWxvS1FWQTJlRzlQV0hOU2RYaFZkVUUzV0d3M2FrWnpSWFZuWWpCYWRWbHZURGRWUmtkNlpXTndWM1p5TWpkUFpGQktiVVZDTTNOMWRqUXpTa0l6T0dGcVdRcG9SWEI1WkVaNlEyRlVhV2xGWTFwQlpWVjJVVUl5TkVsU01IUTVPVEJaVUU5VmEyaEJXbmhCYVRad1FVTnBXRTgwUlZGVk5XdDJLMmh0U3prck1tbzFDbmhZYVRGdVMwZDNiak0zYVd4cE1URjBUMDF0T0N0NFNYUkxXazVFYmtGNFFUZGtOVlFyV1dSb1UzVndOSGhHUjFwa01rMTVlbFpHY0RaWGNGSnNXSElLVFROb01sZHdSVU5uV1VWQk1tSmlWakpJZDAxRVNVcG1kbFZRY0cxSmJteG1kRVZLTjBOd2FWY3dZbFo1YVVJNE0yTndkMHRuYm5GdWNYcEliazFaU3dwQ1pTdFdjMEUyVnpkTVEyRk5Va040UWtGaFluWmpVbnB2UWxFM1JFWnVTV2xJUlRkTVNVMVlUakZ5ZVVOcVZrRXlhbk00TlRORk1HSkhOazFuVEcxTkNteFhVRUZUY2xBd1VHbG9aSGszV0VKd1lXVkpaa2hwUW1neU4xbE9ORmgwVHpkVlJGVjNSMHRXV1Rsa2FuVkRRMnBuZWs1R1NqQkRaMWxGUVRSc01sTUtOV3QyU0hWalJrWjRabXhFYzFJdmJTOUhWbGhSVjNCSlJEWlNjRVZVYWs1bVZYUnlPRFpKVHpWWmREbEVMMXBRT1hSUE1rSlFkM2REZWxsc1Rqa3pNZ3A0VlROTFRrMUVabGt4ZGtGWmNFdGxlSEZKTldJck5IaElkVEpaVEVjdlVFbHNVM1JHVDBOTmRYVnhibFZCZEdkeFNVZHNhR052YzNSTGRFdGFSRzB5Q25KV1VEQlpNVEpvTkZaemVrMXdUbTh6V0dwb2FXeERTaTlrVUZkVVFVdElkbVYyUTNreE1FTm5XVUpHV1RscVZXNTJWRmxLT1ZkTU5WWkdWM0JPZWxrS2FIQmxOMEpsWkVSWlJtaFVhUzlrUVZkWU1WZGhUVXN5ZDBOeUt6bE1MMDVHU1dWS1JGb3hZelIzZWtOQ2NXWnJkVXBhTmpkV2NFMDRlVmNyTUdZM2R3cFFaV0V5VlZSSVpFZHpaRFpMVnpjMlNERmtVMFJQY2tGb1NuVlVhVFZCWWt4VFptdHdNbG8xVTFSU1lqaFNieTlUYURWbFZHRnZOMGRGUldoelZGaHFDbWxrV0RsYWMyaEpZazlHVFRSS1J6YzFTMmxaYjFGTFFtZEhZMEZvTUVOT2FWQk5iMVU1VW5sNVR5dElRV0pqYkdOa0wwNVBZamhOV1RsaE0xUjVTRTRLVDFCRWFXTXZOMVpUZGtsQ1pGZzVkRGxIZDNWVFRIQkNVMEYzTUhwMk5GZHRTVlZMYjJ4ME1uVTBUR1pFY1hBMU1VMU1kVEJ4UlhSSlkzQlBhMmxWWmdvdlMxZzRVekl3WlVzd1dFdzVORFpXWkRkcU5rNTJPVFl4WkROdVQxRXlSRzU2WlRkMWJGcDFRbXQ2YTAxS2NHVlVPWEE1VEZZd00ydEdTMFE0UkhCdENsWm1kblJCYjBkQ1FVcFhWbEJPY204dllqVnRiV0Z4TmxKNE1YbHRhVkpPVUZkSVdrTm1aelprWkRCQ1FtdEhjWEpNYzBwWllXWmFPR3BTU0hnM1Ywc0tRMWRpWkdZeGVrVm1OazFCTVhJNWVVVXJNa3hDZVdkdk9GcFhSVTFpUjJkalYxRkRUMFJST0hKa1FXMVJUazltVjA5TFFsaGlOV1JRVkdZclNXRklOUXBOWm5sMlYxRjZTMFpLYmtKMWFVZzVaV3cwYlRaS2MwOVVkeTlqT1UxS2EzTTVjMFZNY2s4MmVVRk9NemhIVWpobFMyVjJDaTB0TFMwdFJVNUVJRkpUUVNCUVVrbFdRVlJGSUV0RldTMHRMUzB0Q2c9PQo= +kind: Secret +metadata: + labels: + cluster.x-k8s.io/cluster-name: workload-cluster + name: test-cluster-kubeconfig + namespace: default + ownerReferences: + - apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + blockOwnerDeletion: true + controller: true + kind: KubeadmControlPlane + name: workload-cluster-control-plane + uid: 8f4112d0-83f5-44ef-9fc4-041e0eeee08e +type: Opaque \ No newline at end of file