Remove cluster check-expiration subcommand

This command is outdated and no longer used, according to the design call
it should be removed.

Change-Id: I5953f0c66e9cee40c070afe148aa98c9d07113f7
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Relates-To: #588
This commit is contained in:
Ruslan Aliev 2021-06-24 10:53:07 -05:00
parent df55f50cb6
commit 7a89572149
15 changed files with 5 additions and 1104 deletions

View File

@ -1,86 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration
import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/cluster/checkexpiration"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log"
)
const (
checkLong = `
Displays a list of certificate along with expirations from both the management and workload clusters, or in a
self-managed cluster. Checks for TLS Secrets, kubeconf secrets (which gets created while creating the
workload cluster) and also the node certificates present inside /etc/kubernetes/pki directory for each node.
`
checkExample = `
To display all the expiring certificates in the cluster
# airshipctl cluster check-certificate-expiration --kubeconfig testconfig
To display the certificates whose expiration is within threshold of 30 days
# airshipctl cluster check-certificate-expiration -t 30 --kubeconfig testconfig
To output the contents in json format (default operation)
# airshipctl cluster check-certificate-expiration -o json --kubeconfig testconfig
or
# airshipctl cluster check-certificate-expiration --kubeconfig testconfig
To output the contents in yaml format
# airshipctl cluster check-certificate-expiration -o yaml --kubeconfig testconfig
To output the contents whose expiration is within 30 days in yaml format
# airshipctl cluster check-certificate-expiration -t 30 -o yaml --kubeconfig testconfig
`
kubeconfigFlag = "kubeconfig"
)
// NewCheckCommand creates a new command for generating secret information
func NewCheckCommand(cfgFactory config.Factory) *cobra.Command {
c := &checkexpiration.CheckCommand{
Options: checkexpiration.CheckFlags{},
CfgFactory: cfgFactory,
ClientFactory: client.DefaultClient,
}
checkCmd := &cobra.Command{
Use: "check-certificate-expiration",
Short: "Airshipctl command to check expiring TLS certificates, " +
"secrets and kubeconfigs in the kubernetes cluster",
Long: checkLong[1:],
Example: checkExample,
RunE: func(cmd *cobra.Command, args []string) error {
return c.RunE(cmd.OutOrStdout())
},
}
checkCmd.Flags().StringVarP(&c.Options.FormatType, "output", "o", "json", "convert output to yaml or json")
checkCmd.Flags().StringVar(&c.Options.KubeContext, "kubecontext", "", "kubeconfig context to be used")
checkCmd.Flags().StringVar(&c.Options.Kubeconfig, kubeconfigFlag, "",
"path to kubeconfig associated with cluster being managed")
checkCmd.Flags().IntVarP(&c.Options.Threshold, "threshold", "t", -1,
"the max expiration threshold in days before a certificate is expiring. Displays all the certificates by default")
err := checkCmd.MarkFlagRequired(kubeconfigFlag)
if err != nil {
log.Fatalf("marking kubeconfig flag required failed: %v", err)
}
return checkCmd
}

View File

@ -1,36 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration_test
import (
"testing"
"opendev.org/airship/airshipctl/cmd/cluster/checkexpiration"
"opendev.org/airship/airshipctl/testutil"
)
func TestCheckExpiration(t *testing.T) {
cmdTests := []*testutil.CmdTest{
{
Name: "check-expiration-with-help",
CmdLine: "--help",
Cmd: checkexpiration.NewCheckCommand(nil),
},
}
for _, tt := range cmdTests {
testutil.RunTest(t, tt)
}
}

View File

@ -1,33 +0,0 @@
Displays a list of certificate along with expirations from both the management and workload clusters, or in a
self-managed cluster. Checks for TLS Secrets, kubeconf secrets (which gets created while creating the
workload cluster) and also the node certificates present inside /etc/kubernetes/pki directory for each node.
Usage:
check-certificate-expiration [flags]
Examples:
To display all the expiring certificates in the cluster
# airshipctl cluster check-certificate-expiration --kubeconfig testconfig
To display the certificates whose expiration is within threshold of 30 days
# airshipctl cluster check-certificate-expiration -t 30 --kubeconfig testconfig
To output the contents in json format (default operation)
# airshipctl cluster check-certificate-expiration -o json --kubeconfig testconfig
or
# airshipctl cluster check-certificate-expiration --kubeconfig testconfig
To output the contents in yaml format
# airshipctl cluster check-certificate-expiration -o yaml --kubeconfig testconfig
To output the contents whose expiration is within 30 days in yaml format
# airshipctl cluster check-certificate-expiration -t 30 -o yaml --kubeconfig testconfig
Flags:
-h, --help help for check-certificate-expiration
--kubeconfig string path to kubeconfig associated with cluster being managed
--kubecontext string kubeconfig context to be used
-o, --output string convert output to yaml or json (default "json")
-t, --threshold int the max expiration threshold in days before a certificate is expiring. Displays all the certificates by default (default -1)

View File

@ -17,7 +17,6 @@ package cluster
import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/cmd/cluster/checkexpiration"
"opendev.org/airship/airshipctl/cmd/cluster/resetsatoken"
"opendev.org/airship/airshipctl/pkg/config"
)
@ -40,7 +39,6 @@ func NewClusterCommand(cfgFactory config.Factory) *cobra.Command {
clusterRootCmd.AddCommand(NewStatusCommand(cfgFactory))
clusterRootCmd.AddCommand(resetsatoken.NewResetCommand(cfgFactory))
clusterRootCmd.AddCommand(checkexpiration.NewCheckCommand(cfgFactory))
clusterRootCmd.AddCommand(NewGetKubeconfigCommand(cfgFactory))
clusterRootCmd.AddCommand(NewListCommand(cfgFactory))

View File

@ -5,12 +5,11 @@ Usage:
cluster [command]
Available Commands:
check-certificate-expiration Airshipctl command to check expiring TLS certificates, secrets and kubeconfigs in the kubernetes cluster
get-kubeconfig Airshipctl command to retrieve kubeconfig for a desired cluster
help Help about any command
list Airshipctl command to get and list defined clusters
rotate-sa-token Airshipctl command to rotate tokens of Service Account(s)
status Retrieve statuses of deployed cluster components
get-kubeconfig Airshipctl command to retrieve kubeconfig for a desired cluster
help Help about any command
list Airshipctl command to get and list defined clusters
rotate-sa-token Airshipctl command to rotate tokens of Service Account(s)
status Retrieve statuses of deployed cluster components
Flags:
-h, --help help for cluster

View File

@ -32,7 +32,6 @@ SEE ALSO
~~~~~~~~
* :ref:`airshipctl <airshipctl>` - A unified command line tool for management of end-to-end kubernetes cluster deployment on cloud infrastructure environments.
* :ref:`airshipctl cluster check-certificate-expiration <airshipctl_cluster_check-certificate-expiration>` - Airshipctl command to check expiring TLS certificates, secrets and kubeconfigs in the kubernetes cluster
* :ref:`airshipctl cluster get-kubeconfig <airshipctl_cluster_get-kubeconfig>` - Airshipctl command to retrieve kubeconfig for a desired cluster
* :ref:`airshipctl cluster list <airshipctl_cluster_list>` - Airshipctl command to get and list defined clusters
* :ref:`airshipctl cluster rotate-sa-token <airshipctl_cluster_rotate-sa-token>` - Airshipctl command to rotate tokens of Service Account(s)

View File

@ -6,7 +6,6 @@ cluster
:maxdepth: 2
airshipctl_cluster
airshipctl_cluster_check-certificate-expiration
airshipctl_cluster_get-kubeconfig
airshipctl_cluster_list
airshipctl_cluster_rotate-sa-token

View File

@ -1,350 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration
import (
"crypto/x509"
"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"
timeFormat = "Jan 02, 2006 15:04 MST"
nodeCertExpirationAnnotation = "cert-expiration"
)
// CertificateExpirationStore is the customized client store
type CertificateExpirationStore struct {
Kclient client.Interface
Settings config.Factory
ExpirationThreshold int
}
// NewStore returns an instance of a CertificateExpirationStore
func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
kubeconfig, _ string, expirationThreshold int) (CertificateExpirationStore, error) {
airshipconfig, err := cfgFactory()
if err != nil {
return CertificateExpirationStore{}, err
}
// TODO (guhan) Allow kube context to be passed to client Factory
// 4th argument in NewStore takes kube context and is ignored for now.
// To be modified post #388. Refer to
// https://review.opendev.org/#/c/760501/7/pkg/cluster/checkexpiration/command.go@31
kclient, err := clientFactory(airshipconfig.LoadedConfigPath(), kubeconfig)
if err != nil {
return CertificateExpirationStore{}, err
}
return CertificateExpirationStore{
Kclient: kclient,
Settings: cfgFactory,
ExpirationThreshold: expirationThreshold,
}, nil
}
// GetExpiringTLSCertificates returns the list of TLS certificates whose expiration date
// falls within the given expirationThreshold
func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecret, error) {
secrets, err := store.getAllTLSCertificates()
if err != nil {
return nil, err
}
tlsData := make([]TLSSecret, 0)
for _, secret := range secrets.Items {
expiringCertificates := store.getExpiringCertificates(secret)
if len(expiringCertificates) > 0 {
tlsData = append(tlsData, TLSSecret{
Name: secret.Name,
Namespace: secret.Namespace,
ExpiringCertificates: expiringCertificates,
})
}
}
return tlsData, nil
}
// getAllTLSCertificates juist returns all the k8s secrets with tyoe as TLS
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)
}
// getExpiringCertificates skims through all the TLS certificates and returns the ones
// lesser than threshold
func (store CertificateExpirationStore) getExpiringCertificates(secret corev1.Secret) map[string]string {
expiringCertificates := map[string]string{}
for _, certName := range []string{corev1.TLSCertKey, corev1.ServiceAccountRootCAKey} {
if cert, found := secret.Data[certName]; found {
expirationDate, err := extractExpirationDateFromCertificate(cert)
if err != nil {
log.Printf("Unable to parse certificate for %s in secret %s in namespace %s: %v",
certName, secret.Name, secret.Namespace, err)
continue
}
if isWithinDuration(expirationDate, store.ExpirationThreshold) {
expiringCertificates[certName] = expirationDate.String()
}
}
}
return expiringCertificates
}
// isWithinDuration checks if the certificate expirationDate is within the duration (input)
func isWithinDuration(expirationDate time.Time, duration int) bool {
if duration < 0 {
return true
}
daysUntilExpiration := int(time.Until(expirationDate).Hours() / 24)
return 0 <= daysUntilExpiration && daysUntilExpiration < duration
}
// extractExpirationDateFromCertificate parses the certificate and returns the expiration date
func extractExpirationDateFromCertificate(certData []byte) (time.Time, error) {
block, _ := pem.Decode(certData)
if block == nil {
return time.Time{}, ErrPEMFail{Context: "decode", Err: "no PEM data could be found"}
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return time.Time{}, ErrPEMFail{Context: "parse", Err: err.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
}
// GetExpiringNodeCertificates runs through all the nodes and identifies expiration
func (store CertificateExpirationStore) GetExpiringNodeCertificates() ([]NodeCert, error) {
// Node will be updated with an annotation with the expiry content (Activity
// of HostConfig Operator - 'check-expiry' CR Object) every day (Cron like
// activity is performed by reconcile tag in the Operator) Below code is
// implemented to just read the annotation, parse it, identify expirable
// content and report back
// Expected Annotation Format:
// "cert-expiration": "{ admin.conf: Aug 06, 2021 12:36 UTC },
// { apiserver: Aug 06, 2021 12:36 UTC },
// { apiserver-etcd-client: Aug 06, 2021 12:36 UTC },
// { apiserver-kubelet-client: Aug 06, 2021 12:36 UTC },
// { controller-manager.conf: Aug 06, 2021 12:36 UTC },
// { etcd-healthcheck-client: Aug 06, 2021 12:36 UTC },
// { etcd-peer: Aug 06, 2021 12:36 UTC },
// { etcd-server: Aug 06, 2021 12:36 UTC },
// { front-proxy-client: Aug 06, 2021 12:36 UTC },
// { scheduler.conf: Aug 06, 2021 12:36 UTC },
// { ca: Aug 04, 2030 12:36 UTC },
// { etcd-ca: Aug 04, 2030 12:36 UTC },
// { front-proxy-ca: Aug 04, 2030 12:36 UTC }"
nodes, err := store.getNodes(metav1.ListOptions{})
if err != nil {
return nil, err
}
nodeData := make([]NodeCert, 0)
for _, node := range nodes.Items {
expiringNodeCertificates := store.getExpiringNodeCertificates(node)
if len(expiringNodeCertificates) > 0 {
nodeData = append(nodeData, NodeCert{
Name: node.Name,
Namespace: node.Namespace,
ExpiringCertificates: expiringNodeCertificates,
})
}
}
return nodeData, nil
}
// getSecrets returns the Nodes list based on the listOptions
func (store CertificateExpirationStore) getNodes(listOptions metav1.ListOptions) (*corev1.NodeList, error) {
return store.Kclient.ClientSet().CoreV1().Nodes().List(listOptions)
}
// getExpiringNodeCertificates skims through all the node certificates and returns
// the ones lesser than threshold
func (store CertificateExpirationStore) getExpiringNodeCertificates(node corev1.Node) map[string]string {
if cert, found := node.ObjectMeta.Annotations[nodeCertExpirationAnnotation]; found {
certificateList := splitAsList(cert)
expiringCertificates := map[string]string{}
for _, certificate := range certificateList {
certificateName, expirationDate := identifyCertificateNameAndExpirationDate(certificate)
if certificateName != "" && isWithinDuration(expirationDate, store.ExpirationThreshold) {
expiringCertificates[certificateName] = expirationDate.String()
}
}
return expiringCertificates
}
log.Printf("%s annotation missing for node %s in %s", nodeCertExpirationAnnotation,
node.Name, node.Namespace)
return nil
}
// splitAsList performes the required string manipulations and returns list of items
func splitAsList(value string) []string {
return strings.Split(strings.ReplaceAll(value, "{", ""), "},")
}
// identifyCertificateNameAndExpirationDate performs string manipulations and returns
// certificate name and its expiration date
func identifyCertificateNameAndExpirationDate(certificate string) (string, time.Time) {
certificateName := strings.TrimSpace(strings.Split(certificate, ":")[0])
expirationDate := strings.TrimSpace(strings.Split(certificate, ":")[1]) +
":" +
strings.TrimSpace(strings.ReplaceAll(strings.Split(certificate, ":")[2], "}", ""))
formattedExpirationDate, err := time.Parse(timeFormat, expirationDate)
if err != nil {
log.Printf(err.Error())
return "", time.Time{}
}
return certificateName, formattedExpirationDate
}

View File

@ -1,128 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration_test
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
"opendev.org/airship/airshipctl/pkg/cluster/checkexpiration"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/testutil"
)
type testCase struct {
name string
expiryThreshold int
nodeTestFile string
kubeconfTestFile string
tlsSecretTestFile string
nodeExpirationYear string
expectedExpiringNodeCount int
expectedExpiringKubeConfigCount int
expectedExpiringTLSSecretCount int
}
var (
testCases = []*testCase{
{
name: "empty-expect-error",
expectedExpiringNodeCount: 0,
expectedExpiringKubeConfigCount: 0,
expectedExpiringTLSSecretCount: 0,
},
{
name: "node-cert-expiring",
nodeTestFile: nodeFile,
nodeExpirationYear: "2021",
expiryThreshold: testThreshold, // 20 years
expectedExpiringNodeCount: 1,
},
{
name: "node-cert-not-expiring",
nodeExpirationYear: "2025",
nodeTestFile: nodeFile,
expiryThreshold: 10,
expectedExpiringNodeCount: 0,
},
{
name: "all-certs-not-expiring",
nodeExpirationYear: "2025",
nodeTestFile: nodeFile,
tlsSecretTestFile: tlsSecretFile,
kubeconfTestFile: kubeconfFile,
expiryThreshold: 1,
expectedExpiringNodeCount: 0,
expectedExpiringKubeConfigCount: 0,
expectedExpiringTLSSecretCount: 0,
},
{
name: "all-certs-expiring",
nodeExpirationYear: "2021",
nodeTestFile: nodeFile,
tlsSecretTestFile: tlsSecretFile,
kubeconfTestFile: kubeconfFile,
expiryThreshold: testThreshold,
expectedExpiringNodeCount: 1,
expectedExpiringKubeConfigCount: 1,
expectedExpiringTLSSecretCount: 1,
},
}
)
func TestCheckExpiration(t *testing.T) {
for _, testCase := range testCases {
cfg, _ := testutil.InitConfig(t)
settings := func() (*config.Config, error) {
return cfg, nil
}
var objects []runtime.Object
if testCase.nodeExpirationYear != "" && testCase.nodeTestFile != "" {
objects = append(objects, getNodeObject(t, testCase.nodeTestFile, testCase.nodeExpirationYear))
}
if testCase.tlsSecretTestFile != "" {
objects = append(objects, getSecretObject(t, testCase.tlsSecretTestFile))
}
if testCase.kubeconfTestFile != "" {
objects = append(objects, getSecretObject(t, testCase.kubeconfTestFile))
}
ra := fake.WithTypedObjects(objects...)
clientFactory := func(_ string, _ string) (client.Interface, error) {
return fake.NewClient(ra), nil
}
store, err := checkexpiration.NewStore(settings, clientFactory, "", "", testCase.expiryThreshold)
assert.NoError(t, err)
expirationInfo := store.GetExpiringCertificates()
assert.Len(t, expirationInfo.Kubeconfs, testCase.expectedExpiringKubeConfigCount)
assert.Len(t, expirationInfo.TLSSecrets, testCase.expectedExpiringTLSSecretCount)
assert.Len(t, expirationInfo.NodeCerts, testCase.expectedExpiringNodeCount)
}
}

View File

@ -1,133 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration
import (
"encoding/json"
"io"
"strings"
"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"
)
// CheckFlags flags given for checking the expiration
type CheckFlags struct {
Threshold int
FormatType string
Kubeconfig string
KubeContext string
}
// CheckCommand check expiration command
type CheckCommand struct {
Options CheckFlags
CfgFactory config.Factory
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"`
NodeCerts []NodeCert `json:"nodeCerts,omitempty" yaml:"nodeCerts,omitempty"`
}
// TLSSecret captures expiration information of certificates embedded in TLS secrets
type TLSSecret struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
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"`
}
// NodeCert captures certificate expiry information for certificates on each node
type NodeCert struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
ExpiringCertificates map[string]string `json:"certificate,omitempty" yaml:"certificate,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") {
return ErrInvalidFormat{RequestedFormat: c.Options.FormatType}
}
secretStore, err := NewStore(c.CfgFactory, c.ClientFactory, c.Options.Kubeconfig,
c.Options.KubeContext, c.Options.Threshold)
if err != nil {
return err
}
expirationInfo := secretStore.GetExpiringCertificates()
if c.Options.FormatType == "yaml" {
err = yaml.WriteOut(w, expirationInfo)
if err != nil {
return err
}
} else {
buffer, err := json.MarshalIndent(expirationInfo, "", " ")
if err != nil {
return err
}
_, err = w.Write(buffer)
if err != nil {
return err
}
}
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())
}
expiringNodeCertificates, err := store.GetExpiringNodeCertificates()
if err != nil {
log.Printf(err.Error())
}
return ExpirationStore{
TLSSecrets: expiringTLSCertificates,
Kubeconfs: expiringKubeConfCertificates,
NodeCerts: expiringNodeCertificates,
}
}

View File

@ -1,257 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration_test
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/kubectl/pkg/scheme"
"opendev.org/airship/airshipctl/pkg/cluster/checkexpiration"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/testutil"
)
const (
testThreshold = 7200
nodeFile = "testdata/node.yaml"
kubeconfFile = "testdata/kubeconfig.yaml"
tlsSecretFile = "testdata/tls-secret.yaml" //nolint:gosec
expectedJSONOutput = ` {
"tlsSecrets": [
{
"name": "test-cluster-etcd",
"namespace": "target-infra",
"certificate": {
"ca.crt": "2030-08-31 10:12:49 +0000 UTC",
"tls.crt": "2030-08-31 10:12:49 +0000 UTC"
}
}
],
"kubeconfs": [
{
"secretName": "test-cluster-kubeconfig",
"secretNamespace": "target-infra",
"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"
}
]
}
],
"nodeCerts": [
{
"name": "test-node",
"certificate": {
"admin.conf": "2021-08-06 12:36:00 +0000 UTC",
"apiserver": "2021-08-06 12:36:00 +0000 UTC",
"apiserver-etcd-client": "2021-08-06 12:36:00 +0000 UTC",
"apiserver-kubelet-client": "2021-08-06 12:36:00 +0000 UTC",
"ca": "2021-08-04 12:36:00 +0000 UTC",
"controller-manager.conf": "2021-08-06 12:36:00 +0000 UTC",
"etcd-ca": "2021-08-04 12:36:00 +0000 UTC",
"etcd-healthcheck-client": "2021-08-06 12:36:00 +0000 UTC",
"etcd-peer": "2021-08-06 12:36:00 +0000 UTC",
"etcd-server": "2021-08-06 12:36:00 +0000 UTC",
"front-proxy-ca": "2021-08-04 12:36:00 +0000 UTC",
"front-proxy-client": "2021-08-06 12:36:00 +0000 UTC",
"scheduler.conf": "2021-08-06 12:36:00 +0000 UTC"
}
}
]
}`
expectedYAMLOutput = `
---
kubeconfs:
- cluster:
- certificateName: CertificateAuthorityData
expirationDate: 2030-08-31 10:12:48 +0000 UTC
name: workload-cluster
secretName: test-cluster-kubeconfig
secretNamespace: target-infra
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
name: test-cluster-etcd
namespace: target-infra
nodeCerts:
- name: test-node
certificate:
admin.conf: 2021-08-06 12:36:00 +0000 UTC
apiserver: 2021-08-06 12:36:00 +0000 UTC
apiserver-etcd-client: 2021-08-06 12:36:00 +0000 UTC
apiserver-kubelet-client: 2021-08-06 12:36:00 +0000 UTC
ca: 2021-08-04 12:36:00 +0000 UTC
controller-manager.conf: 2021-08-06 12:36:00 +0000 UTC
etcd-ca: 2021-08-04 12:36:00 +0000 UTC
etcd-healthcheck-client: 2021-08-06 12:36:00 +0000 UTC
etcd-peer: 2021-08-06 12:36:00 +0000 UTC
etcd-server: 2021-08-06 12:36:00 +0000 UTC
front-proxy-ca: 2021-08-04 12:36:00 +0000 UTC
front-proxy-client: 2021-08-06 12:36:00 +0000 UTC
scheduler.conf: 2021-08-06 12:36:00 +0000 UTC
...
`
)
func TestRunE(t *testing.T) {
tests := []struct {
testCaseName string
testErr string
checkFlags checkexpiration.CheckFlags
cfgFactory config.Factory
expectedOutput string
}{
{
testCaseName: "invalid-input-format",
cfgFactory: func() (*config.Config, error) {
return nil, nil
},
checkFlags: checkexpiration.CheckFlags{
Threshold: 0,
FormatType: "test-yaml",
},
testErr: checkexpiration.ErrInvalidFormat{RequestedFormat: "test-yaml"}.Error(),
},
{
testCaseName: "valid-input-format-json",
cfgFactory: func() (*config.Config, error) {
cfg, _ := testutil.InitConfig(t)
return cfg, nil
},
checkFlags: checkexpiration.CheckFlags{
Threshold: testThreshold,
FormatType: "json",
Kubeconfig: "",
},
testErr: "",
expectedOutput: expectedJSONOutput,
},
{
testCaseName: "valid-input-format-yaml",
cfgFactory: func() (*config.Config, error) {
cfg, _ := testutil.InitConfig(t)
return cfg, nil
},
checkFlags: checkexpiration.CheckFlags{
Threshold: testThreshold,
FormatType: "yaml",
},
testErr: "",
expectedOutput: expectedYAMLOutput,
},
}
for _, tt := range tests {
t.Run(tt.testCaseName, func(t *testing.T) {
objects := []runtime.Object{
getSecretObject(t, tlsSecretFile),
getSecretObject(t, kubeconfFile),
getNodeObject(t, nodeFile, "2021"),
}
ra := fake.WithTypedObjects(objects...)
command := checkexpiration.CheckCommand{
Options: tt.checkFlags,
CfgFactory: tt.cfgFactory,
ClientFactory: func(_ string, _ string) (client.Interface, error) {
return fake.NewClient(ra), nil
},
}
var buffer bytes.Buffer
err := command.RunE(&buffer)
if tt.testErr != "" {
require.Error(t, err)
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())
case "yaml":
assert.YAMLEq(t, tt.expectedOutput, buffer.String())
}
}
})
}
}
func getSecretObject(t *testing.T, fileName string) *v1.Secret {
t.Helper()
object := readObjectFromFile(t, fileName)
secret, ok := object.(*v1.Secret)
require.True(t, ok)
return secret
}
func getNodeObject(t *testing.T, fileName string, expirationYear string) *v1.Node {
t.Helper()
object := readObjectFromFile(t, fileName)
node, ok := object.(*v1.Node)
require.True(t, ok)
node.Annotations["cert-expiration"] = strings.ReplaceAll(node.Annotations["cert-expiration"],
"{{year}}", expirationYear)
return node
}
func readObjectFromFile(t *testing.T, fileName string) runtime.Object {
t.Helper()
contents, err := ioutil.ReadFile(fileName)
require.NoError(t, err)
jsonContents, err := yaml.ToJSON(contents)
require.NoError(t, err)
object, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), jsonContents)
require.NoError(t, err)
return object
}

View File

@ -1,36 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package checkexpiration
import "fmt"
// ErrInvalidFormat is called when the user provides format other than yaml/json
type ErrInvalidFormat struct {
RequestedFormat string
}
func (e ErrInvalidFormat) Error() string {
return fmt.Sprintf("invalid output format specified %s. Allowed values are json|yaml", e.RequestedFormat)
}
// ErrPEMFail is called where there is a PEM related failure while parsing the certificate block
type ErrPEMFail struct {
Context string
Err string
}
func (e ErrPEMFail) Error() string {
return fmt.Sprintf("failed to %s certificate PEM: %s", e.Context, e.Err)
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
apiVersion: v1
kind: Node
metadata:
annotations:
cert-expiration: "{ admin.conf: Aug 06, {{year}} 12:36 UTC },{ apiserver: Aug 06, {{year}} 12:36 UTC },{ apiserver-etcd-client: Aug 06, {{year}} 12:36 UTC },{ apiserver-kubelet-client: Aug 06, {{year}} 12:36 UTC },{ controller-manager.conf: Aug 06, {{year}} 12:36 UTC },{ etcd-healthcheck-client: Aug 06, {{year}} 12:36 UTC },{ etcd-peer: Aug 06, {{year}} 12:36 UTC },{ etcd-server: Aug 06, {{year}} 12:36 UTC },{ front-proxy-client: Aug 06, {{year}} 12:36 UTC },{ scheduler.conf: Aug 06, {{year}} 12:36 UTC },{ ca: Aug 04, {{year}} 12:36 UTC },{ etcd-ca: Aug 04, {{year}} 12:36 UTC },{ front-proxy-ca: Aug 04, {{year}} 12:36 UTC }"
name: test-node

View File

@ -1,12 +0,0 @@
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5ekNDQWJPZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01Ea3dNakV3TURjME9Wb1hEVE13TURnek1URXdNVEkwT1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTHdVCkErWWRNcWF0clI4WWVHcEVYTFlIeVZHRi9Na0g1L3FqWEdpcmxITCtiNEhXYmg2enkvSWY3N1MzNWhnTzFPSkcKcG5wdmk5ck5ISEMyV3hNblVUOFVxSE1lYjNyb0phMlFpME1sNGlNTFFncTd0TGhLQ04zTnFwYmk5OEJ4d1VxSAo3eGkzWmU3WEZ2NVJyRlpFa2hicW9ycVJRZzg0cHdRTTNvMkh1NmJSWElETjc5bnVMV3piZ0pYUzhwMytnZHFuCkxIa3owOGN4VkxxZmJPOFMxOFEwdWJUbnY3RjVBblBPZkhJY2xyR2h3MFVUZXRWZGxuVmNISnRpZ29xYjBBdysKOUY5WkNaN0ZZUzE2eEJ0L0N0OHpKZEQ3MEFGZ2NMRm4vSTJlSG05bTFSK0FISWxJU3ZLbzl0OVBtekJxWS9tbgpHOUFoektYSlBTSHRPbEJqMXBzQ0F3RUFBYU1tTUNRd0RnWURWUjBQQVFIL0JBUURBZ0trTUJJR0ExVWRFd0VCCi93UUlNQVlCQWY4Q0FRQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQVAzLzdmYzhBNlFjWU5CUnQxdDRWQ0QKRnYwMDB0Q09mVjVxdnNOU2RQYTRZY2NaTDBmT2dUVmtNbGczbTVRMmVKUkpVTHR5V0NHQ3BNcHRFR2duMGI3eQpIWkhtWkpkZjZBN0twbFRqQWxyemRDOUpPTVBrQUtjaXhNcWhPcDFxdTI0dHl5eEZQZWdMeTE2SU5ZMGl5ZzI2ClJhYkowREdQcTNZUitwZHphRkZ2YUN6bk1yTGtDckJpV0xvUmdrK2xaT3NoUU1EaHl4Y1crQW5mcHRINHlxM2YKZjhhdFZPVGprMGVIYVlQUlNKSlFlM0RLOXFEY0V4bVFib1orM1NLYXRtK0RNMWVieE5CNlVKaGdYWEluZ3dQQwp4czV0azJkUGlJaXZldXZYbGVqTkZ1c0RzekV1NU9ZN08xVzZNSUdmNlBPQ3JmSzk0S3JhVWVhWng4bUlRcE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5ekNDQWJPZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01Ea3dNakV3TURjME9Wb1hEVE13TURnek1URXdNVEkwT1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTHdVCkErWWRNcWF0clI4WWVHcEVYTFlIeVZHRi9Na0g1L3FqWEdpcmxITCtiNEhXYmg2enkvSWY3N1MzNWhnTzFPSkcKcG5wdmk5ck5ISEMyV3hNblVUOFVxSE1lYjNyb0phMlFpME1sNGlNTFFncTd0TGhLQ04zTnFwYmk5OEJ4d1VxSAo3eGkzWmU3WEZ2NVJyRlpFa2hicW9ycVJRZzg0cHdRTTNvMkh1NmJSWElETjc5bnVMV3piZ0pYUzhwMytnZHFuCkxIa3owOGN4VkxxZmJPOFMxOFEwdWJUbnY3RjVBblBPZkhJY2xyR2h3MFVUZXRWZGxuVmNISnRpZ29xYjBBdysKOUY5WkNaN0ZZUzE2eEJ0L0N0OHpKZEQ3MEFGZ2NMRm4vSTJlSG05bTFSK0FISWxJU3ZLbzl0OVBtekJxWS9tbgpHOUFoektYSlBTSHRPbEJqMXBzQ0F3RUFBYU1tTUNRd0RnWURWUjBQQVFIL0JBUURBZ0trTUJJR0ExVWRFd0VCCi93UUlNQVlCQWY4Q0FRQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQVAzLzdmYzhBNlFjWU5CUnQxdDRWQ0QKRnYwMDB0Q09mVjVxdnNOU2RQYTRZY2NaTDBmT2dUVmtNbGczbTVRMmVKUkpVTHR5V0NHQ3BNcHRFR2duMGI3eQpIWkhtWkpkZjZBN0twbFRqQWxyemRDOUpPTVBrQUtjaXhNcWhPcDFxdTI0dHl5eEZQZWdMeTE2SU5ZMGl5ZzI2ClJhYkowREdQcTNZUitwZHphRkZ2YUN6bk1yTGtDckJpV0xvUmdrK2xaT3NoUU1EaHl4Y1crQW5mcHRINHlxM2YKZjhhdFZPVGprMGVIYVlQUlNKSlFlM0RLOXFEY0V4bVFib1orM1NLYXRtK0RNMWVieE5CNlVKaGdYWEluZ3dQQwp4czV0azJkUGlJaXZldXZYbGVqTkZ1c0RzekV1NU9ZN08xVzZNSUdmNlBPQ3JmSzk0S3JhVWVhWng4bUlRcE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBdkJRRDVoMHlwcTJ0SHhoNGFrUmN0Z2ZKVVlYOHlRZm4rcU5jYUt1VWN2NXZnZFp1CkhyUEw4aC92dExmbUdBN1U0a2FtZW0rTDJzMGNjTFpiRXlkUlB4U29jeDV2ZXVnbHJaQ0xReVhpSXd0Q0NydTAKdUVvSTNjMnFsdUwzd0hIQlNvZnZHTGRsN3RjVy9sR3NWa1NTRnVxaXVwRkNEemluQkF6ZWpZZTdwdEZjZ00zdgoyZTR0Yk51QWxkTHluZjZCMnFjc2VUUFR4ekZVdXA5czd4TFh4RFM1dE9lL3NYa0NjODU4Y2h5V3NhSERSUk42CjFWMldkVndjbTJLQ2lwdlFERDcwWDFrSm5zVmhMWHJFRzM4SzN6TWwwUHZRQVdCd3NXZjhqWjRlYjJiVkg0QWMKaVVoSzhxajIzMCtiTUdwaithY2IwQ0hNcGNrOUllMDZVR1BXbXdJREFRQUJBb0lCQUJESkZJUUFEUm8xRytOUAprc2VoTEVrT3J0ZjR4bFBHd2R4cm9mNnhlWUU5MWdQWGVHS0RGMnVYa0JRbjZZQXlLcXU3TkhadTZDTng5TnpXCldaQi9ETkE5YnI4L2N5R2NBR2phSXFPdWlOMHB6dzRZTEl2YUI2cU1CWEtMOVNLV3hISjdhVXBpYTlXQ0dzbzkKemN5eE4veVZta3BlVm0vM1ZXaVdJWEt1TDRBMnZmaHJjbHdVc3NwNFA4Q1dZUFZVbExOT0JsYlQrNy9qQ1RubwpyRDVYcjNDVEFiUmNBNEdvcGx1ZFphaUtqY2FpZXpYeHdTaGFmWkFURWJJM0xwejZSTnRBMDRLbjEyR0JLRDQwCk1zV21xUU1zRE4zUkgveFdKbnp5TFVSTGVlcDdRUTdXQytZYmRmdFpTZ3FKUkRNYmw1aHdwUXRLeFBoUmhHakcKSGEwWUVIa0NnWUVBNkdjSCtzUzczd3FwUmloZXJ3aFVEY21Na3VEZDBiMkEyWlhpcjBRUStKUXZrVHVSd2NZVgpHdDU3Zm40UnBybFUwNzByMTZRQzcrdTU0OVdQVlFpbExvMURvVUVHMC9QVEhSV2ZQVjBtOWh2cGF2akdtMG1zCmpmd20rMXZ1WGFiS0RMblg0dXcxQ0JKSU9vdmJ6RnhrTGJWZk91YVhrN1BhckhiOGdHRHR2ZjhDZ1lFQXp5elcKZ3VtT25tMmVNYy80ZHJmMDI2Q1doZk1ZMXF0VnFEb2s4anVlMVlEaVdhZEMwOGo3VEVoQ1dTQUZPOC9ZOVBDRApKQ3cvcGpUZThERlRLZEw3d2t5ZVUrS2JJRFRLNHJ5clMzenZmV3Q4Zjg2ZmU5SVd3L3FDaEN2cm5tSTdNeS8rCkowL1BCZFFGL0xkeS9PWlVGcW9wblJNS1VnVTJXMWhxRTJOcUgyVUNnWUExV0NqeHU2U3YvcDk2Tmh2OXF6aTMKN1hKeDZHR2lHaEJ3WVVJbUhzYVNlRmt1eWZDYi9ONnRTekluaDhKL2RYenVHVGJ1Q1h5UEc1bVFuVjJJRkRMdQpLNGpCZzg2UWFpQWtSZWxHU1pKKzNVdEh2WkRBNWpsUVlmZUVyTVpiQXNUUUJQeHozdW9SVHpqN0QwMUZiRk9tClZrSmtuN2RkTk9SVnYvNFhiYWhFZXdLQmdENURTM1NzbktBZ2NacW0xaFZYMDg3dHhFOGRjQ21UOUhwS2Z6QU4KbXY2dmJWZGtYVUVvOWQxSEdpbU81Z1BEdzRCWmlCQW0vRG9IU2JrR0dlaEg4RUhFcFJDdzJjNGtENVYwL2tZQgpsamdyUlk5am1hcXN5UXE5RHR5S0ZwWFREOWVpWk0rTHZMd1RySGoyNlNmNFVPMCsxcUxPUmh2QVZVVytuS0tYCkRoM0JBb0dBRjVEU0R2U2ltVjhtaHFWbjc2M1dwOUJXN0RwbTAxTE5qMHRZbVFhZVpyK3VoZ1BKdW5SYzhBUE4KWXRudjlsY04wSkx1MzhWaWI3eVFzVmgvVUsvYVdwS2w1YUZzTXdRMVNJTUJXUTVEeVR0VEE0VGZXQzFUSUYxUQpuempLd0NSTHBaZ1F6ZkNCaTBIT3doZ0pUaDZBNng4SG1hYXU5ZmZLdE90SUpkTUZ5d1E9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
kind: Secret
metadata:
labels:
cluster.x-k8s.io/cluster-name: test-cluster
name: test-cluster-etcd
namespace: target-infra
type: Opaque