diff --git a/pkg/certs/cert-inspection/certgraphanalysis/collector.go b/pkg/certs/cert-inspection/certgraphanalysis/collector.go index 36dfd59d0f..73bcc48255 100644 --- a/pkg/certs/cert-inspection/certgraphanalysis/collector.go +++ b/pkg/certs/cert-inspection/certgraphanalysis/collector.go @@ -2,12 +2,14 @@ package certgraphanalysis import ( "context" + "fmt" "strings" "github.com/openshift/api/annotations" "github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" @@ -251,11 +253,16 @@ func MergePKILists(ctx context.Context, first, second *certgraphapi.PKIList) *ce } onDiskResourceData = deduplicateOnDiskMetadata(onDiskResourceData) + inMemoryResourceData := certgraphapi.PerInMemoryResourceData{ + CertKeyPairs: append(first.InMemoryResourceData.CertKeyPairs, second.InMemoryResourceData.CertKeyPairs...), + } + return &certgraphapi.PKIList{ CertificateAuthorityBundles: *caBundlesList, CertKeyPairs: *certList, InClusterResourceData: inClusterData, OnDiskResourceData: onDiskResourceData, + InMemoryResourceData: inMemoryResourceData, } } @@ -299,3 +306,84 @@ func GetBootstrapIPAndHostname(ctx context.Context, kubeClient kubernetes.Interf return bootstrapIP, bootstrapHostname, nil } + +type InMemoryCertDetail struct { + Namespace string + LabelSelector labels.Selector + Description string + NamePrefix string + Validity string + CertInfo certgraphapi.PKIRegistryCertKeyPairInfo +} + +// CreateInMemoryPKIList creates a PKIList listing in-memory certificate for each apiserver +func CreateInMemoryPKIList(ctx context.Context, kubeClient kubernetes.Interface, details []InMemoryCertDetail) (*certgraphapi.PKIList, error) { + errs := []error{} + result := &certgraphapi.PKIList{} + + for _, detail := range details { + err := addInMemoryCertificateStub(ctx, result, kubeClient, detail) + if err != nil { + errs = append(errs, fmt.Errorf("failed to add in-memory certificate stub for %#v: %w", detail, err)) + } + } + return result, utilerrors.NewAggregate(errs) + +} + +func addInMemoryCertificateStub(ctx context.Context, list *certgraphapi.PKIList, kubeClient kubernetes.Interface, detail InMemoryCertDetail) error { + if list == nil { + list = &certgraphapi.PKIList{} + } + + if list.InMemoryResourceData.CertKeyPairs == nil { + list.InMemoryResourceData.CertKeyPairs = []certgraphapi.PKIRegistryInMemoryCertKeyPair{} + } + if list.CertKeyPairs.Items == nil { + list.CertKeyPairs.Items = []certgraphapi.CertKeyPair{} + } + + // For each matched pod in namespace, create a cert key pair + podList, err := kubeClient.CoreV1().Pods(detail.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: detail.LabelSelector.String(), + }) + + if err != nil { + return err + } + + for i, pod := range podList.Items { + certKeyPair := certgraphapi.CertKeyPair{ + Name: fmt.Sprintf("%s-%d::1", detail.NamePrefix, i), + Description: detail.Description, + Spec: certgraphapi.CertKeyPairSpec{ + InMemoryLocations: []certgraphapi.InClusterPodLocation{ + { + Namespace: pod.Namespace, + // Using fake pod name to avoid removing IPs or hashes + Name: fmt.Sprintf("%s-%d", detail.NamePrefix, i), + }, + }, + CertMetadata: certgraphapi.CertKeyMetadata{ + ValidityDuration: detail.Validity, + CertIdentifier: certgraphapi.CertIdentifier{ + // PubkeyModulus needs to be unique so that the secret would not be removed during deduplication + PubkeyModulus: fmt.Sprintf("in-memory-%s-%d", detail.NamePrefix, i), + }, + }, + }, + } + + list.CertKeyPairs.Items = append(list.CertKeyPairs.Items, certKeyPair) + + list.InMemoryResourceData.CertKeyPairs = append(list.InMemoryResourceData.CertKeyPairs, certgraphapi.PKIRegistryInMemoryCertKeyPair{ + PodLocation: certgraphapi.InClusterPodLocation{ + Namespace: pod.Namespace, + Name: fmt.Sprintf("%s-%d", detail.NamePrefix, i), + }, + CertKeyInfo: detail.CertInfo, + }) + } + return nil + +} diff --git a/pkg/certs/cert-inspection/certgraphanalysis/collector_test.go b/pkg/certs/cert-inspection/certgraphanalysis/collector_test.go new file mode 100644 index 0000000000..76ccf953cd --- /dev/null +++ b/pkg/certs/cert-inspection/certgraphanalysis/collector_test.go @@ -0,0 +1,321 @@ +package certgraphanalysis + +import ( + "context" + "fmt" + "strings" + "testing" + + certgraphapi "github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clientgotesting "k8s.io/client-go/testing" +) + +func TestCreateInMemoryPKIList(t *testing.T) { + tests := []struct { + name string + details []InMemoryCertDetail + initialPods []*corev1.Pod + expectError bool + expectedCount int + expectedCertKeyPairs []certgraphapi.CertKeyPair + expectedInMemoryCertKeyPairs []certgraphapi.PKIRegistryInMemoryCertKeyPair + }{ + { + name: "Empty details", + details: []InMemoryCertDetail{}, + initialPods: []*corev1.Pod{}, + expectError: false, + expectedCount: 0, + expectedCertKeyPairs: []certgraphapi.CertKeyPair{}, + expectedInMemoryCertKeyPairs: []certgraphapi.PKIRegistryInMemoryCertKeyPair{}, + }, + { + name: "Single valid detail, one pod", + details: []InMemoryCertDetail{ + { + Namespace: "test-ns", + NamePrefix: "test-cert", + Description: "Test certificate", + LabelSelector: labels.Set(map[string]string{"app": "test"}).AsSelector(), + Validity: "1y", + CertInfo: certgraphapi.PKIRegistryCertKeyPairInfo{ + Description: "Test certificate info", + }, + }, + }, + initialPods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "pod-0", + Labels: map[string]string{"app": "test"}, + }, + }, + }, + expectError: false, + expectedCount: 1, + expectedCertKeyPairs: []certgraphapi.CertKeyPair{ + { + Name: "test-cert-0::1", + Description: "Test certificate", + Spec: certgraphapi.CertKeyPairSpec{ + InMemoryLocations: []certgraphapi.InClusterPodLocation{ + { + Namespace: "test-ns", + Name: "test-cert-0", + }, + }, + CertMetadata: certgraphapi.CertKeyMetadata{ + ValidityDuration: "1y", + CertIdentifier: certgraphapi.CertIdentifier{ + PubkeyModulus: "in-memory-test-cert-0", + }, + }, + }, + }, + }, + expectedInMemoryCertKeyPairs: []certgraphapi.PKIRegistryInMemoryCertKeyPair{ + { + PodLocation: certgraphapi.InClusterPodLocation{ + Namespace: "test-ns", + Name: "test-cert-0", + }, + CertKeyInfo: certgraphapi.PKIRegistryCertKeyPairInfo{ + Description: "Test certificate info", + }, + }, + }, + }, + { + name: "Single valid detail, two pods", + details: []InMemoryCertDetail{ + { + Namespace: "test-ns", + NamePrefix: "test-cert", + Description: "Test certificate", + LabelSelector: labels.Set(map[string]string{"app": "test"}).AsSelector(), + Validity: "2y", + CertInfo: certgraphapi.PKIRegistryCertKeyPairInfo{ + Description: "Another test cert info", + }, + }, + }, + initialPods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "pod-0", + Labels: map[string]string{"app": "test"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "pod-1", + Labels: map[string]string{"app": "test"}, + }, + }, + }, + expectError: false, + expectedCount: 2, + expectedCertKeyPairs: []certgraphapi.CertKeyPair{ + { + Name: "test-cert-0::1", + Description: "Test certificate", + Spec: certgraphapi.CertKeyPairSpec{ + InMemoryLocations: []certgraphapi.InClusterPodLocation{ + { + Namespace: "test-ns", + Name: "test-cert-0", + }, + }, + CertMetadata: certgraphapi.CertKeyMetadata{ + ValidityDuration: "2y", + CertIdentifier: certgraphapi.CertIdentifier{ + PubkeyModulus: "in-memory-test-cert-0", + }, + }, + }, + }, + { + Name: "test-cert-1::1", + Description: "Test certificate", + Spec: certgraphapi.CertKeyPairSpec{ + InMemoryLocations: []certgraphapi.InClusterPodLocation{ + { + Namespace: "test-ns", + Name: "test-cert-1", + }, + }, + CertMetadata: certgraphapi.CertKeyMetadata{ + ValidityDuration: "2y", + CertIdentifier: certgraphapi.CertIdentifier{ + PubkeyModulus: "in-memory-test-cert-1", + }, + }, + }, + }, + }, + expectedInMemoryCertKeyPairs: []certgraphapi.PKIRegistryInMemoryCertKeyPair{ + { + PodLocation: certgraphapi.InClusterPodLocation{ + Namespace: "test-ns", + Name: "test-cert-0", + }, + CertKeyInfo: certgraphapi.PKIRegistryCertKeyPairInfo{ + Description: "Another test cert info", + }, + }, + { + PodLocation: certgraphapi.InClusterPodLocation{ + Namespace: "test-ns", + Name: "test-cert-1", + }, + CertKeyInfo: certgraphapi.PKIRegistryCertKeyPairInfo{ + Description: "Another test cert info", + }, + }, + }, + }, + { + name: "No matching pods - returns no error, zero certs", + details: []InMemoryCertDetail{ + { + Namespace: "test-ns", + NamePrefix: "no-match-cert", + Description: "No matching pods certificate", + LabelSelector: labels.Set(map[string]string{"app": "no-match"}).AsSelector(), + Validity: "1y", + }, + }, + initialPods: []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "pod-0", + Labels: map[string]string{"app": "other"}, + }, + }, + }, + expectError: false, + expectedCount: 0, + expectedCertKeyPairs: []certgraphapi.CertKeyPair{}, + expectedInMemoryCertKeyPairs: []certgraphapi.PKIRegistryInMemoryCertKeyPair{}, + }, + { + name: "Pod listing error in addInMemoryCertificateStub", + details: []InMemoryCertDetail{ + { + Namespace: "error-ns", + NamePrefix: "error-cert", + Description: "Cert for error case", + LabelSelector: labels.Set(map[string]string{"app": "error"}).AsSelector(), + Validity: "3h", + }, + }, + initialPods: []*corev1.Pod{}, // No pods, but the error should come from the list call itself + expectError: true, + expectedCount: 0, + expectedCertKeyPairs: []certgraphapi.CertKeyPair{}, + expectedInMemoryCertKeyPairs: []certgraphapi.PKIRegistryInMemoryCertKeyPair{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.TODO() + clientObjects := []runtime.Object{} + for _, pod := range test.initialPods { + clientObjects = append(clientObjects, pod) + } + kubeClient := fake.NewSimpleClientset(clientObjects...) + + // Add reactor for the error case + if test.name == "Pod listing error in addInMemoryCertificateStub" { + kubeClient.PrependReactor("list", "pods", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { + listAction := action.(clientgotesting.ListAction) + if listAction.GetNamespace() == "error-ns" { + return true, nil, fmt.Errorf("simulated pod list error for namespace %s", listAction.GetNamespace()) + } + return false, nil, nil + }) + } + + result, err := CreateInMemoryPKIList(ctx, kubeClient, test.details) + + if test.expectError { + if err == nil { + t.Errorf("expected an error but got none") + } else if !strings.Contains(err.Error(), "simulated pod list error") { + t.Errorf("expected error to contain 'simulated pod list error', but got: %v", err) + } + } else if err != nil { + t.Errorf("did not expect an error but got: %v", err) + } + + if len(result.CertKeyPairs.Items) != test.expectedCount { + t.Errorf("expected %d items in result.CertKeyPairs.Items, but got %d", test.expectedCount, len(result.CertKeyPairs.Items)) + } + if len(result.InMemoryResourceData.CertKeyPairs) != test.expectedCount { + t.Errorf("expected %d items in result.InMemoryResourceData.CertKeyPairs, but got %d", test.expectedCount, len(result.InMemoryResourceData.CertKeyPairs)) + } + + // Detailed assertions for CertKeyPairs.Items + for i, expectedCert := range test.expectedCertKeyPairs { + if i >= len(result.CertKeyPairs.Items) { + t.Errorf("expected CertKeyPair at index %d, but none found", i) + continue + } + actualCert := result.CertKeyPairs.Items[i] + if actualCert.Name != expectedCert.Name { + t.Errorf("CertKeyPair %d: expected Name %q, got %q", i, expectedCert.Name, actualCert.Name) + } + if actualCert.Description != expectedCert.Description { + t.Errorf("CertKeyPair %d: expected Description %q, got %q", i, expectedCert.Description, actualCert.Description) + } + if len(actualCert.Spec.InMemoryLocations) != len(expectedCert.Spec.InMemoryLocations) { + t.Errorf("CertKeyPair %d: expected %d InMemoryLocations, got %d", i, len(expectedCert.Spec.InMemoryLocations), len(actualCert.Spec.InMemoryLocations)) + } else { + for j, expectedLoc := range expectedCert.Spec.InMemoryLocations { + actualLoc := actualCert.Spec.InMemoryLocations[j] + if actualLoc.Namespace != expectedLoc.Namespace { + t.Errorf("CertKeyPair %d, location %d: expected Namespace %q, got %q", i, j, expectedLoc.Namespace, actualLoc.Namespace) + } + if actualLoc.Name != expectedLoc.Name { + t.Errorf("CertKeyPair %d, location %d: expected Name %q, got %q", i, j, expectedLoc.Name, actualLoc.Name) + } + } + } + if actualCert.Spec.CertMetadata.ValidityDuration != expectedCert.Spec.CertMetadata.ValidityDuration { + t.Errorf("CertKeyPair %d: expected ValidityDuration %q, got %q", i, expectedCert.Spec.CertMetadata.ValidityDuration, actualCert.Spec.CertMetadata.ValidityDuration) + } + if actualCert.Spec.CertMetadata.CertIdentifier.PubkeyModulus != expectedCert.Spec.CertMetadata.CertIdentifier.PubkeyModulus { + t.Errorf("CertKeyPair %d: expected PubkeyModulus %q, got %q", i, expectedCert.Spec.CertMetadata.CertIdentifier.PubkeyModulus, actualCert.Spec.CertMetadata.CertIdentifier.PubkeyModulus) + } + } + + // Detailed assertions for InMemoryResourceData.CertKeyPairs + for i, expectedInMemoryCert := range test.expectedInMemoryCertKeyPairs { + if i >= len(result.InMemoryResourceData.CertKeyPairs) { + t.Errorf("expected InMemoryCertKeyPair at index %d, but none found", i) + continue + } + actualInMemoryCert := result.InMemoryResourceData.CertKeyPairs[i] + if actualInMemoryCert.PodLocation.Namespace != expectedInMemoryCert.PodLocation.Namespace { + t.Errorf("InMemoryCertKeyPair %d: expected PodLocation.Namespace %q, got %q", i, expectedInMemoryCert.PodLocation.Namespace, actualInMemoryCert.PodLocation.Namespace) + } + if actualInMemoryCert.PodLocation.Name != expectedInMemoryCert.PodLocation.Name { + t.Errorf("InMemoryCertKeyPair %d: expected PodLocation.Name %q, got %q", i, expectedInMemoryCert.PodLocation.Name, actualInMemoryCert.PodLocation.Name) + } + if actualInMemoryCert.CertKeyInfo.Description != expectedInMemoryCert.CertKeyInfo.Description { + t.Errorf("InMemoryCertKeyPair %d: expected CertKeyInfo.Description %q, got %q", i, expectedInMemoryCert.CertKeyInfo.Description, actualInMemoryCert.CertKeyInfo.Description) + } + } + }) + } +} diff --git a/pkg/certs/cert-inspection/certgraphanalysis/deduplication.go b/pkg/certs/cert-inspection/certgraphanalysis/deduplication.go index c89ec6bdda..70eeff4789 100644 --- a/pkg/certs/cert-inspection/certgraphanalysis/deduplication.go +++ b/pkg/certs/cert-inspection/certgraphanalysis/deduplication.go @@ -25,6 +25,7 @@ func deduplicateCertKeyPairs(in []*certgraphapi.CertKeyPair) []*certgraphapi.Cer found = true ret[j] = CombineSecretLocations(ret[j], currIn.Spec.SecretLocations) ret[j] = CombineCertOnDiskLocations(ret[j], currIn.Spec.OnDiskLocations) + ret[j] = CombineCertInMemoryLocations(ret[j], currIn.Spec.InMemoryLocations) break } } @@ -199,3 +200,20 @@ func deduplicateOnDiskMetadata(in certgraphapi.PerOnDiskResourceData) certgrapha } return out } + +// CombineCertInMemoryLocations returns a CertKeyPair with all in-memory locations from in and rhs de-duplicated into a single list +func CombineCertInMemoryLocations(in *certgraphapi.CertKeyPair, rhs []certgraphapi.InClusterPodLocation) *certgraphapi.CertKeyPair { + out := in.DeepCopy() + for _, curr := range rhs { + found := false + for _, existing := range in.Spec.InMemoryLocations { + if curr == existing { + found = true + } + } + if !found { + out.Spec.InMemoryLocations = append(out.Spec.InMemoryLocations, curr) + } + } + return out +} diff --git a/pkg/certs/cert-inspection/certgraphanalysis/deduplication_test.go b/pkg/certs/cert-inspection/certgraphanalysis/deduplication_test.go new file mode 100644 index 0000000000..39945225e2 --- /dev/null +++ b/pkg/certs/cert-inspection/certgraphanalysis/deduplication_test.go @@ -0,0 +1,185 @@ +package certgraphanalysis + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi" +) + +func newCertKeyPair(name, pubKeyModulus string, secretLocs []certgraphapi.InClusterSecretLocation, onDiskLocs []certgraphapi.OnDiskCertKeyPairLocation, inMemoryLocs []certgraphapi.InClusterPodLocation) *certgraphapi.CertKeyPair { + return &certgraphapi.CertKeyPair{ + Name: name, + Spec: certgraphapi.CertKeyPairSpec{ + CertMetadata: certgraphapi.CertKeyMetadata{ + CertIdentifier: certgraphapi.CertIdentifier{ + PubkeyModulus: pubKeyModulus, + }, + }, + SecretLocations: secretLocs, + OnDiskLocations: onDiskLocs, + InMemoryLocations: inMemoryLocs, + }, + } +} + +func TestDeduplicateCertKeyPairs(t *testing.T) { + tests := []struct { + name string + input []*certgraphapi.CertKeyPair + expected []*certgraphapi.CertKeyPair + panics bool + }{ + { + name: "Empty input", + input: []*certgraphapi.CertKeyPair{}, + expected: []*certgraphapi.CertKeyPair{}, + }, + { + name: "No duplicates", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", nil, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", nil, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + }, + }, + { + name: "Exact duplicates", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + }, + }, + { + name: "Duplicates with different locations merged", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns2", Name: "sec2"}}, nil, nil), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}, {Namespace: "ns2", Name: "sec2"}}, nil, nil), + }, + }, + { + name: "Duplicates with all location types merged", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("certA", "modX", + []certgraphapi.InClusterSecretLocation{{Namespace: "nsA", Name: "secA"}}, + []certgraphapi.OnDiskCertKeyPairLocation{{Cert: certgraphapi.OnDiskLocation{Path: "/path/to/certA.crt"}, Key: certgraphapi.OnDiskLocation{Path: "/path/to/certA.key"}}}, + []certgraphapi.InClusterPodLocation{{Namespace: "nsP", Name: "podA"}}), + newCertKeyPair("certA-dup", "modX", + []certgraphapi.InClusterSecretLocation{{Namespace: "nsB", Name: "secB"}}, + []certgraphapi.OnDiskCertKeyPairLocation{{Cert: certgraphapi.OnDiskLocation{Path: "/path/to/certB.crt"}, Key: certgraphapi.OnDiskLocation{Path: "/path/to/certB.key"}}}, + []certgraphapi.InClusterPodLocation{{Namespace: "nsQ", Name: "podB"}}), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("certA", "modX", + []certgraphapi.InClusterSecretLocation{{Namespace: "nsA", Name: "secA"}, {Namespace: "nsB", Name: "secB"}}, + []certgraphapi.OnDiskCertKeyPairLocation{ + {Cert: certgraphapi.OnDiskLocation{Path: "/path/to/certA.crt"}, Key: certgraphapi.OnDiskLocation{Path: "/path/to/certA.key"}}, + {Cert: certgraphapi.OnDiskLocation{Path: "/path/to/certB.crt"}, Key: certgraphapi.OnDiskLocation{Path: "/path/to/certB.key"}}, + }, + []certgraphapi.InClusterPodLocation{{Namespace: "nsP", Name: "podA"}, {Namespace: "nsQ", Name: "podB"}}), + }, + }, + { + name: "Different PubkeyModulus", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", nil, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", nil, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + }, + }, + { + name: "Mixed duplicates and unique", + input: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + newCertKeyPair("cert1-dup", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns3", Name: "sec3"}}, nil, nil), + }, + expected: []*certgraphapi.CertKeyPair{ + newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}, {Namespace: "ns3", Name: "sec3"}}, nil, nil), + newCertKeyPair("cert2", "mod2", nil, nil, nil), + }, + }, + { + name: "Input slice contains nil CertKeyPair", + input: []*certgraphapi.CertKeyPair{nil}, + panics: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.panics { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic as expected") + } + }() + } + + result := deduplicateCertKeyPairs(test.input) + + if !test.panics { + if len(result) != len(test.expected) { + t.Fatalf("Expected %d items, got %d. Result: %+v, Expected: %+v", len(test.expected), len(result), result, test.expected) + } + for i := range result { + // Use reflect.DeepEqual for comparing CertKeyPair structs more thoroughly + diff := cmp.Diff(result[i], test.expected[i]) + if diff != "" { + t.Errorf("Mismatch at index %d. diff: %s", i, diff) + } + } + } + }) + } +} + +func TestDeduplicateCertKeyPairList(t *testing.T) { + tests := []struct { + name string + input *certgraphapi.CertKeyPairList + expected *certgraphapi.CertKeyPairList + }{ + { + name: "Empty list", + input: &certgraphapi.CertKeyPairList{Items: []certgraphapi.CertKeyPair{}}, + expected: &certgraphapi.CertKeyPairList{Items: []certgraphapi.CertKeyPair{}}, + }, + { + name: "List with duplicates", + input: &certgraphapi.CertKeyPairList{ + Items: []certgraphapi.CertKeyPair{ + *newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}}, nil, nil), + *newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns2", Name: "sec2"}}, nil, nil), + }, + }, + expected: &certgraphapi.CertKeyPairList{ + Items: []certgraphapi.CertKeyPair{ + *newCertKeyPair("cert1", "mod1", []certgraphapi.InClusterSecretLocation{{Namespace: "ns1", Name: "sec1"}, {Namespace: "ns2", Name: "sec2"}}, nil, nil), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := deduplicateCertKeyPairList(test.input) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("Test %q failed. Expected %+v, got %+v", test.name, test.expected, result) + } + }) + } +} diff --git a/pkg/certs/cert-inspection/certgraphapi/type_registry.go b/pkg/certs/cert-inspection/certgraphapi/type_registry.go index 4d2698f3ef..a4d86dde14 100644 --- a/pkg/certs/cert-inspection/certgraphapi/type_registry.go +++ b/pkg/certs/cert-inspection/certgraphapi/type_registry.go @@ -1,8 +1,9 @@ package certgraphapi type PKIRegistryCertKeyPair struct { - InClusterLocation *PKIRegistryInClusterCertKeyPair - OnDiskLocation *PKIRegistryOnDiskCertKeyPair + InClusterLocation *PKIRegistryInClusterCertKeyPair + OnDiskLocation *PKIRegistryOnDiskCertKeyPair + InMemoryPodLocation *PKIRegistryInMemoryCertKeyPair } // PKIRegistryOnDiskCertKeyPair identifies certificate key pair on disk and stores its metadata @@ -13,6 +14,14 @@ type PKIRegistryOnDiskCertKeyPair struct { CertKeyInfo PKIRegistryCertKeyPairInfo `json:"certKeyInfo"` } +// PKIRegistryInMemoryCertKeyPair identifies certificate key pair and stores its metadata +type PKIRegistryInMemoryCertKeyPair struct { + // PodLocation points to the pod location + PodLocation InClusterPodLocation `json:"podLocation"` + // CertKeyInfo stores metadata for certificate key pair + CertKeyInfo PKIRegistryCertKeyPairInfo `json:"certKeyInfo"` +} + // PKIRegistryInClusterCertKeyPair identifies certificate key pair and stores its metadata type PKIRegistryInClusterCertKeyPair struct { // SecretLocation points to the secret location diff --git a/pkg/certs/cert-inspection/certgraphapi/types.go b/pkg/certs/cert-inspection/certgraphapi/types.go index ced300af83..90f91d401c 100644 --- a/pkg/certs/cert-inspection/certgraphapi/types.go +++ b/pkg/certs/cert-inspection/certgraphapi/types.go @@ -14,6 +14,7 @@ type PKIList struct { InClusterResourceData PerInClusterResourceData OnDiskResourceData PerOnDiskResourceData + InMemoryResourceData PerInMemoryResourceData CertificateAuthorityBundles CertificateAuthorityBundleList CertKeyPairs CertKeyPairList @@ -29,6 +30,12 @@ type PerInClusterResourceData struct { CertKeyPairs []PKIRegistryInClusterCertKeyPair `json:"certKeyPairs"` } +// PerInMemoryResourceData tracks metadata that corresponds to specific certificates stored in pod memory. +type PerInMemoryResourceData struct { + // +mapType:=atomic + CertKeyPairs []PKIRegistryInMemoryCertKeyPair `json:"certKeyPairs"` +} + // PerOnDiskResourceData tracks metadata that corresponds to specific files on disk. // This data should not duplicate the analysis of the certkeypair lists, but is pulled from files on disk. // It will be stitched together by a generator after the fact. @@ -89,8 +96,9 @@ type CertKeyPairStatus struct { } type CertKeyPairSpec struct { - SecretLocations []InClusterSecretLocation - OnDiskLocations []OnDiskCertKeyPairLocation + SecretLocations []InClusterSecretLocation + OnDiskLocations []OnDiskCertKeyPairLocation + InMemoryLocations []InClusterPodLocation CertMetadata CertKeyMetadata Details CertKeyPairDetails @@ -106,6 +114,11 @@ type InClusterConfigMapLocation struct { Name string } +type InClusterPodLocation struct { + Namespace string + Name string +} + type OnDiskCertKeyPairLocation struct { Cert OnDiskLocation Key OnDiskLocation