Check TLS certificate expiration
Reference:- https://hackmd.io/aGaz7YXSSHybGcyol8vYEw Relates-To: #391 Change-Id: Ia1d4524f5228542cf3fc4b29074c668eca3c55bb
This commit is contained in:
parent
334ff8041b
commit
ecfb38c7bf
@ -15,23 +15,29 @@
|
||||
package checkexpiration
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/config"
|
||||
"opendev.org/airship/airshipctl/pkg/errors"
|
||||
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
||||
)
|
||||
|
||||
// CertificateExpirationStore is the customized client store
|
||||
type CertificateExpirationStore struct {
|
||||
Kclient client.Interface
|
||||
Settings config.Factory
|
||||
Kclient client.Interface
|
||||
Settings config.Factory
|
||||
ExpirationThreshold int
|
||||
}
|
||||
|
||||
// Expiration captures expiration information of all expirable entities in the cluster
|
||||
type Expiration struct{}
|
||||
|
||||
// NewStore returns an instance of a CertificateExpirationStore
|
||||
func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
||||
kubeconfig string, _ string) (CertificateExpirationStore, error) {
|
||||
kubeconfig, _ string, expirationThreshold int) (CertificateExpirationStore, error) {
|
||||
airshipconfig, err := cfgFactory()
|
||||
if err != nil {
|
||||
return CertificateExpirationStore{}, err
|
||||
@ -47,14 +53,80 @@ func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
||||
}
|
||||
|
||||
return CertificateExpirationStore{
|
||||
Kclient: kclient,
|
||||
Settings: cfgFactory,
|
||||
Kclient: kclient,
|
||||
Settings: cfgFactory,
|
||||
ExpirationThreshold: expirationThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetExpiringCertificates checks for the expiration data
|
||||
// NOT IMPLEMENTED (guhan)
|
||||
// TODO (guhan) check for TLS certificates, workload kubeconfig and node certificates
|
||||
func (store CertificateExpirationStore) GetExpiringCertificates(expirationThreshold int) (Expiration, error) {
|
||||
return Expiration{}, errors.ErrNotImplemented{What: "check certificate expiration logic"}
|
||||
// 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.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
|
||||
}
|
||||
|
@ -39,21 +39,30 @@ type CheckCommand struct {
|
||||
ClientFactory client.Factory
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
secretStore, err := NewStore(c.CfgFactory, c.ClientFactory, c.Options.Kubeconfig,
|
||||
c.Options.KubeContext, c.Options.Threshold)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expirationInfo, err := secretStore.GetExpiringCertificates(c.Options.Threshold)
|
||||
expirationInfo, err := secretStore.GetExpiringTLSCertificates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Options.FormatType == "yaml" {
|
||||
err = yaml.WriteOut(w, expirationInfo)
|
||||
if err != nil {
|
||||
|
@ -16,11 +16,15 @@ package checkexpiration_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"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"
|
||||
@ -30,7 +34,28 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testNotImplementedErr = "not implemented: check certificate expiration logic"
|
||||
testThreshold = 5000
|
||||
|
||||
expectedJSONOutput = `[
|
||||
{
|
||||
"name": "test-cluster-etcd",
|
||||
"namespace": "default",
|
||||
"certificate": {
|
||||
"ca.crt": "2030-08-31 10:12:49 +0000 UTC",
|
||||
"tls.crt": "2030-08-31 10:12:49 +0000 UTC"
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
expectedYAMLOutput = `
|
||||
---
|
||||
- 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: default
|
||||
...
|
||||
`
|
||||
)
|
||||
|
||||
func TestRunE(t *testing.T) {
|
||||
@ -59,11 +84,12 @@ func TestRunE(t *testing.T) {
|
||||
return cfg, nil
|
||||
},
|
||||
checkFlags: checkexpiration.CheckFlags{
|
||||
Threshold: 5000,
|
||||
Threshold: testThreshold,
|
||||
FormatType: "json",
|
||||
Kubeconfig: "",
|
||||
},
|
||||
testErr: testNotImplementedErr,
|
||||
testErr: "",
|
||||
expectedOutput: expectedJSONOutput,
|
||||
},
|
||||
{
|
||||
testCaseName: "valid-input-format-yaml",
|
||||
@ -72,17 +98,17 @@ func TestRunE(t *testing.T) {
|
||||
return cfg, nil
|
||||
},
|
||||
checkFlags: checkexpiration.CheckFlags{
|
||||
Threshold: 5000,
|
||||
Threshold: testThreshold,
|
||||
FormatType: "yaml",
|
||||
},
|
||||
testErr: testNotImplementedErr,
|
||||
testErr: "",
|
||||
expectedOutput: expectedYAMLOutput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.testCaseName, func(t *testing.T) {
|
||||
var objects []runtime.Object
|
||||
// TODO (guhan) append a dummy object for testing
|
||||
objects := []runtime.Object{getTLSSecret(t)}
|
||||
ra := fake.WithTypedObjects(objects...)
|
||||
|
||||
command := checkexpiration.CheckCommand{
|
||||
@ -99,8 +125,38 @@ func TestRunE(t *testing.T) {
|
||||
if tt.testErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.testErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
switch tt.checkFlags.FormatType {
|
||||
case "json":
|
||||
assert.JSONEq(t, tt.expectedOutput, buffer.String())
|
||||
case "yaml":
|
||||
assert.YAMLEq(t, tt.expectedOutput, buffer.String())
|
||||
}
|
||||
}
|
||||
// TODO (guhan) add else part to check the actual vs expected o/p
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getTLSSecret(t *testing.T) *v1.Secret {
|
||||
t.Helper()
|
||||
object := readObjectFromFile(t, "testdata/tls-secret.yaml")
|
||||
secret, ok := object.(*v1.Secret)
|
||||
require.True(t, ok)
|
||||
return secret
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -24,3 +24,13 @@ type ErrInvalidFormat struct {
|
||||
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)
|
||||
}
|
||||
|
12
pkg/cluster/checkexpiration/testdata/tls-secret.yaml
vendored
Normal file
12
pkg/cluster/checkexpiration/testdata/tls-secret.yaml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
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: default
|
||||
type: Opaque
|
Loading…
x
Reference in New Issue
Block a user