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
|
package checkexpiration
|
||||||
|
|
||||||
import (
|
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/config"
|
||||||
"opendev.org/airship/airshipctl/pkg/errors"
|
|
||||||
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CertificateExpirationStore is the customized client store
|
// CertificateExpirationStore is the customized client store
|
||||||
type CertificateExpirationStore struct {
|
type CertificateExpirationStore struct {
|
||||||
Kclient client.Interface
|
Kclient client.Interface
|
||||||
Settings config.Factory
|
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
|
// NewStore returns an instance of a CertificateExpirationStore
|
||||||
func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
||||||
kubeconfig string, _ string) (CertificateExpirationStore, error) {
|
kubeconfig, _ string, expirationThreshold int) (CertificateExpirationStore, error) {
|
||||||
airshipconfig, err := cfgFactory()
|
airshipconfig, err := cfgFactory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateExpirationStore{}, err
|
return CertificateExpirationStore{}, err
|
||||||
@ -47,14 +53,80 @@ func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CertificateExpirationStore{
|
return CertificateExpirationStore{
|
||||||
Kclient: kclient,
|
Kclient: kclient,
|
||||||
Settings: cfgFactory,
|
Settings: cfgFactory,
|
||||||
|
ExpirationThreshold: expirationThreshold,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExpiringCertificates checks for the expiration data
|
// GetExpiringTLSCertificates returns the list of TLS certificates whose expiration date
|
||||||
// NOT IMPLEMENTED (guhan)
|
// falls within the given expirationThreshold
|
||||||
// TODO (guhan) check for TLS certificates, workload kubeconfig and node certificates
|
func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecret, error) {
|
||||||
func (store CertificateExpirationStore) GetExpiringCertificates(expirationThreshold int) (Expiration, error) {
|
secrets, err := store.getAllTLSCertificates()
|
||||||
return Expiration{}, errors.ErrNotImplemented{What: "check certificate expiration logic"}
|
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
|
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
|
// RunE is the implementation of check command
|
||||||
func (c *CheckCommand) RunE(w io.Writer) error {
|
func (c *CheckCommand) RunE(w io.Writer) error {
|
||||||
if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") {
|
if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") {
|
||||||
return ErrInvalidFormat{RequestedFormat: c.Options.FormatType}
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationInfo, err := secretStore.GetExpiringCertificates(c.Options.Threshold)
|
expirationInfo, err := secretStore.GetExpiringTLSCertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Options.FormatType == "yaml" {
|
if c.Options.FormatType == "yaml" {
|
||||||
err = yaml.WriteOut(w, expirationInfo)
|
err = yaml.WriteOut(w, expirationInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -16,11 +16,15 @@ package checkexpiration_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"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/cluster/checkexpiration"
|
||||||
"opendev.org/airship/airshipctl/pkg/config"
|
"opendev.org/airship/airshipctl/pkg/config"
|
||||||
@ -30,7 +34,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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) {
|
func TestRunE(t *testing.T) {
|
||||||
@ -59,11 +84,12 @@ func TestRunE(t *testing.T) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
},
|
},
|
||||||
checkFlags: checkexpiration.CheckFlags{
|
checkFlags: checkexpiration.CheckFlags{
|
||||||
Threshold: 5000,
|
Threshold: testThreshold,
|
||||||
FormatType: "json",
|
FormatType: "json",
|
||||||
Kubeconfig: "",
|
Kubeconfig: "",
|
||||||
},
|
},
|
||||||
testErr: testNotImplementedErr,
|
testErr: "",
|
||||||
|
expectedOutput: expectedJSONOutput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testCaseName: "valid-input-format-yaml",
|
testCaseName: "valid-input-format-yaml",
|
||||||
@ -72,17 +98,17 @@ func TestRunE(t *testing.T) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
},
|
},
|
||||||
checkFlags: checkexpiration.CheckFlags{
|
checkFlags: checkexpiration.CheckFlags{
|
||||||
Threshold: 5000,
|
Threshold: testThreshold,
|
||||||
FormatType: "yaml",
|
FormatType: "yaml",
|
||||||
},
|
},
|
||||||
testErr: testNotImplementedErr,
|
testErr: "",
|
||||||
|
expectedOutput: expectedYAMLOutput,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.testCaseName, func(t *testing.T) {
|
t.Run(tt.testCaseName, func(t *testing.T) {
|
||||||
var objects []runtime.Object
|
objects := []runtime.Object{getTLSSecret(t)}
|
||||||
// TODO (guhan) append a dummy object for testing
|
|
||||||
ra := fake.WithTypedObjects(objects...)
|
ra := fake.WithTypedObjects(objects...)
|
||||||
|
|
||||||
command := checkexpiration.CheckCommand{
|
command := checkexpiration.CheckCommand{
|
||||||
@ -99,8 +125,38 @@ func TestRunE(t *testing.T) {
|
|||||||
if tt.testErr != "" {
|
if tt.testErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.testErr)
|
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 {
|
func (e ErrInvalidFormat) Error() string {
|
||||||
return fmt.Sprintf("invalid output format specified %s. Allowed values are json|yaml", e.RequestedFormat)
|
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