Merge "check kubeconf certificate expiration"
This commit is contained in:
commit
c5342bd833
pkg/cluster/checkexpiration
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
17
pkg/cluster/checkexpiration/testdata/kubeconfig.yaml
vendored
Normal file
17
pkg/cluster/checkexpiration/testdata/kubeconfig.yaml
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user