Merge "check kubeconf certificate expiration"
This commit is contained in:
commit
c5342bd833
@ -19,15 +19,22 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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/config"
|
||||||
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kubeconfigIdentifierSuffix = "-kubeconfig"
|
||||||
|
)
|
||||||
|
|
||||||
// CertificateExpirationStore is the customized client store
|
// CertificateExpirationStore is the customized client store
|
||||||
type CertificateExpirationStore struct {
|
type CertificateExpirationStore struct {
|
||||||
Kclient client.Interface
|
Kclient client.Interface
|
||||||
@ -85,6 +92,11 @@ func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecre
|
|||||||
func (store CertificateExpirationStore) getAllTLSCertificates() (*corev1.SecretList, error) {
|
func (store CertificateExpirationStore) getAllTLSCertificates() (*corev1.SecretList, error) {
|
||||||
secretTypeFieldSelector := fmt.Sprintf("type=%s", corev1.SecretTypeTLS)
|
secretTypeFieldSelector := fmt.Sprintf("type=%s", corev1.SecretTypeTLS)
|
||||||
listOptions := metav1.ListOptions{FieldSelector: secretTypeFieldSelector}
|
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)
|
return store.Kclient.ClientSet().CoreV1().Secrets("").List(listOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,3 +142,119 @@ func extractExpirationDateFromCertificate(certData []byte) (time.Time, error) {
|
|||||||
}
|
}
|
||||||
return cert.NotAfter, nil
|
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/config"
|
||||||
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
||||||
|
"opendev.org/airship/airshipctl/pkg/log"
|
||||||
"opendev.org/airship/airshipctl/pkg/util/yaml"
|
"opendev.org/airship/airshipctl/pkg/util/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,6 +40,12 @@ type CheckCommand struct {
|
|||||||
ClientFactory client.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"`
|
||||||
|
}
|
||||||
|
|
||||||
// TLSSecret captures expiration information of certificates embedded in TLS secrets
|
// TLSSecret captures expiration information of certificates embedded in TLS secrets
|
||||||
type TLSSecret struct {
|
type TLSSecret struct {
|
||||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
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"`
|
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
|
// 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") {
|
||||||
@ -58,10 +80,7 @@ func (c *CheckCommand) RunE(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationInfo, err := secretStore.GetExpiringTLSCertificates()
|
expirationInfo := secretStore.GetExpiringCertificates()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Options.FormatType == "yaml" {
|
if c.Options.FormatType == "yaml" {
|
||||||
err = yaml.WriteOut(w, expirationInfo)
|
err = yaml.WriteOut(w, expirationInfo)
|
||||||
@ -80,3 +99,21 @@ func (c *CheckCommand) RunE(w io.Writer) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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 (
|
const (
|
||||||
testThreshold = 5000
|
testThreshold = 5000
|
||||||
|
|
||||||
expectedJSONOutput = `[
|
expectedJSONOutput = ` {
|
||||||
|
"tlsSecrets": [
|
||||||
{
|
{
|
||||||
"name": "test-cluster-etcd",
|
"name": "test-cluster-etcd",
|
||||||
"namespace": "default",
|
"namespace": "default",
|
||||||
@ -45,10 +46,43 @@ const (
|
|||||||
"tls.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": "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 = `
|
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:
|
- certificate:
|
||||||
ca.crt: 2030-08-31 10:12:49 +0000 UTC
|
ca.crt: 2030-08-31 10:12:49 +0000 UTC
|
||||||
tls.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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.testCaseName, func(t *testing.T) {
|
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...)
|
ra := fake.WithTypedObjects(objects...)
|
||||||
|
|
||||||
command := checkexpiration.CheckCommand{
|
command := checkexpiration.CheckCommand{
|
||||||
@ -127,6 +164,7 @@ func TestRunE(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), tt.testErr)
|
assert.Contains(t, err.Error(), tt.testErr)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
t.Log(buffer.String())
|
||||||
switch tt.checkFlags.FormatType {
|
switch tt.checkFlags.FormatType {
|
||||||
case "json":
|
case "json":
|
||||||
assert.JSONEq(t, tt.expectedOutput, buffer.String())
|
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()
|
t.Helper()
|
||||||
object := readObjectFromFile(t, "testdata/tls-secret.yaml")
|
|
||||||
|
object := readObjectFromFile(t, fileName)
|
||||||
secret, ok := object.(*v1.Secret)
|
secret, ok := object.(*v1.Secret)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
return secret
|
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