From e8b1fde84c48fa28e028ad6fc7707b1ef1e689df Mon Sep 17 00:00:00 2001 From: Piyush Tiwari <piyush.s.tiwari@oracle.com> Date: Thu, 30 Jan 2025 15:19:40 +0530 Subject: [PATCH] honor secret updates, modify cert names for tls secret to be secret-uid unique MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change certificate name created to oci-nic-<secret-uid> - add watching for secret add/update - add etag to cabundle and cert cache, start using cabundle cache - create certificates and cabundles with hashes for secret type TLS - start updating certs for secret type artifacts - start updating ca bundles for secret type artifacts - prune certificates when updating for secret type artifacts Co-authored-by: Arnold Gálovics <arnold_galovics@docktape.com> Co-authored-by: Piyush Tiwari <piyush.s.tiwari@oracle.com> --- main.go | 20 +- pkg/certificate/certificate.go | 147 ++++++- pkg/certificate/certificate_test.go | 226 ++++++++++- pkg/controllers/ingress/certificate_util.go | 365 ++++++++++++++++++ .../ingress/certificate_util_test.go | 318 +++++++++++++++ pkg/controllers/ingress/ingress.go | 52 ++- .../ingress/ingressPathWithTlsSecret.yaml | 22 ++ pkg/controllers/ingress/ingress_test.go | 50 ++- pkg/controllers/ingress/util.go | 357 +++++++---------- pkg/controllers/ingress/util_test.go | 298 +++++++++++--- pkg/exception/util.go | 24 ++ pkg/loadbalancer/loadbalancer.go | 21 +- pkg/oci/client/certificate.go | 2 + pkg/oci/client/certificatemanagement.go | 24 ++ pkg/server/server.go | 7 +- pkg/util/util.go | 9 + 16 files changed, 1604 insertions(+), 338 deletions(-) create mode 100644 pkg/controllers/ingress/certificate_util.go create mode 100644 pkg/controllers/ingress/certificate_util_test.go create mode 100644 pkg/controllers/ingress/ingressPathWithTlsSecret.yaml diff --git a/main.go b/main.go index ec73ace6..3ba8454f 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ package main import ( "context" "flag" + corev1 "k8s.io/api/core/v1" "net/http" "os" "os/signal" @@ -144,7 +145,12 @@ func main() { } }() - informerFactory := informers.NewSharedInformerFactory(client, 1*time.Minute) + informerFactory := informers.NewSharedInformerFactoryWithOptions(client, 1*time.Minute, + informers.WithCustomResyncConfig( + map[metav1.Object]time.Duration{ + &corev1.Secret{}: 0, + }, + )) // listen for interrupts or the Linux SIGTERM signal and cancel // our context, which the leader election code will observe and @@ -157,13 +163,13 @@ func main() { cancel() }() - ingressClassInformer, ingressInformer, serviceInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer := setupInformers(informerFactory, ctx, classParamInformer) + ingressClassInformer, ingressInformer, serviceInformer, secretInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer := setupInformers(informerFactory, ctx, classParamInformer) server.SetupWebhookServer(ingressInformer, serviceInformer, client, ctx) mux := http.NewServeMux() reg, err := server.SetupMetricsServer(opts.MetricsBackend, opts.MetricsPort, mux, ctx) - run := server.SetUpControllers(opts, ingressClassInformer, ingressInformer, client, serviceInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer, c, reg) + run := server.SetUpControllers(opts, ingressClassInformer, ingressInformer, client, serviceInformer, secretInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer, c, reg) metric.ServeMetrics(opts.MetricsPort, mux) // we use the Lease lock type since edits to Leases are less common @@ -214,7 +220,7 @@ func main() { }) } -func setupInformers(informerFactory informers.SharedInformerFactory, ctx context.Context, classParamInformer ctrcache.Informer) (networkinginformers.IngressClassInformer, networkinginformers.IngressInformer, v1.ServiceInformer, v1.EndpointsInformer, v1.PodInformer, v1.NodeInformer, v1.ServiceAccountInformer) { +func setupInformers(informerFactory informers.SharedInformerFactory, ctx context.Context, classParamInformer ctrcache.Informer) (networkinginformers.IngressClassInformer, networkinginformers.IngressInformer, v1.ServiceInformer, v1.SecretInformer, v1.EndpointsInformer, v1.PodInformer, v1.NodeInformer, v1.ServiceAccountInformer) { ingressClassInformer := informerFactory.Networking().V1().IngressClasses() go ingressClassInformer.Informer().Run(ctx.Done()) @@ -224,6 +230,9 @@ func setupInformers(informerFactory informers.SharedInformerFactory, ctx context serviceInformer := informerFactory.Core().V1().Services() go serviceInformer.Informer().Run(ctx.Done()) + secretInformer := informerFactory.Core().V1().Secrets() + go secretInformer.Informer().Run(ctx.Done()) + endpointInformer := informerFactory.Core().V1().Endpoints() go endpointInformer.Informer().Run(ctx.Done()) @@ -245,6 +254,7 @@ func setupInformers(informerFactory informers.SharedInformerFactory, ctx context ingressClassInformer.Informer().HasSynced, ingressInformer.Informer().HasSynced, serviceInformer.Informer().HasSynced, + secretInformer.Informer().HasSynced, endpointInformer.Informer().HasSynced, podInformer.Informer().HasSynced, classParamInformer.HasSynced, @@ -253,5 +263,5 @@ func setupInformers(informerFactory informers.SharedInformerFactory, ctx context klog.Fatal("failed to sync informers") } - return ingressClassInformer, ingressInformer, serviceInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer + return ingressClassInformer, ingressInformer, serviceInformer, secretInformer, endpointInformer, podInformer, nodeInformer, serviceAccountInformer } diff --git a/pkg/certificate/certificate.go b/pkg/certificate/certificate.go index 95d596e9..61978d96 100644 --- a/pkg/certificate/certificate.go +++ b/pkg/certificate/certificate.go @@ -11,6 +11,7 @@ package certificate import ( "context" + "fmt" "net/http" "sync" "time" @@ -22,6 +23,10 @@ import ( "k8s.io/klog/v2" ) +const ( + certificateServiceTimeout = 2 * time.Minute +) + type CertificatesClient struct { ManagementClient CertificateManagementInterface CertificatesClient CertificateInterface @@ -42,9 +47,9 @@ func New(managementClient CertificateManagementInterface, } } -func (certificatesClient *CertificatesClient) SetCertCache(cert *certificatesmanagement.Certificate) { +func (certificatesClient *CertificatesClient) SetCertCache(cert *certificatesmanagement.Certificate, etag string) { certificatesClient.certMu.Lock() - certificatesClient.CertCache[*cert.Id] = &CertCacheObj{Cert: cert, Age: time.Now()} + certificatesClient.CertCache[*cert.Id] = &CertCacheObj{Cert: cert, Age: time.Now(), Etag: etag} certificatesClient.certMu.Unlock() } @@ -54,9 +59,9 @@ func (certificatesClient *CertificatesClient) GetFromCertCache(certId string) *C return certificatesClient.CertCache[certId] } -func (certificatesClient *CertificatesClient) SetCaBundleCache(caBundle *certificatesmanagement.CaBundle) { +func (certificatesClient *CertificatesClient) SetCaBundleCache(caBundle *certificatesmanagement.CaBundle, etag string) { certificatesClient.caMu.Lock() - certificatesClient.CaBundleCache[*caBundle.Id] = &CaBundleCacheObj{CaBundle: caBundle, Age: time.Now()} + certificatesClient.CaBundleCache[*caBundle.Id] = &CaBundleCacheObj{CaBundle: caBundle, Age: time.Now(), Etag: etag} certificatesClient.caMu.Unlock() } @@ -67,37 +72,37 @@ func (certificatesClient *CertificatesClient) GetFromCaBundleCache(id string) *C } func (certificatesClient *CertificatesClient) CreateCertificate(ctx context.Context, - req certificatesmanagement.CreateCertificateRequest) (*certificatesmanagement.Certificate, error) { + req certificatesmanagement.CreateCertificateRequest) (*certificatesmanagement.Certificate, string, error) { resp, err := certificatesClient.ManagementClient.CreateCertificate(ctx, req) if err != nil { klog.Errorf("Error creating certificate %s, %s ", *req.Name, err.Error()) - return nil, err + return nil, "", err } - return &resp.Certificate, nil + return certificatesClient.waitForActiveCertificate(ctx, *resp.Certificate.Id) } func (certificatesClient *CertificatesClient) CreateCaBundle(ctx context.Context, - req certificatesmanagement.CreateCaBundleRequest) (*certificatesmanagement.CaBundle, error) { + req certificatesmanagement.CreateCaBundleRequest) (*certificatesmanagement.CaBundle, string, error) { resp, err := certificatesClient.ManagementClient.CreateCaBundle(ctx, req) if err != nil { klog.Errorf("Error creating ca bundle %s, %s ", *req.Name, err.Error()) - return nil, err + return nil, "", err } - return &resp.CaBundle, nil + return certificatesClient.waitForActiveCaBundle(ctx, *resp.CaBundle.Id) } func (certificatesClient *CertificatesClient) GetCertificate(ctx context.Context, - req certificatesmanagement.GetCertificateRequest) (*certificatesmanagement.Certificate, error) { + req certificatesmanagement.GetCertificateRequest) (*certificatesmanagement.Certificate, string, error) { klog.Infof("Getting certificate for ocid %s ", *req.CertificateId) resp, err := certificatesClient.ManagementClient.GetCertificate(ctx, req) if err != nil { klog.Errorf("Error getting certificate %s, %s ", *req.CertificateId, err.Error()) - return nil, err + return nil, "", err } - return &resp.Certificate, nil + return &resp.Certificate, *resp.Etag, nil } func (certificatesClient *CertificatesClient) ListCertificates(ctx context.Context, @@ -112,6 +117,21 @@ func (certificatesClient *CertificatesClient) ListCertificates(ctx context.Conte return &resp.CertificateCollection, resp.OpcNextPage, nil } +func (certificatesClient *CertificatesClient) UpdateCertificate(ctx context.Context, + req certificatesmanagement.UpdateCertificateRequest) (*certificatesmanagement.Certificate, string, error) { + _, err := certificatesClient.ManagementClient.UpdateCertificate(ctx, req) + if err != nil { + if !util.IsServiceError(err, 409) { + klog.Errorf("Error updating certificate %s: %s", *req.CertificateId, err) + } else { + klog.Errorf("Error updating certificate %s due to 409-Conflict", *req.CertificateId) + } + return nil, "", err + } + + return certificatesClient.waitForActiveCertificate(ctx, *req.CertificateId) +} + func (certificatesClient *CertificatesClient) ScheduleCertificateDeletion(ctx context.Context, req certificatesmanagement.ScheduleCertificateDeletionRequest) error { _, err := certificatesClient.ManagementClient.ScheduleCertificateDeletion(ctx, req) @@ -122,16 +142,68 @@ func (certificatesClient *CertificatesClient) ScheduleCertificateDeletion(ctx co return nil } +func (certificatesClient *CertificatesClient) ListCertificateVersions(ctx context.Context, + req certificatesmanagement.ListCertificateVersionsRequest) (*certificatesmanagement.CertificateVersionCollection, *string, error) { + resp, err := certificatesClient.ManagementClient.ListCertificateVersions(ctx, req) + if err != nil { + klog.Errorf("Error listing certificate versions for request %s, %s ", util.PrettyPrint(req), err.Error()) + return nil, nil, err + } + + return &resp.CertificateVersionCollection, resp.OpcNextPage, nil +} + +func (certificatesClient *CertificatesClient) ScheduleCertificateVersionDeletion(ctx context.Context, + req certificatesmanagement.ScheduleCertificateVersionDeletionRequest) (*certificatesmanagement.Certificate, string, error) { + klog.Infof("Scheduling version %d of Certificate %s for deletion", *req.CertificateVersionNumber, *req.CertificateId) + _, err := certificatesClient.ManagementClient.ScheduleCertificateVersionDeletion(ctx, req) + if err != nil { + klog.Errorf("Error scheduling certificate version for deletion, certificateId %s, version %d, %s ", + *req.CertificateId, *req.CertificateVersionNumber, err.Error()) + return nil, "", err + } + + return certificatesClient.waitForActiveCertificate(ctx, *req.CertificateId) +} + +func (certificatesClient *CertificatesClient) waitForActiveCertificate(ctx context.Context, + certificateId string) (*certificatesmanagement.Certificate, string, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, certificateServiceTimeout) + defer cancel() + + for { + resp, err := certificatesClient.ManagementClient.GetCertificate(timeoutCtx, certificatesmanagement.GetCertificateRequest{ + CertificateId: &certificateId, + }) + if err != nil { + return nil, "", err + } + + if resp.Certificate.LifecycleState == certificatesmanagement.CertificateLifecycleStateActive { + return &resp.Certificate, *resp.Etag, nil + } + + if resp.Certificate.LifecycleState != certificatesmanagement.CertificateLifecycleStateUpdating && + resp.Certificate.LifecycleState != certificatesmanagement.CertificateLifecycleStateCreating { + return nil, "", fmt.Errorf("certificate %s went into an unexpected state %s while updating", + *resp.Certificate.Id, resp.Certificate.LifecycleState) + } + + klog.Infof("Certificate %s still not active, waiting", certificateId) + time.Sleep(3 * time.Second) + } +} + func (certificatesClient *CertificatesClient) GetCaBundle(ctx context.Context, - req certificatesmanagement.GetCaBundleRequest) (*certificatesmanagement.CaBundle, error) { + req certificatesmanagement.GetCaBundleRequest) (*certificatesmanagement.CaBundle, string, error) { klog.Infof("Getting ca bundle with ocid %s ", *req.CaBundleId) resp, err := certificatesClient.ManagementClient.GetCaBundle(ctx, req) if err != nil { klog.Errorf("Error getting certificate %s, %s ", *req.CaBundleId, err.Error()) - return nil, err + return nil, "", err } - return &resp.CaBundle, nil + return &resp.CaBundle, *resp.Etag, nil } func (certificatesClient *CertificatesClient) ListCaBundles(ctx context.Context, @@ -146,6 +218,21 @@ func (certificatesClient *CertificatesClient) ListCaBundles(ctx context.Context, return &resp.CaBundleCollection, nil } +func (certificatesClient *CertificatesClient) UpdateCaBundle(ctx context.Context, + req certificatesmanagement.UpdateCaBundleRequest) (*certificatesmanagement.CaBundle, string, error) { + _, err := certificatesClient.ManagementClient.UpdateCaBundle(ctx, req) + if err != nil { + if !util.IsServiceError(err, 409) { + klog.Errorf("Error updating ca bundle %s: %s", *req.CaBundleId, err) + } else { + klog.Errorf("Error updating ca bundle %s due to 409-Conflict", *req.CaBundleId) + } + return nil, "", err + } + + return certificatesClient.waitForActiveCaBundle(ctx, *req.CaBundleId) +} + func (certificatesClient *CertificatesClient) DeleteCaBundle(ctx context.Context, req certificatesmanagement.DeleteCaBundleRequest) (*http.Response, error) { klog.Infof("Deleting ca bundle with ocid %s ", *req.CaBundleId) @@ -158,6 +245,34 @@ func (certificatesClient *CertificatesClient) DeleteCaBundle(ctx context.Context return resp.HTTPResponse(), nil } +func (certificatesClient *CertificatesClient) waitForActiveCaBundle(ctx context.Context, + caBundleId string) (*certificatesmanagement.CaBundle, string, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, certificateServiceTimeout) + defer cancel() + + for { + resp, err := certificatesClient.ManagementClient.GetCaBundle(timeoutCtx, certificatesmanagement.GetCaBundleRequest{ + CaBundleId: &caBundleId, + }) + if err != nil { + return nil, "", err + } + + if resp.CaBundle.LifecycleState == certificatesmanagement.CaBundleLifecycleStateActive { + return &resp.CaBundle, *resp.Etag, nil + } + + if resp.CaBundle.LifecycleState != certificatesmanagement.CaBundleLifecycleStateUpdating && + resp.CaBundle.LifecycleState != certificatesmanagement.CaBundleLifecycleStateCreating { + return nil, "", fmt.Errorf("ca bundle %s went into an unexpected state %s while updating", + *resp.CaBundle.Id, resp.CaBundle.LifecycleState) + } + + klog.Infof("cabundle %s still not active, waiting", caBundleId) + time.Sleep(3 * time.Second) + } +} + func (certificatesClient *CertificatesClient) GetCertificateBundle(ctx context.Context, req certificates.GetCertificateBundleRequest) (certificates.CertificateBundle, error) { klog.Infof("Getting certificate bundle for certificate ocid %s ", *req.CertificateId) diff --git a/pkg/certificate/certificate_test.go b/pkg/certificate/certificate_test.go index 49f3bb97..199dcadb 100644 --- a/pkg/certificate/certificate_test.go +++ b/pkg/certificate/certificate_test.go @@ -14,7 +14,12 @@ import ( ociclient "github.com/oracle/oci-native-ingress-controller/pkg/oci/client" ) -const ErrorListingCaBundle = "error listing Ca Bundles" +const ( + ErrorListingCaBundle = "error listing Ca Bundles" + errorMsg = "no cert found" + namespace = "test" + errorImportCert = "errorImportCert" +) func setup() *CertificatesClient { certClient := GetCertClient() @@ -38,7 +43,7 @@ func TestCertificatesClient_Cache(t *testing.T) { OpcRetryToken: nil, RequestMetadata: common.RequestMetadata{}, } - cert, err := client.CreateCertificate(context.TODO(), request) + cert, _, err := client.CreateCertificate(context.TODO(), request) Expect(err).Should(BeNil()) Expect(cert).Should(Not(BeNil())) @@ -54,7 +59,7 @@ func TestCertificatesClient_CreateCertificate(t *testing.T) { OpcRetryToken: nil, RequestMetadata: common.RequestMetadata{}, } - cert, err := client.CreateCertificate(context.TODO(), request) + cert, _, err := client.CreateCertificate(context.TODO(), request) Expect(err).Should(BeNil()) Expect(cert).Should(Not(BeNil())) @@ -70,7 +75,7 @@ func TestCertificatesClient_CreateCaBundle(t *testing.T) { OpcRetryToken: nil, RequestMetadata: common.RequestMetadata{}, } - cert, err := client.CreateCaBundle(context.TODO(), request) + cert, _, err := client.CreateCaBundle(context.TODO(), request) Expect(err).Should(BeNil()) Expect(cert).Should(Not(BeNil())) @@ -84,8 +89,9 @@ func TestCertificatesClient_GetCertificate(t *testing.T) { OpcRequestId: nil, RequestMetadata: common.RequestMetadata{}, } - cert, err := client.GetCertificate(context.TODO(), request) + cert, etag, err := client.GetCertificate(context.TODO(), request) Expect(err).Should(BeNil()) + Expect(etag).Should(Equal("etag")) Expect(cert).Should(Not(BeNil())) } @@ -103,6 +109,61 @@ func TestCertificatesClient_ListCertificates(t *testing.T) { Expect(cert).Should(Not(BeNil())) } + +func TestCertificatesClient_UpdateCertificate(t *testing.T) { + RegisterTestingT(t) + client := setup() + + request := certificatesmanagement.UpdateCertificateRequest{ + CertificateId: common.String("id"), + RequestMetadata: common.RequestMetadata{}, + } + cert, _, err := client.UpdateCertificate(context.TODO(), request) + Expect(err).Should(BeNil()) + Expect(cert).Should(Not(BeNil())) + + request.CertificateId = common.String("error") + cert, _, err = client.UpdateCertificate(context.TODO(), request) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} + +func TestCertificatesClient_ListCertificateVersions(t *testing.T) { + RegisterTestingT(t) + client := setup() + + request := certificatesmanagement.ListCertificateVersionsRequest{ + CertificateId: common.String("id"), + SortOrder: certificatesmanagement.ListCertificateVersionsSortOrderDesc, + } + certVersionSummary, _, err := client.ListCertificateVersions(context.TODO(), request) + Expect(err).Should(BeNil()) + Expect(certVersionSummary).ShouldNot(BeNil()) +} + +func TestCertificatesClient_ScheduleCertificateVersionDeletion(t *testing.T) { + RegisterTestingT(t) + client := setup() + + request := certificatesmanagement.ScheduleCertificateVersionDeletionRequest{ + CertificateId: common.String("id"), + CertificateVersionNumber: common.Int64(3), + } + cert, _, err := client.ScheduleCertificateVersionDeletion(context.TODO(), request) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) +} + +func TestCertificatesClient_waitForActiveCertificate(t *testing.T) { + RegisterTestingT(t) + client := setup() + + certificateId := "id" + cert, _, err := client.waitForActiveCertificate(context.TODO(), certificateId) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) +} + func TestCertificatesClient_GetCaBundle(t *testing.T) { RegisterTestingT(t) client := setup() @@ -112,8 +173,9 @@ func TestCertificatesClient_GetCaBundle(t *testing.T) { OpcRequestId: nil, RequestMetadata: common.RequestMetadata{}, } - caBundle, err := client.GetCaBundle(context.TODO(), request) + caBundle, etag, err := client.GetCaBundle(context.TODO(), request) Expect(err).Should(BeNil()) + Expect(etag).Should(Equal("etag")) Expect(caBundle).Should(Not(BeNil())) } @@ -160,6 +222,24 @@ func TestCertificatesClient_ListCaBundles(t *testing.T) { } +func TestCertificatesClient_UpdateCaBundle(t *testing.T) { + RegisterTestingT(t) + client := setup() + + request := certificatesmanagement.UpdateCaBundleRequest{ + CaBundleId: common.String("id"), + RequestMetadata: common.RequestMetadata{}, + } + cert, _, err := client.UpdateCaBundle(context.TODO(), request) + Expect(err).Should(BeNil()) + Expect(cert).Should(Not(BeNil())) + + request.CaBundleId = common.String("error") + cert, _, err = client.UpdateCaBundle(context.TODO(), request) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} + func TestScheduleCertificateDeletion(t *testing.T) { RegisterTestingT(t) client := setup() @@ -197,6 +277,16 @@ func TestDeleteCaBundle(t *testing.T) { Expect(err).Should(Not(BeNil())) } +func TestCertificatesClient_waitForActiveCaBundle(t *testing.T) { + RegisterTestingT(t) + client := setup() + + certificateId := "id" + caBundle, _, err := client.waitForActiveCaBundle(context.TODO(), certificateId) + Expect(err).Should(BeNil()) + Expect(caBundle).ShouldNot(BeNil()) +} + func getDeleteCaBundleRequest(id string) certificatesmanagement.DeleteCaBundleRequest { request := certificatesmanagement.DeleteCaBundleRequest{ CaBundleId: &id, @@ -215,22 +305,71 @@ type MockCertificateManagerClient struct { } func (m MockCertificateManagerClient) CreateCertificate(ctx context.Context, request certificatesmanagement.CreateCertificateRequest) (certificatesmanagement.CreateCertificateResponse, error) { - return certificatesmanagement.CreateCertificateResponse{}, nil + id := "id" + etag := "etag" + return certificatesmanagement.CreateCertificateResponse{ + RawResponse: nil, + Certificate: certificatesmanagement.Certificate{ + Id: &id, + }, + Etag: &etag, + OpcRequestId: &id, + }, nil } func (m MockCertificateManagerClient) GetCertificate(ctx context.Context, request certificatesmanagement.GetCertificateRequest) (certificatesmanagement.GetCertificateResponse, error) { - return certificatesmanagement.GetCertificateResponse{}, nil + + if *request.CertificateId == "error" { + return certificatesmanagement.GetCertificateResponse{}, errors.New(errorMsg) + } + id := "id" + name := "cert" + authorityId := "authId" + etag := "etag" + var confType certificatesmanagement.CertificateConfigTypeEnum + if *request.CertificateId == errorImportCert { + name = "error" + confType = certificatesmanagement.CertificateConfigTypeImported + } else { + confType, _ = certificatesmanagement.GetMappingCertificateConfigTypeEnum(*request.CertificateId) + } + var number int64 + number = 234 + certVersionSummary := certificatesmanagement.CertificateVersionSummary{ + VersionNumber: &number, + } + return certificatesmanagement.GetCertificateResponse{ + RawResponse: nil, + Certificate: certificatesmanagement.Certificate{ + Id: &id, + Name: &name, + ConfigType: confType, + IssuerCertificateAuthorityId: &authorityId, + CurrentVersion: &certVersionSummary, + LifecycleState: certificatesmanagement.CertificateLifecycleStateActive, + }, + Etag: &etag, + OpcRequestId: nil, + }, nil } func (m MockCertificateManagerClient) ListCertificates(ctx context.Context, request certificatesmanagement.ListCertificatesRequest) (certificatesmanagement.ListCertificatesResponse, error) { + id := "id" return certificatesmanagement.ListCertificatesResponse{ RawResponse: nil, CertificateCollection: certificatesmanagement.CertificateCollection{}, - OpcRequestId: nil, - OpcNextPage: common.String("next"), + OpcRequestId: &id, + OpcNextPage: &id, }, nil } +func (m MockCertificateManagerClient) UpdateCertificate(ctx context.Context, request certificatesmanagement.UpdateCertificateRequest) (certificatesmanagement.UpdateCertificateResponse, error) { + if *request.CertificateId == "error" { + return certificatesmanagement.UpdateCertificateResponse{}, errors.New("cannot find certificate") + } + return certificatesmanagement.UpdateCertificateResponse{}, nil +} + func (m MockCertificateManagerClient) ScheduleCertificateDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateDeletionRequest) (certificatesmanagement.ScheduleCertificateDeletionResponse, error) { var err error if *request.CertificateId == "error" { @@ -239,21 +378,78 @@ func (m MockCertificateManagerClient) ScheduleCertificateDeletion(ctx context.Co return certificatesmanagement.ScheduleCertificateDeletionResponse{}, err } +func (m MockCertificateManagerClient) ListCertificateVersions(ctx context.Context, request certificatesmanagement.ListCertificateVersionsRequest) (certificatesmanagement.ListCertificateVersionsResponse, error) { + return certificatesmanagement.ListCertificateVersionsResponse{}, nil +} + +func (m MockCertificateManagerClient) ScheduleCertificateVersionDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateVersionDeletionRequest) (certificatesmanagement.ScheduleCertificateVersionDeletionResponse, error) { + return certificatesmanagement.ScheduleCertificateVersionDeletionResponse{}, nil +} + func (m MockCertificateManagerClient) CreateCaBundle(ctx context.Context, request certificatesmanagement.CreateCaBundleRequest) (certificatesmanagement.CreateCaBundleResponse, error) { - return certificatesmanagement.CreateCaBundleResponse{}, nil + id := "id" + etag := "etag" + return certificatesmanagement.CreateCaBundleResponse{ + RawResponse: nil, + CaBundle: certificatesmanagement.CaBundle{ + Id: &id, + }, + Etag: &etag, + OpcRequestId: nil, + }, nil } func (m MockCertificateManagerClient) GetCaBundle(ctx context.Context, request certificatesmanagement.GetCaBundleRequest) (certificatesmanagement.GetCaBundleResponse, error) { - return certificatesmanagement.GetCaBundleResponse{}, nil + id := "id" + name := "cabundle" + etag := "etag" + return certificatesmanagement.GetCaBundleResponse{ + RawResponse: nil, + CaBundle: certificatesmanagement.CaBundle{ + Id: &id, + Name: &name, + LifecycleState: certificatesmanagement.CaBundleLifecycleStateActive, + }, + OpcRequestId: &id, + Etag: &etag, + }, nil } func (m MockCertificateManagerClient) ListCaBundles(ctx context.Context, request certificatesmanagement.ListCaBundlesRequest) (certificatesmanagement.ListCaBundlesResponse, error) { - if request.LifecycleState == certificatesmanagement.ListCaBundlesLifecycleStateActive { + if request.LifecycleState == certificatesmanagement.ListCaBundlesLifecycleStateDeleted { + err := errors.New(ErrorListingCaBundle) + return certificatesmanagement.ListCaBundlesResponse{}, err + } + + if *request.Name == "error" { return certificatesmanagement.ListCaBundlesResponse{}, nil } - err := errors.New(ErrorListingCaBundle) - return certificatesmanagement.ListCaBundlesResponse{}, err + + var items []certificatesmanagement.CaBundleSummary + name := "ic-oci-config" + id := "id" + item := certificatesmanagement.CaBundleSummary{ + Id: &id, + Name: &name, + } + items = append(items, item) + + return certificatesmanagement.ListCaBundlesResponse{ + RawResponse: nil, + CaBundleCollection: certificatesmanagement.CaBundleCollection{ + Items: items, + }, + OpcRequestId: nil, + OpcNextPage: nil, + }, nil +} + +func (m MockCertificateManagerClient) UpdateCaBundle(ctx context.Context, request certificatesmanagement.UpdateCaBundleRequest) (certificatesmanagement.UpdateCaBundleResponse, error) { + if *request.CaBundleId == "error" { + return certificatesmanagement.UpdateCaBundleResponse{}, errors.New("cannot find ca bundle") + } + return certificatesmanagement.UpdateCaBundleResponse{}, nil } func (m MockCertificateManagerClient) DeleteCaBundle(ctx context.Context, request certificatesmanagement.DeleteCaBundleRequest) (certificatesmanagement.DeleteCaBundleResponse, error) { diff --git a/pkg/controllers/ingress/certificate_util.go b/pkg/controllers/ingress/certificate_util.go new file mode 100644 index 00000000..79d30b2d --- /dev/null +++ b/pkg/controllers/ingress/certificate_util.go @@ -0,0 +1,365 @@ +/* + * + * * OCI Native Ingress Controller + * * + * * Copyright (c) 2024 Oracle America, Inc. and its affiliates. + * * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + * + */ + +package ingress + +import ( + "context" + "fmt" + "github.com/oracle/oci-go-sdk/v65/certificatesmanagement" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-native-ingress-controller/pkg/certificate" + "github.com/oracle/oci-native-ingress-controller/pkg/util" + "k8s.io/klog/v2" + "time" +) + +// Interacting with OCI Cert service, opinionated for Ingress Controller + +func GetCertificate(certificateId *string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, string, error) { + certCacheObj := certificatesClient.GetFromCertCache(*certificateId) + if certCacheObj != nil { + now := time.Now() + if now.Sub(certCacheObj.Age).Minutes() < util.CertificateCacheMaxAgeInMinutes { + return certCacheObj.Cert, certCacheObj.Etag, nil + } + klog.Infof("Refreshing certificate %s", *certificateId) + } + return getCertificateBustCache(*certificateId, certificatesClient) +} + +func getCertificateBustCache(certificateId string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, string, error) { + getCertificateRequest := certificatesmanagement.GetCertificateRequest{ + CertificateId: &certificateId, + } + + cert, etag, err := certificatesClient.GetCertificate(context.TODO(), getCertificateRequest) + if err == nil { + certificatesClient.SetCertCache(cert, etag) + } + return cert, etag, err +} + +// VerifyOrGetCertificateIdByName verifies that cert with certificateId has name certificateName +// If not, try to find certificate with matching name +func VerifyOrGetCertificateIdByName(certificateId string, certificateName string, compartmentId string, + certificatesClient *certificate.CertificatesClient) (string, error) { + if certificateId != "" { + cert, _, err := GetCertificate(&certificateId, certificatesClient) + if err != nil { + return "", err + } + + if *cert.Name == certificateName { + return certificateId, nil + } + } + + return FindCertificateWithName(certificateName, compartmentId, certificatesClient) +} + +func FindCertificateWithName(certificateName string, compartmentId string, + certificatesClient *certificate.CertificatesClient) (string, error) { + listCertificatesRequest := certificatesmanagement.ListCertificatesRequest{ + Name: &certificateName, + CompartmentId: &compartmentId, + LifecycleState: certificatesmanagement.ListCertificatesLifecycleStateActive, + } + + klog.Infof("Searching for certificates with name %s in compartment %s.", certificateName, compartmentId) + listCertificates, _, err := certificatesClient.ListCertificates(context.TODO(), listCertificatesRequest) + if err != nil { + return "", err + } + + if listCertificates.Items != nil { + numberOfCertificates := len(listCertificates.Items) + klog.Infof("Found %d certificates with name %s in compartment %s.", numberOfCertificates, certificateName, compartmentId) + if numberOfCertificates > 0 { + return *listCertificates.Items[0].Id, nil + } + } + klog.Infof("Found no certificates with name %s in compartment %s.", certificateName, compartmentId) + return "", nil +} + +func CreateImportedTypeCertificate(tlsSecretData *TLSSecretData, certificateName string, compartmentId string, + certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, error) { + configDetails := certificatesmanagement.CreateCertificateByImportingConfigDetails{ + CertChainPem: tlsSecretData.CaCertificateChain, + CertificatePem: tlsSecretData.ServerCertificate, + PrivateKeyPem: tlsSecretData.PrivateKey, + } + + certificateDetails := certificatesmanagement.CreateCertificateDetails{ + Name: &certificateName, + CertificateConfig: configDetails, + CompartmentId: &compartmentId, + FreeformTags: map[string]string{ + certificateHashTagKey: hashPublicTlsData(tlsSecretData), + }, + } + createCertificateRequest := certificatesmanagement.CreateCertificateRequest{ + CreateCertificateDetails: certificateDetails, + OpcRetryToken: &certificateName, + } + + createCertificate, etag, err := certificatesClient.CreateCertificate(context.TODO(), createCertificateRequest) + if err != nil { + return nil, err + } + + certificatesClient.SetCertCache(createCertificate, etag) + klog.Infof("Created a certificate with ocid %s", *createCertificate.Id) + return createCertificate, nil +} + +func UpdateImportedTypeCertificate(certificateId *string, tlsSecretData *TLSSecretData, + certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, error) { + _, etag, err := GetCertificate(certificateId, certificatesClient) + if err != nil { + return nil, err + } + + configDetails := certificatesmanagement.UpdateCertificateByImportingConfigDetails{ + CertChainPem: tlsSecretData.CaCertificateChain, + CertificatePem: tlsSecretData.ServerCertificate, + PrivateKeyPem: tlsSecretData.PrivateKey, + } + + updateCertificateDetails := certificatesmanagement.UpdateCertificateDetails{ + CertificateConfig: configDetails, + FreeformTags: map[string]string{ + certificateHashTagKey: hashPublicTlsData(tlsSecretData), + }, + } + + updateCertificateRequest := certificatesmanagement.UpdateCertificateRequest{ + CertificateId: certificateId, + IfMatch: common.String(etag), + UpdateCertificateDetails: updateCertificateDetails, + } + + updateCertificate, etag, err := certificatesClient.UpdateCertificate(context.TODO(), updateCertificateRequest) + + // This can happen by create/update listener calls that cause the certificate etag to change because of a new association + if util.IsServiceError(err, 409) { + klog.Infof("Update certificate returned code %d for certificate %s. Refreshing cache.", 409, *certificateId) + getCertificateBustCache(*certificateId, certificatesClient) + return nil, fmt.Errorf("unable to update certificate %s due to conflict, controller will retry later", *certificateId) + } + + if err != nil { + return nil, err + } + + certificatesClient.SetCertCache(updateCertificate, etag) + return updateCertificate, nil +} + +func ScheduleCertificateVersionDeletion(certificateId string, versionNumber int64, certificatesClient *certificate.CertificatesClient) error { + request := certificatesmanagement.ScheduleCertificateVersionDeletionRequest{ + ScheduleCertificateVersionDeletionDetails: certificatesmanagement.ScheduleCertificateVersionDeletionDetails{ + TimeOfDeletion: &common.SDKTime{ + Time: time.Now().Add(time.Hour * 24 * 10), + }, + }, + CertificateId: &certificateId, + CertificateVersionNumber: &versionNumber, + } + + updatedCert, etag, err := certificatesClient.ScheduleCertificateVersionDeletion(context.TODO(), request) + if err != nil { + return err + } + + certificatesClient.SetCertCache(updatedCert, etag) + return nil +} + +func PruneCertificateVersions(certificateId string, currentVersion int64, + versionsToPreserveCount int, certificatesClient *certificate.CertificatesClient) error { + listCertificateVersionsRequest := certificatesmanagement.ListCertificateVersionsRequest{ + CertificateId: &certificateId, + SortOrder: certificatesmanagement.ListCertificateVersionsSortOrderDesc, + } + + certificateVersionCollection, _, err := certificatesClient.ListCertificateVersions(context.TODO(), listCertificateVersionsRequest) + if err != nil { + return err + } + + versionsToPreserveSeen := 0 + for _, certVersionSummary := range certificateVersionCollection.Items { + if *certVersionSummary.VersionNumber > currentVersion { + continue + } + + if (isCertificateVersionFailed(certVersionSummary.Stages) || versionsToPreserveSeen >= versionsToPreserveCount) && + certVersionSummary.TimeOfDeletion == nil && !isCertificateVersionCurrent(certVersionSummary.Stages) { + err = ScheduleCertificateVersionDeletion(certificateId, *certVersionSummary.VersionNumber, certificatesClient) + if err != nil { + klog.Errorf("unable to delete certificate version number %d for certificate %s, skipping for now: %s", + *certVersionSummary.VersionNumber, certificateId, err.Error()) + } + continue + } + versionsToPreserveSeen = versionsToPreserveSeen + 1 + } + + return nil +} + +func isCertificateVersionFailed(certificateVersionStages []certificatesmanagement.VersionStageEnum) bool { + return certificateVersionStagesContainsStage(certificateVersionStages, certificatesmanagement.VersionStageFailed) +} + +func isCertificateVersionCurrent(certificateVersionStages []certificatesmanagement.VersionStageEnum) bool { + return certificateVersionStagesContainsStage(certificateVersionStages, certificatesmanagement.VersionStageCurrent) +} + +func certificateVersionStagesContainsStage(certificateVersionStages []certificatesmanagement.VersionStageEnum, + stage certificatesmanagement.VersionStageEnum) bool { + for _, value := range certificateVersionStages { + if stage == value { + return true + } + } + return false +} + +func GetCaBundle(caBundleId string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.CaBundle, string, error) { + caBundleCacheObj := certificatesClient.GetFromCaBundleCache(caBundleId) + if caBundleCacheObj != nil { + now := time.Now() + if now.Sub(caBundleCacheObj.Age).Minutes() < util.CertificateCacheMaxAgeInMinutes { + return caBundleCacheObj.CaBundle, caBundleCacheObj.Etag, nil + } + klog.Infof("Refreshing ca bundle %s", caBundleId) + } + + return getCaBundleBustCache(caBundleId, certificatesClient) +} + +func getCaBundleBustCache(caBundleId string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.CaBundle, string, error) { + klog.Infof("Getting ca bundle for id %s.", caBundleId) + getCaBundleRequest := certificatesmanagement.GetCaBundleRequest{ + CaBundleId: &caBundleId, + } + + caBundle, etag, err := certificatesClient.GetCaBundle(context.TODO(), getCaBundleRequest) + + if err == nil { + certificatesClient.SetCaBundleCache(caBundle, etag) + } + return caBundle, etag, err +} + +// VerifyOrGetCaBundleIdByName verifies that caBundle with caBundleId has name caBundleName +// If not, try to find ca bundle with matching name +func VerifyOrGetCaBundleIdByName(caBundleId string, caBundleName string, compartmentId string, + certificatesClient *certificate.CertificatesClient) (*string, error) { + if caBundleId != "" { + caBundle, _, err := GetCaBundle(caBundleId, certificatesClient) + if err != nil { + return nil, err + } + + if *caBundle.Name == caBundleName { + return &caBundleId, nil + } + } + + return FindCaBundleWithName(caBundleName, compartmentId, certificatesClient) +} + +func FindCaBundleWithName(certificateName string, compartmentId string, + certificatesClient *certificate.CertificatesClient) (*string, error) { + listCaBundlesRequest := certificatesmanagement.ListCaBundlesRequest{ + Name: &certificateName, + CompartmentId: &compartmentId, + LifecycleState: certificatesmanagement.ListCaBundlesLifecycleStateActive, + } + + klog.Infof("Searching for ca bundles with name %s in compartment %s.", certificateName, compartmentId) + listCaBundles, err := certificatesClient.ListCaBundles(context.TODO(), listCaBundlesRequest) + if err != nil { + return nil, err + } + + if listCaBundles.Items != nil { + numberOfCertificates := len(listCaBundles.Items) + klog.Infof("Found %d bundles with name %s in compartment %s.", numberOfCertificates, certificateName, compartmentId) + if numberOfCertificates > 0 { + return listCaBundles.Items[0].Id, nil + } + } + klog.Infof("Found no bundles with name %s in compartment %s.", certificateName, compartmentId) + return nil, nil +} + +func CreateCaBundle(certificateName string, compartmentId string, certificatesClient *certificate.CertificatesClient, + certificateContents *string) (*certificatesmanagement.CaBundle, error) { + caBundleDetails := certificatesmanagement.CreateCaBundleDetails{ + Name: &certificateName, + CompartmentId: &compartmentId, + CaBundlePem: certificateContents, + FreeformTags: map[string]string{ + caBundleHashTagKey: hashString(certificateContents), + }, + } + createCaBundleRequest := certificatesmanagement.CreateCaBundleRequest{ + CreateCaBundleDetails: caBundleDetails, + OpcRetryToken: &certificateName, + } + createCaBundle, etag, err := certificatesClient.CreateCaBundle(context.TODO(), createCaBundleRequest) + if err != nil { + return nil, err + } + + certificatesClient.SetCaBundleCache(createCaBundle, etag) + return createCaBundle, nil +} + +func UpdateCaBundle(caBundleId string, certificatesClient *certificate.CertificatesClient, + certificateContents *string) (*certificatesmanagement.CaBundle, error) { + _, etag, err := GetCaBundle(caBundleId, certificatesClient) + if err != nil { + return nil, err + } + + caBundleDetails := certificatesmanagement.UpdateCaBundleDetails{ + CaBundlePem: certificateContents, + FreeformTags: map[string]string{ + caBundleHashTagKey: hashString(certificateContents), + }, + } + + updateCaBundleRequest := certificatesmanagement.UpdateCaBundleRequest{ + CaBundleId: &caBundleId, + UpdateCaBundleDetails: caBundleDetails, + IfMatch: &etag, + } + + updateCaBundle, etag, err := certificatesClient.UpdateCaBundle(context.TODO(), updateCaBundleRequest) + + // This can happen by create/update backend set calls that cause the certificate etag to change because of a new association + if util.IsServiceError(err, 409) { + klog.Infof("Update ca bundle returned code %d for ca bundle %s. Refreshing cache.", 409, caBundleId) + getCaBundleBustCache(caBundleId, certificatesClient) + return nil, fmt.Errorf("unable to update ca bundle %s due to conflict, controller will retry later", caBundleId) + } + + if err != nil { + return nil, err + } + + certificatesClient.SetCaBundleCache(updateCaBundle, etag) + return updateCaBundle, nil +} diff --git a/pkg/controllers/ingress/certificate_util_test.go b/pkg/controllers/ingress/certificate_util_test.go new file mode 100644 index 00000000..74c0aaca --- /dev/null +++ b/pkg/controllers/ingress/certificate_util_test.go @@ -0,0 +1,318 @@ +/* + * + * * OCI Native Ingress Controller + * * + * * Copyright (c) 2024 Oracle America, Inc. and its affiliates. + * * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + * + */ + +package ingress + +import ( + . "github.com/onsi/gomega" + "github.com/oracle/oci-go-sdk/v65/certificatesmanagement" + "github.com/oracle/oci-go-sdk/v65/common" + corev1 "k8s.io/api/core/v1" + "testing" +) + +func TestGetCertificate(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + certId := "id" + certId2 := "id2" + + certificate, etag, err := GetCertificate(&certId, mockClient.GetCertClient()) + Expect(certificate != nil).Should(BeTrue()) + Expect(etag).Should(Equal("etag")) + Expect(err).Should(BeNil()) + + // cache fetch + certificate, etag, err = GetCertificate(&certId, mockClient.GetCertClient()) + Expect(certificate != nil).Should(BeTrue()) + Expect(etag).Should(Equal("etag")) + Expect(err).Should(BeNil()) + + certificate, etag, err = GetCertificate(&certId2, mockClient.GetCertClient()) + Expect(certificate != nil).Should(BeTrue()) + Expect(etag).Should(Equal("etag")) + Expect(err).Should(BeNil()) +} + +func TestVerifyOrGetCertificateIdByName(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + id := "id" + name := "cert" + compartmentId := "compartmentId" + actualCertificateId, err := VerifyOrGetCertificateIdByName(id, name, compartmentId, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(actualCertificateId).Should(Equal(id)) + + actualCertificateId, err = VerifyOrGetCertificateIdByName("", name, compartmentId, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(actualCertificateId).ShouldNot(Equal("")) +} + +func TestFindCertificateWithName(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + name := "name" + compartmentId := "compartmentId" + certificateId, err := FindCertificateWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(certificateId).ShouldNot(Equal("")) + Expect(err).Should(BeNil()) + + name = "nonexistent" + certificateId, err = FindCertificateWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(certificateId).Should(Equal("")) + Expect(err).Should(BeNil()) + + name = "error" + certificateId, err = FindCertificateWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(certificateId).Should(Equal("")) + Expect(err).ShouldNot(BeNil()) +} + +func TestCreateImportedTypeCertificate(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + tlsSecretData := &TLSSecretData{ + ServerCertificate: common.String("serverCert"), + CaCertificateChain: common.String("certChain"), + PrivateKey: common.String("privateKey"), + } + certificateName := "name" + compartmentId := "compartmentId" + cert, err := CreateImportedTypeCertificate(tlsSecretData, certificateName, compartmentId, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) + + certificateName = "error" + cert, err = CreateImportedTypeCertificate(tlsSecretData, certificateName, compartmentId, mockClient.GetCertClient()) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} + +func TestUpdateImportedTypeCertificate(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + tlsSecretData := &TLSSecretData{ + ServerCertificate: common.String("serverCert"), + CaCertificateChain: common.String("certChain"), + PrivateKey: common.String("privateKey"), + } + certificateId := common.String("id") + cert, err := UpdateImportedTypeCertificate(certificateId, tlsSecretData, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) + + *certificateId = "conflictError" + cert, err = UpdateImportedTypeCertificate(certificateId, tlsSecretData, mockClient.GetCertClient()) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) + + *certificateId = "error" + cert, err = UpdateImportedTypeCertificate(certificateId, tlsSecretData, mockClient.GetCertClient()) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} + +func TestScheduleCertificateVersionDeletion(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + certificateId := "id" + versionNumber := int64(2) + err = ScheduleCertificateVersionDeletion(certificateId, versionNumber, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + + certificateId = "error" + err = ScheduleCertificateVersionDeletion(certificateId, versionNumber, mockClient.GetCertClient()) + Expect(err).ShouldNot(BeNil()) +} + +func TestPruneCertificateVersions(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + certificateId := "id" + currentVersion := int64(8) + err = PruneCertificateVersions(certificateId, currentVersion, 5, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + + certificateId = "error" + err = PruneCertificateVersions(certificateId, currentVersion, 5, mockClient.GetCertClient()) + Expect(err).ShouldNot(BeNil()) +} + +func TestIsCertificateVersionFailed(t *testing.T) { + RegisterTestingT(t) + + certVersionStages := []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageLatest, + certificatesmanagement.VersionStageCurrent, + } + Expect(isCertificateVersionFailed(certVersionStages)).Should(BeFalse()) + + certVersionStages = []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageLatest, + certificatesmanagement.VersionStageFailed, + } + Expect(isCertificateVersionFailed(certVersionStages)).Should(BeTrue()) +} + +func TestIsCertificateVersionCurrent(t *testing.T) { + RegisterTestingT(t) + + certVersionStages := []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageLatest, + certificatesmanagement.VersionStageCurrent, + } + Expect(isCertificateVersionCurrent(certVersionStages)).Should(BeTrue()) + + certVersionStages = []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageLatest, + certificatesmanagement.VersionStageFailed, + } + Expect(isCertificateVersionCurrent(certVersionStages)).Should(BeFalse()) +} + +func TestCertificateVersionStagesContainsStage(t *testing.T) { + RegisterTestingT(t) + + stages := []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageLatest, certificatesmanagement.VersionStageCurrent} + Expect(certificateVersionStagesContainsStage(stages, certificatesmanagement.VersionStageLatest)).Should(BeTrue()) + Expect(certificateVersionStagesContainsStage(stages, certificatesmanagement.VersionStageCurrent)).Should(BeTrue()) + Expect(certificateVersionStagesContainsStage(stages, certificatesmanagement.VersionStageFailed)).Should(BeFalse()) + Expect(certificateVersionStagesContainsStage(stages, certificatesmanagement.VersionStagePrevious)).Should(BeFalse()) +} + +func TestGetCaBundle(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + caBundleId := "id" + caBundleErrorId := "error" + + caBundle, etag, err := GetCaBundle(caBundleId, mockClient.GetCertClient()) + Expect(caBundle != nil).Should(BeTrue()) + Expect(etag).Should(Equal("etag")) + Expect(err).Should(BeNil()) + + // cache fetch + caBundle, etag, err = GetCaBundle(caBundleId, mockClient.GetCertClient()) + Expect(caBundle != nil).Should(BeTrue()) + Expect(etag).Should(Equal("etag")) + Expect(err).Should(BeNil()) + + caBundle, etag, err = GetCaBundle(caBundleErrorId, mockClient.GetCertClient()) + Expect(caBundle).Should(BeNil()) + Expect(etag).Should(Equal("")) + Expect(err).ShouldNot(BeNil()) +} + +func TestVerifyOrGetCaBundleIdByName(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + id := "id" + name := "caBundle" + compartmentId := "compartmentId" + actualCaBundleId, err := VerifyOrGetCaBundleIdByName(id, name, compartmentId, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(*actualCaBundleId).Should(Equal(id)) + + actualCaBundleId, err = VerifyOrGetCaBundleIdByName("", name, compartmentId, mockClient.GetCertClient()) + Expect(err).Should(BeNil()) + Expect(*actualCaBundleId).ShouldNot(Equal("")) +} + +func TestFindCaBundleWithName(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + name := "name" + compartmentId := "compartmentId" + caBundleId, err := FindCaBundleWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(*caBundleId).ShouldNot(BeNil()) + Expect(err).Should(BeNil()) + + name = "nonexistent" + caBundleId, err = FindCaBundleWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(caBundleId).Should(BeNil()) + Expect(err).Should(BeNil()) + + name = "error" + caBundleId, err = FindCaBundleWithName(name, compartmentId, mockClient.GetCertClient()) + Expect(caBundleId).Should(BeNil()) + Expect(err).ShouldNot(BeNil()) +} + +func TestCreateCaBundle(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + certificatesContent := common.String("certChain") + caBundleName := "name" + compartmentId := "compartmentId" + cert, err := CreateCaBundle(caBundleName, compartmentId, mockClient.GetCertClient(), certificatesContent) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) + + caBundleName = "error" + cert, err = CreateCaBundle(caBundleName, compartmentId, mockClient.GetCertClient(), certificatesContent) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} + +func TestUpdateCaBundle(t *testing.T) { + RegisterTestingT(t) + c, _, _ := initsUtil(&corev1.SecretList{}) + mockClient, err := c.GetClient(&MockConfigGetter{}) + Expect(err).Should(BeNil()) + + certificatesContent := common.String("certChain") + caBundleId := "id" + cert, err := UpdateCaBundle(caBundleId, mockClient.GetCertClient(), certificatesContent) + Expect(err).Should(BeNil()) + Expect(cert).ShouldNot(BeNil()) + + caBundleId = "conflictError" + cert, err = UpdateCaBundle(caBundleId, mockClient.GetCertClient(), certificatesContent) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) + + caBundleId = "error" + cert, err = UpdateCaBundle(caBundleId, mockClient.GetCertClient(), certificatesContent) + Expect(err).ShouldNot(BeNil()) + Expect(cert).Should(BeNil()) +} diff --git a/pkg/controllers/ingress/ingress.go b/pkg/controllers/ingress/ingress.go index 476bf0ba..92711f9e 100644 --- a/pkg/controllers/ingress/ingress.go +++ b/pkg/controllers/ingress/ingress.go @@ -13,6 +13,8 @@ import ( "context" "encoding/json" "fmt" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" coreinformers "k8s.io/client-go/informers/core/v1" "reflect" "time" @@ -56,6 +58,7 @@ type Controller struct { ingressLister networkinglisters.IngressLister serviceLister corelisters.ServiceLister saLister corelisters.ServiceAccountLister + secretLister corelisters.SecretLister queue workqueue.RateLimitingInterface informer networkinginformers.IngressInformer client *client.ClientProvider @@ -65,7 +68,7 @@ type Controller struct { // NewController creates a new Controller. func NewController(controllerClass string, defaultCompartmentId string, ingressClassInformer networkinginformers.IngressClassInformer, ingressInformer networkinginformers.IngressInformer, - saInformer coreinformers.ServiceAccountInformer, serviceLister corelisters.ServiceLister, + saInformer coreinformers.ServiceAccountInformer, serviceLister corelisters.ServiceLister, secretInformer coreinformers.SecretInformer, client *client.ClientProvider, reg *prometheus.Registry) *Controller { @@ -77,6 +80,7 @@ func NewController(controllerClass string, defaultCompartmentId string, informer: ingressInformer, serviceLister: serviceLister, saLister: saInformer.Lister(), + secretLister: secretInformer.Lister(), client: client, queue: workqueue.NewRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(10*time.Second, 5*time.Minute)), metricsCollector: metric.NewIngressCollector(controllerClass, reg), @@ -90,6 +94,13 @@ func NewController(controllerClass string, defaultCompartmentId string, }, ) + secretInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerDetailedFuncs{ + AddFunc: c.secretAdd, + UpdateFunc: c.secretUpdate, + }, + ) + return c } @@ -144,6 +155,37 @@ func (c *Controller) ingressDelete(obj interface{}) { c.enqueueIngress(ic) } +func (c *Controller) secretAdd(obj interface{}, isInInitialList bool) { + if isInInitialList { + return + } + c.secretAddOrUpdate(obj) +} + +func (c *Controller) secretUpdate(old, new interface{}) { + c.secretAddOrUpdate(new) +} + +func (c *Controller) secretAddOrUpdate(obj interface{}) { + secret := obj.(*corev1.Secret) + + ingresses, err := c.ingressLister.Ingresses(secret.Namespace).List(labels.Everything()) + if err != nil { + klog.Errorf("error listing Ingresses for update of secret %s/%s: %s", secret.Namespace, secret.Name, err) + return + } + + for _, ingress := range ingresses { + for _, tls := range ingress.Spec.TLS { + if tls.SecretName == secret.Name { + klog.V(4).Infof("updating ingress %s because of secret %s", + klog.KObj(ingress), klog.KObj(secret)) + c.enqueueIngress(ingress) + } + } + } +} + func (c *Controller) processNextItem() bool { // Wait until there is a new item in the working queue key, quit := c.queue.Get() @@ -354,7 +396,7 @@ func (c *Controller) ensureIngress(ctx context.Context, ingress *networkingv1.In startBuildTime := util.GetCurrentTimeInUnixMillis() klog.V(2).InfoS("creating backend set for ingress", "ingress", klog.KObj(ingress), "backendSetName", bsName) artifact, artifactType := stateStore.GetTLSConfigForBackendSet(bsName) - backendSetSslConfig, err := GetSSLConfigForBackendSet(ingress.Namespace, artifactType, artifact, lb, bsName, c.defaultCompartmentId, wrapperClient) + backendSetSslConfig, err := GetSSLConfigForBackendSet(ingress.Namespace, artifactType, artifact, lb, bsName, c.defaultCompartmentId, c.secretLister, wrapperClient) if err != nil { return err } @@ -389,7 +431,7 @@ func (c *Controller) ensureIngress(ctx context.Context, ingress *networkingv1.In var listenerSslConfig *ociloadbalancer.SslConfigurationDetails artifact, artifactType := stateStore.GetTLSConfigForListener(port) - listenerSslConfig, err := GetSSLConfigForListener(ingress.Namespace, nil, artifactType, artifact, c.defaultCompartmentId, wrapperClient) + listenerSslConfig, err := GetSSLConfigForListener(ingress.Namespace, nil, artifactType, artifact, c.defaultCompartmentId, c.secretLister, wrapperClient) if err != nil { return err } @@ -516,7 +558,7 @@ func syncListener(ctx context.Context, namespace string, stateStore *state.State artifact, artifactType := stateStore.GetTLSConfigForListener(int32(*listener.Port)) var sslConfig *ociloadbalancer.SslConfigurationDetails if artifact != "" { - sslConfig, err = GetSSLConfigForListener(namespace, &listener, artifactType, artifact, c.defaultCompartmentId, wrapperClient) + sslConfig, err = GetSSLConfigForListener(namespace, &listener, artifactType, artifact, c.defaultCompartmentId, c.secretLister, wrapperClient) if err != nil { return err } @@ -575,7 +617,7 @@ func syncBackendSet(ctx context.Context, ingress *networkingv1.Ingress, lbID str needsUpdate := false artifact, artifactType := stateStore.GetTLSConfigForBackendSet(*bs.Name) - sslConfig, err := GetSSLConfigForBackendSet(ingress.Namespace, artifactType, artifact, lb, *bs.Name, c.defaultCompartmentId, wrapperClient) + sslConfig, err := GetSSLConfigForBackendSet(ingress.Namespace, artifactType, artifact, lb, *bs.Name, c.defaultCompartmentId, c.secretLister, wrapperClient) if err != nil { return err } diff --git a/pkg/controllers/ingress/ingressPathWithTlsSecret.yaml b/pkg/controllers/ingress/ingressPathWithTlsSecret.yaml new file mode 100644 index 00000000..09547f36 --- /dev/null +++ b/pkg/controllers/ingress/ingressPathWithTlsSecret.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-readiness + namespace: default +spec: + tls: + - hosts: + - foo.bar.com + secretName: tls-secret + rules: + - host: "foo.bar.com" + http: + paths: + - pathType: Exact + path: "/testecho1" + backend: + service: + name: testecho1 + port: + number: 80 \ No newline at end of file diff --git a/pkg/controllers/ingress/ingress_test.go b/pkg/controllers/ingress/ingress_test.go index 8e9f8a6a..511c5cbf 100644 --- a/pkg/controllers/ingress/ingress_test.go +++ b/pkg/controllers/ingress/ingress_test.go @@ -2,6 +2,7 @@ package ingress import ( "context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" coreinformers "k8s.io/client-go/informers/core/v1" "sync" "testing" @@ -27,9 +28,10 @@ import ( const ( ingressPath = "ingressPath.yaml" ingressPathWithFinalizer = "ingressPathWithFinalizer.yaml" + ingressPathWithTlsSecret = "ingressPathWithTlsSecret.yaml" ) -func setUp(ctx context.Context, ingressClassList *networkingv1.IngressClassList, ingressList *networkingv1.IngressList, testService *v1.ServiceList) (networkinginformers.IngressClassInformer, networkinginformers.IngressInformer, coreinformers.ServiceAccountInformer, corelisters.ServiceLister, *fakeclientset.Clientset) { +func setUp(ctx context.Context, ingressClassList *networkingv1.IngressClassList, ingressList *networkingv1.IngressList, testService *v1.ServiceList) (networkinginformers.IngressClassInformer, networkinginformers.IngressInformer, coreinformers.ServiceAccountInformer, corelisters.ServiceLister, coreinformers.SecretInformer, *fakeclientset.Clientset) { fakeClient := fakeclientset.NewSimpleClientset() action := "list" @@ -50,12 +52,16 @@ func setUp(ctx context.Context, ingressClassList *networkingv1.IngressClassList, serviceInformer := informerFactory.Core().V1().Services() serviceLister := serviceInformer.Lister() + secretInformer := informerFactory.Core().V1().Secrets() + secretInformer.Lister() + saInformer := informerFactory.Core().V1().ServiceAccounts() informerFactory.Start(ctx.Done()) cache.WaitForCacheSync(ctx.Done(), ingressClassInformer.Informer().HasSynced) cache.WaitForCacheSync(ctx.Done(), ingressInformer.Informer().HasSynced) - return ingressClassInformer, ingressInformer, saInformer, serviceLister, fakeClient + cache.WaitForCacheSync(ctx.Done(), secretInformer.Informer().HasSynced) + return ingressClassInformer, ingressInformer, saInformer, serviceLister, secretInformer, fakeClient } func inits(ctx context.Context, ingressClassList *networkingv1.IngressClassList, ingressList *networkingv1.IngressList) *Controller { @@ -78,7 +84,7 @@ func inits(ctx context.Context, ingressClassList *networkingv1.IngressClassList, CaBundleCache: map[string]*ociclient.CaBundleCacheObj{}, } - ingressClassInformer, ingressInformer, saInformer, serviceLister, k8client := setUp(ctx, ingressClassList, ingressList, testService) + ingressClassInformer, ingressInformer, saInformer, serviceLister, secretInformer, k8client := setUp(ctx, ingressClassList, ingressList, testService) wrapperClient := client.NewWrapperClient(k8client, nil, loadBalancerClient, certificatesClient, nil) fakeClient := &client.ClientProvider{ K8sClient: k8client, @@ -86,7 +92,7 @@ func inits(ctx context.Context, ingressClassList *networkingv1.IngressClassList, Cache: NewMockCacheStore(wrapperClient), } c := NewController("oci.oraclecloud.com/native-ingress-controller", "", ingressClassInformer, - ingressInformer, saInformer, serviceLister, fakeClient, nil) + ingressInformer, saInformer, serviceLister, secretInformer, fakeClient, nil) return c } @@ -192,6 +198,42 @@ func TestIngressDelete(t *testing.T) { Expect(c.queue.Len()).Should(Equal(queueSize + 1)) } +func TestSecretAdd(t *testing.T) { + RegisterTestingT(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ingressClassList := util.GetIngressClassList() + ingressList := util.ReadResourceAsIngressList(ingressPathWithTlsSecret) + c := inits(ctx, ingressClassList, ingressList) + queueSize := c.queue.Len() + c.secretAdd(&v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls-secret"}}, false) + Expect(c.queue.Len()).Should(Equal(queueSize + 1)) +} + +func TestSecretAdd_IsInInitialList(t *testing.T) { + RegisterTestingT(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ingressClassList := util.GetIngressClassList() + ingressList := util.ReadResourceAsIngressList(ingressPathWithTlsSecret) + c := inits(ctx, ingressClassList, ingressList) + queueSize := c.queue.Len() + c.secretAdd(&v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls-secret"}}, true) + Expect(c.queue.Len()).Should(Equal(queueSize)) +} + +func TestSecretUpdate(t *testing.T) { + RegisterTestingT(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ingressClassList := util.GetIngressClassList() + ingressList := util.ReadResourceAsIngressList(ingressPathWithTlsSecret) + c := inits(ctx, ingressClassList, ingressList) + queueSize := c.queue.Len() + c.secretUpdate(nil, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls-secret"}}) + Expect(c.queue.Len()).Should(Equal(queueSize + 1)) +} + func TestProcessNextItem(t *testing.T) { RegisterTestingT(t) ctx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/controllers/ingress/util.go b/pkg/controllers/ingress/util.go index 309b6cb5..8383c11b 100644 --- a/pkg/controllers/ingress/util.go +++ b/pkg/controllers/ingress/util.go @@ -11,24 +11,28 @@ package ingress import ( "context" + "crypto/sha256" "crypto/tls" + "encoding/hex" "encoding/pem" "errors" "fmt" - "reflect" - "strings" - "time" - "github.com/oracle/oci-go-sdk/v65/certificates" "github.com/oracle/oci-go-sdk/v65/certificatesmanagement" ociloadbalancer "github.com/oracle/oci-go-sdk/v65/loadbalancer" - "github.com/oracle/oci-native-ingress-controller/pkg/certificate" "github.com/oracle/oci-native-ingress-controller/pkg/client" "github.com/oracle/oci-native-ingress-controller/pkg/state" "github.com/oracle/oci-native-ingress-controller/pkg/util" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" + v1 "k8s.io/client-go/listers/core/v1" "k8s.io/klog/v2" + "reflect" + "strings" +) + +const ( + certificateHashTagKey = "oci-native-ingress-controller-certificate-hash" + caBundleHashTagKey = "oci-native-ingress-controller-ca-bundle-hash" + certificateVersionsToPreserveCount = 5 ) func compareHealthCheckers(healthCheckerDetails *ociloadbalancer.HealthCheckerDetails, healthChecker *ociloadbalancer.HealthChecker) bool { @@ -57,154 +61,36 @@ func compareHttpHealthCheckerAttributes(healthCheckerDetails *ociloadbalancer.He reflect.DeepEqual(healthCheckerDetails.IsForcePlainText, healthChecker.IsForcePlainText) } -// SSL UTILS - -func CreateImportedTypeCertificate(caCertificatesChain *string, serverCertificate *string, privateKey *string, certificateName string, compartmentId string, - certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, error) { - configDetails := certificatesmanagement.CreateCertificateByImportingConfigDetails{ - CertChainPem: caCertificatesChain, - CertificatePem: serverCertificate, - PrivateKeyPem: privateKey, - } +// SSL UTIL - certificateDetails := certificatesmanagement.CreateCertificateDetails{ - Name: &certificateName, - CertificateConfig: configDetails, - CompartmentId: &compartmentId, - } - createCertificateRequest := certificatesmanagement.CreateCertificateRequest{ - CreateCertificateDetails: certificateDetails, - OpcRetryToken: &certificateName, - } - - createCertificate, err := certificatesClient.CreateCertificate(context.TODO(), createCertificateRequest) - if err != nil { - return nil, err - } - - certificatesClient.SetCertCache(createCertificate) - klog.Infof("Created a certificate with ocid %s", *createCertificate.Id) - return createCertificate, nil -} - -func GetCertificate(certificateId *string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.Certificate, error) { - certCacheObj := certificatesClient.GetFromCertCache(*certificateId) - if certCacheObj != nil { - now := time.Now() - if now.Sub(certCacheObj.Age).Minutes() < util.CertificateCacheMaxAgeInMinutes { - return certCacheObj.Cert, nil - } - klog.Infof("Refreshing certificate %s", *certificateId) - } - getCertificateRequest := certificatesmanagement.GetCertificateRequest{ - CertificateId: certificateId, - } - - cert, err := certificatesClient.GetCertificate(context.TODO(), getCertificateRequest) - if err == nil { - certificatesClient.SetCertCache(cert) - } - return cert, err -} - -func FindCertificateWithName(certificateName string, compartmentId string, - certificatesClient *certificate.CertificatesClient) (*string, error) { - listCertificatesRequest := certificatesmanagement.ListCertificatesRequest{ - Name: &certificateName, - CompartmentId: &compartmentId, - LifecycleState: certificatesmanagement.ListCertificatesLifecycleStateActive, - } - - klog.Infof("Searching for certificates with name %s in compartment %s.", certificateName, compartmentId) - listCertificates, _, err := certificatesClient.ListCertificates(context.TODO(), listCertificatesRequest) - if err != nil { - return nil, err - } - - if listCertificates.Items != nil { - numberOfCertificates := len(listCertificates.Items) - klog.Infof("Found %d certificates with name %s in compartment %s.", numberOfCertificates, certificateName, compartmentId) - if numberOfCertificates > 0 { - return listCertificates.Items[0].Id, nil - } - } - klog.Infof("Found no certificates with name %s in compartment %s.", certificateName, compartmentId) - return nil, nil -} - -func FindCaBundleWithName(certificateName string, compartmentId string, - certificatesClient *certificate.CertificatesClient) (*string, error) { - listCaBundlesRequest := certificatesmanagement.ListCaBundlesRequest{ - Name: &certificateName, - CompartmentId: &compartmentId, - LifecycleState: certificatesmanagement.ListCaBundlesLifecycleStateActive, - } - - klog.Infof("Searching for ca bundles with name %s in compartment %s.", certificateName, compartmentId) - listCaBundles, err := certificatesClient.ListCaBundles(context.TODO(), listCaBundlesRequest) - if err != nil { - return nil, err - } - - if listCaBundles.Items != nil { - numberOfCertificates := len(listCaBundles.Items) - klog.Infof("Found %d bundles with name %s in compartment %s.", numberOfCertificates, certificateName, compartmentId) - if numberOfCertificates > 0 { - return listCaBundles.Items[0].Id, nil - } - } - klog.Infof("Found no bundles with name %s in compartment %s.", certificateName, compartmentId) - return nil, nil +type TLSSecretData struct { + // This would hold server certificate and any chain of trust. + CaCertificateChain *string + ServerCertificate *string + PrivateKey *string } -func GetCaBundle(caBundleId string, certificatesClient *certificate.CertificatesClient) (*certificatesmanagement.CaBundle, error) { - caBundleCacheObj := certificatesClient.GetFromCaBundleCache(caBundleId) - if caBundleCacheObj != nil { - return caBundleCacheObj.CaBundle, nil - } - - klog.Infof("Getting ca bundle for id %s.", caBundleId) - getCaBundleRequest := certificatesmanagement.GetCaBundleRequest{ - CaBundleId: &caBundleId, +func hashPublicTlsData(data *TLSSecretData) string { + concatString := "" + if data != nil && data.CaCertificateChain != nil { + concatString = concatString + *data.CaCertificateChain } - - caBundle, err := certificatesClient.GetCaBundle(context.TODO(), getCaBundleRequest) - - if err == nil { - certificatesClient.SetCaBundleCache(caBundle) + if data != nil && data.ServerCertificate != nil { + concatString = concatString + *data.ServerCertificate } - return caBundle, err + return hashString(&concatString) } -func CreateCaBundle(certificateName string, compartmentId string, certificatesClient *certificate.CertificatesClient, - certificateContents *string) (*certificatesmanagement.CaBundle, error) { - caBundleDetails := certificatesmanagement.CreateCaBundleDetails{ - Name: &certificateName, - CompartmentId: &compartmentId, - CaBundlePem: certificateContents, - } - createCaBundleRequest := certificatesmanagement.CreateCaBundleRequest{ - CreateCaBundleDetails: caBundleDetails, - OpcRetryToken: &certificateName, +func hashString(data *string) string { + h := sha256.New() + if data != nil { + h.Write([]byte(*data)) } - createCaBundle, err := certificatesClient.CreateCaBundle(context.TODO(), createCaBundleRequest) - if err != nil { - return nil, err - } - - certificatesClient.SetCaBundleCache(createCaBundle) - return createCaBundle, nil + return hex.EncodeToString(h.Sum(nil)) } -type TLSSecretData struct { - // This would hold server certificate and any chain of trust. - CaCertificateChain *string - ServerCertificate *string - PrivateKey *string -} - -func getTlsSecretContent(namespace string, secretName string, client kubernetes.Interface) (*TLSSecretData, error) { - secret, err := client.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) +func getTlsSecretContent(namespace string, secretName string, secretLister v1.SecretLister) (*TLSSecretData, error) { + secret, err := secretLister.Secrets(namespace).Get(secretName) if err != nil { return nil, err } @@ -247,47 +133,40 @@ func splitLeafAndCaCertChain(certChainPEMBlock []byte, keyPEMBlock []byte) (stri return leafCertString, caCertChainString, nil } -func getCertificateNameFromSecret(secretName string) string { +func getCertificateNameFromSecret(namespace string, secretName string, secretLister v1.SecretLister) (string, error) { if secretName == "" { - return "" + return "", nil + } + + secret, err := secretLister.Secrets(namespace).Get(secretName) + if err != nil { + return "", fmt.Errorf("unable to GET secret %s: %w", klog.KRef(namespace, secretName), err) } - return fmt.Sprintf("ic-%s", secretName) + + return fmt.Sprintf("oci-nic-%s", secret.UID), nil } -func GetSSLConfigForBackendSet(namespace string, artifactType string, artifact string, lb *ociloadbalancer.LoadBalancer, bsName string, compartmentId string, client *client.WrapperClient) (*ociloadbalancer.SslConfigurationDetails, error) { +func GetSSLConfigForBackendSet(namespace string, artifactType string, artifact string, lb *ociloadbalancer.LoadBalancer, bsName string, + compartmentId string, secretLister v1.SecretLister, client *client.WrapperClient) (*ociloadbalancer.SslConfigurationDetails, error) { var backendSetSslConfig *ociloadbalancer.SslConfigurationDetails - createCaBundle := false var caBundleId *string bs, ok := lb.BackendSets[bsName] if artifactType == state.ArtifactTypeSecret && artifact != "" { klog.Infof("Secret name for backend set %s is %s", bsName, artifact) - if ok && bs.SslConfiguration != nil && isTrustAuthorityCaBundle(bs.SslConfiguration.TrustedCertificateAuthorityIds[0]) { - newCertificateName := getCertificateNameFromSecret(artifact) - caBundle, err := GetCaBundle(bs.SslConfiguration.TrustedCertificateAuthorityIds[0], client.GetCertClient()) - if err != nil { - return nil, err - } - klog.Infof("Ca bundle name is %s, new certificate name is %s", *caBundle.Name, newCertificateName) - if *caBundle.Name != newCertificateName { - klog.Infof("Ca bundle for backend set %s needs update. Old name %s, New name %s", *bs.Name, *caBundle.Name, newCertificateName) - createCaBundle = true - } else { - caBundleId = caBundle.Id - } - } else { - createCaBundle = true + currentCaBundleId := "" + if ok && bs.SslConfiguration != nil && len(bs.SslConfiguration.TrustedCertificateAuthorityIds) > 0 && + isTrustAuthorityCaBundle(bs.SslConfiguration.TrustedCertificateAuthorityIds[0]) { + currentCaBundleId = bs.SslConfiguration.TrustedCertificateAuthorityIds[0] } - if createCaBundle { - cId, err := CreateOrGetCaBundleForBackendSet(namespace, artifact, compartmentId, client) - if err != nil { - return nil, err - } - caBundleId = cId + newCaBundleId, err := ensureCaBundleForBackendSet(currentCaBundleId, namespace, artifact, compartmentId, secretLister, client) + if err != nil { + return nil, err } + caBundleId = newCaBundleId if caBundleId != nil { caBundleIds := []string{*caBundleId} @@ -296,7 +175,7 @@ func GetSSLConfigForBackendSet(namespace string, artifactType string, artifact s } if artifactType == state.ArtifactTypeCertificate && artifact != "" { - cert, err := GetCertificate(&artifact, client.GetCertClient()) + cert, _, err := GetCertificate(&artifact, client.GetCertClient()) if err != nil { return nil, err } @@ -338,42 +217,27 @@ func GetSSLConfigForBackendSet(namespace string, artifactType string, artifact s return backendSetSslConfig, nil } -func GetSSLConfigForListener(namespace string, listener *ociloadbalancer.Listener, artifactType string, artifact string, compartmentId string, client *client.WrapperClient) (*ociloadbalancer.SslConfigurationDetails, error) { +func GetSSLConfigForListener(namespace string, listener *ociloadbalancer.Listener, artifactType string, artifact string, + compartmentId string, secretLister v1.SecretLister, client *client.WrapperClient) (*ociloadbalancer.SslConfigurationDetails, error) { var currentCertificateId string var newCertificateId string - createCertificate := false var listenerSslConfig *ociloadbalancer.SslConfigurationDetails - if listener != nil && listener.SslConfiguration != nil { + if listener != nil && listener.SslConfiguration != nil && len(listener.SslConfiguration.CertificateIds) > 0 { currentCertificateId = listener.SslConfiguration.CertificateIds[0] - if state.ArtifactTypeCertificate == artifactType && currentCertificateId != artifact { - newCertificateId = artifact - } else if state.ArtifactTypeSecret == artifactType { - cert, err := GetCertificate(¤tCertificateId, client.GetCertClient()) - if err != nil { - return nil, err - } - certificateName := getCertificateNameFromSecret(artifact) - if certificateName != "" && *cert.Name != certificateName { - createCertificate = true - } - } - } else { - if state.ArtifactTypeSecret == artifactType { - createCertificate = true - } - if state.ArtifactTypeCertificate == artifactType { - newCertificateId = artifact - } } - if createCertificate { - cId, err := CreateOrGetCertificateForListener(namespace, artifact, compartmentId, client) + if state.ArtifactTypeCertificate == artifactType { + newCertificateId = artifact + } + + if state.ArtifactTypeSecret == artifactType && artifact != "" { + cId, err := ensureCertificateForListener(currentCertificateId, namespace, artifact, compartmentId, secretLister, client) if err != nil { return nil, err } - newCertificateId = *cId + newCertificateId = cId } if newCertificateId != "" { @@ -383,48 +247,104 @@ func GetSSLConfigForListener(namespace string, listener *ociloadbalancer.Listene return listenerSslConfig, nil } -func CreateOrGetCertificateForListener(namespace string, secretName string, compartmentId string, client *client.WrapperClient) (*string, error) { - certificateName := getCertificateNameFromSecret(secretName) - certificateId, err := FindCertificateWithName(certificateName, compartmentId, client.GetCertClient()) +// ensureCertificateForListener creates/updates a certificate for Listeners, when the artifact is of type secret +// inputCertificateId is optional, pass if trying to update a certificate, name of certificate will still be checked +func ensureCertificateForListener(inputCertificateId string, namespace string, secretName string, compartmentId string, secretLister v1.SecretLister, client *client.WrapperClient) (string, error) { + certificateName, err := getCertificateNameFromSecret(namespace, secretName, secretLister) if err != nil { - return nil, err + return "", err + } + + tlsSecretData, err := getTlsSecretContent(namespace, secretName, secretLister) + if err != nil { + return "", err + } + + certificateId, err := VerifyOrGetCertificateIdByName(inputCertificateId, certificateName, compartmentId, client.GetCertClient()) + if err != nil { + return "", nil } - if certificateId == nil { - tlsSecretData, err := getTlsSecretContent(namespace, secretName, client.GetK8Client()) + if certificateId == "" { + klog.Infof("Need to create certificate for secret %s", klog.KRef(namespace, secretName)) + createCertificate, err := CreateImportedTypeCertificate(tlsSecretData, certificateName, compartmentId, client.GetCertClient()) if err != nil { - return nil, err + return "", err } - createCertificate, err := CreateImportedTypeCertificate(tlsSecretData.CaCertificateChain, tlsSecretData.ServerCertificate, - tlsSecretData.PrivateKey, certificateName, compartmentId, client.GetCertClient()) + certificateId = *createCertificate.Id + } else { + cert, _, err := GetCertificate(&certificateId, client.GetCertClient()) if err != nil { - return nil, err + return "", err } - certificateId = createCertificate.Id + if cert.FreeformTags == nil || hashPublicTlsData(tlsSecretData) != cert.FreeformTags[certificateHashTagKey] { + klog.Infof("Need to update certificate %s for secret %s", certificateId, klog.KRef(namespace, secretName)) + cert, err = UpdateImportedTypeCertificate(&certificateId, tlsSecretData, client.GetCertClient()) + if err != nil { + return "", err + } + + klog.Infof("Pruning Certificate %s for stale versions", certificateId) + err = PruneCertificateVersions(certificateId, *cert.CurrentVersion.VersionNumber, certificateVersionsToPreserveCount, client.GetCertClient()) + if err != nil { + klog.Errorf("Unable to prune certificate %s for stale versions: %s", certificateId, err.Error()) + } + } + + if !isCertificateCurrentVersionLatest(cert) { + klog.Warningf("For certificate %s, current version detected is not the latest one. "+ + "Please update secret %s to have the latest desired details.", certificateId, klog.KRef(namespace, secretName)) + if cert.LifecycleDetails != nil { + klog.Warningf("Lifecycle details for certificate %s: %s", certificateId, *cert.LifecycleDetails) + } + } } + return certificateId, nil } -func CreateOrGetCaBundleForBackendSet(namespace string, secretName string, compartmentId string, client *client.WrapperClient) (*string, error) { - certificateName := getCertificateNameFromSecret(secretName) - caBundleId, err := FindCaBundleWithName(certificateName, compartmentId, client.GetCertClient()) +func ensureCaBundleForBackendSet(inputCaBundleId string, namespace string, secretName string, compartmentId string, secretLister v1.SecretLister, client *client.WrapperClient) (*string, error) { + caBundleName, err := getCertificateNameFromSecret(namespace, secretName, secretLister) + if err != nil { + return nil, err + } + + tlsSecretData, err := getTlsSecretContent(namespace, secretName, secretLister) + if err != nil { + return nil, err + } + + caBundleId, err := VerifyOrGetCaBundleIdByName(inputCaBundleId, caBundleName, compartmentId, client.GetCertClient()) if err != nil { return nil, err } if caBundleId == nil { - tlsSecretData, err := getTlsSecretContent(namespace, secretName, client.GetK8Client()) + klog.Infof("Need to create ca bundle for secret %s", klog.KRef(namespace, secretName)) + createCaBundle, err := CreateCaBundle(caBundleName, compartmentId, client.GetCertClient(), tlsSecretData.CaCertificateChain) if err != nil { return nil, err } - createCaBundle, err := CreateCaBundle(certificateName, compartmentId, client.GetCertClient(), tlsSecretData.CaCertificateChain) + + caBundleId = createCaBundle.Id + } else { + caBundle, _, err := GetCaBundle(*caBundleId, client.GetCertClient()) if err != nil { return nil, err } - caBundleId = createCaBundle.Id + + if caBundle.FreeformTags == nil || hashString(tlsSecretData.CaCertificateChain) != caBundle.FreeformTags[caBundleHashTagKey] { + klog.Infof("Detected hash mismatch for ca bundle related to secret %s, will update ca bundle %s", + klog.KRef(namespace, secretName), *caBundle.Id) + _, err = UpdateCaBundle(*caBundleId, client.GetCertClient(), tlsSecretData.CaCertificateChain) + if err != nil { + return nil, err + } + } } + return caBundleId, nil } @@ -445,3 +365,12 @@ func backendSetSslConfigNeedsUpdate(calculatedConfig *ociloadbalancer.SslConfigu return false } + +func isCertificateCurrentVersionLatest(cert *certificatesmanagement.Certificate) bool { + for _, stage := range cert.CurrentVersion.Stages { + if stage == certificatesmanagement.VersionStageLatest { + return true + } + } + return false +} diff --git a/pkg/controllers/ingress/util_test.go b/pkg/controllers/ingress/util_test.go index 57f99b91..74137da8 100644 --- a/pkg/controllers/ingress/util_test.go +++ b/pkg/controllers/ingress/util_test.go @@ -17,6 +17,12 @@ import ( "crypto/x509/pkix" "encoding/pem" "errors" + "github.com/oracle/oci-native-ingress-controller/pkg/exception" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/informers" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "math/big" "net" "net/http" @@ -128,13 +134,20 @@ const ( errorImportCert = "errorImportCert" ) -func initsUtil() (*client.ClientProvider, ociloadbalancer.LoadBalancer) { +func getSecretListerForSecretList(list *corev1.SecretList) corelisters.SecretLister { k8client := fakeclientset.NewSimpleClientset() - secret := util.GetSampleCertSecret("test", "oci-cert", "chain", "cert", "key") - action := "get" - resource := "secrets" - obj := secret - util.FakeClientGetCall(k8client, action, resource, obj) + util.FakeClientGetCall(k8client, "list", "secrets", list) + informerFactory := informers.NewSharedInformerFactory(k8client, 0) + secretInformer := informerFactory.Core().V1().Secrets() + secretInformer.Lister() + informerFactory.Start(context.Background().Done()) + cache.WaitForCacheSync(context.Background().Done(), secretInformer.Informer().HasSynced) + return secretInformer.Lister() +} + +func initsUtil(secretList *corev1.SecretList) (*client.ClientProvider, ociloadbalancer.LoadBalancer, corelisters.SecretLister) { + k8client := fakeclientset.NewSimpleClientset() + secretLister := getSecretListerForSecretList(secretList) certClient := GetCertClient() certManageClient := GetCertManageClient() @@ -201,36 +214,56 @@ func initsUtil() (*client.ClientProvider, ociloadbalancer.LoadBalancer) { DefaultConfigGetter: &MockConfigGetter{}, Cache: NewMockCacheStore(wrapperClient), } - return mockClient, lb + return mockClient, lb, secretLister +} + +func TestHashPublicTlsData(t *testing.T) { + RegisterTestingT(t) + + tlsData := TLSSecretData{ + CaCertificateChain: common.String("ca-cert-chain"), + ServerCertificate: common.String("server-cert"), + PrivateKey: common.String("server-key"), + } + + hashedString := hashPublicTlsData(&tlsData) + Expect(hashedString).Should(Equal("e6920702515dc87338e84377a9d9fb985c2a7c7140c4518217831e9fbbd7cb43")) + + // Ensure it is consistent + Expect(hashPublicTlsData(&tlsData)).Should(Equal(hashedString)) } func TestGetSSLConfigForBackendSet(t *testing.T) { RegisterTestingT(t) - c, lb := initsUtil() + c, lb, secretLister := initsUtil(&corev1.SecretList{ + Items: []corev1.Secret{ + *util.GetSampleCertSecret(namespace, "oci-config", "chain", "cert", "key"), + }, + }) mockClient, err := c.GetClient(&MockConfigGetter{}) Expect(err).Should(BeNil()) - config, err := GetSSLConfigForBackendSet(namespace, state.ArtifactTypeSecret, "oci-config", &lb, "testecho1", "", mockClient) + config, err := GetSSLConfigForBackendSet(namespace, state.ArtifactTypeSecret, "oci-config", &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(config != nil).Should(BeTrue()) - config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeIssuedByInternalCa), &lb, "testecho1", "", mockClient) + config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeIssuedByInternalCa), &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(config != nil).Should(BeTrue()) - config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeManagedExternallyIssuedByInternalCa), &lb, "testecho1", "", mockClient) + config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeManagedExternallyIssuedByInternalCa), &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(config != nil).Should(BeTrue()) - config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeImported), &lb, "testecho1", "", mockClient) + config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, string(certificatesmanagement.CertificateConfigTypeImported), &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(config != nil).Should(BeTrue()) // No ca bundle scenario - config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, errorImportCert, &lb, "testecho1", "", mockClient) + config, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, errorImportCert, &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(BeNil()) - _, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, "error", &lb, "testecho1", "", mockClient) + _, err = GetSSLConfigForBackendSet(namespace, state.ArtifactTypeCertificate, "error", &lb, "testecho1", "", secretLister, mockClient) Expect(err).Should(Not(BeNil())) Expect(err.Error()).Should(Equal(errorMsg)) @@ -238,19 +271,27 @@ func TestGetSSLConfigForBackendSet(t *testing.T) { func TestGetSSLConfigForListener(t *testing.T) { RegisterTestingT(t) - c, _ := initsUtil() + + secretList := &corev1.SecretList{ + Items: []corev1.Secret{ + *util.GetSampleCertSecret(namespace, "secret", "chain", "cert", "key"), + *util.GetSampleCertSecret(namespace, "secret-cert", "chain", "cert", "key"), + }, + } + + c, _, secretLister := initsUtil(secretList) mockClient, err := c.GetClient(&MockConfigGetter{}) Expect(err).Should(BeNil()) //no listener for cert - sslConfig, err := GetSSLConfigForListener(namespace, nil, state.ArtifactTypeCertificate, "certificate", "", mockClient) + sslConfig, err := GetSSLConfigForListener(namespace, nil, state.ArtifactTypeCertificate, "certificate", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(sslConfig != nil).Should(BeTrue()) Expect(len(sslConfig.CertificateIds)).Should(Equal(1)) Expect(sslConfig.CertificateIds[0]).Should(Equal("certificate")) //no listener for secret - sslConfig, err = GetSSLConfigForListener(namespace, nil, state.ArtifactTypeSecret, "secret", "", mockClient) + sslConfig, err = GetSSLConfigForListener(namespace, nil, state.ArtifactTypeSecret, "secret", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(sslConfig != nil).Should(BeTrue()) Expect(len(sslConfig.CertificateIds)).Should(Equal(1)) @@ -265,14 +306,14 @@ func TestGetSSLConfigForListener(t *testing.T) { listener := ociloadbalancer.Listener{ SslConfiguration: &customSslConfig, } - sslConfig, err = GetSSLConfigForListener(namespace, &listener, state.ArtifactTypeCertificate, "certificate", "", mockClient) + sslConfig, err = GetSSLConfigForListener(namespace, &listener, state.ArtifactTypeCertificate, "certificate", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(sslConfig != nil).Should(BeTrue()) Expect(len(sslConfig.CertificateIds)).Should(Equal(1)) Expect(sslConfig.CertificateIds[0]).Should(Equal("certificate")) // Listener + secret - sslConfig, err = GetSSLConfigForListener(namespace, &listener, state.ArtifactTypeSecret, "secret-cert", "", mockClient) + sslConfig, err = GetSSLConfigForListener(namespace, &listener, state.ArtifactTypeSecret, "secret-cert", "", secretLister, mockClient) Expect(err).Should(BeNil()) Expect(sslConfig != nil).Should(BeTrue()) Expect(len(sslConfig.CertificateIds)).Should(Equal(1)) @@ -280,29 +321,6 @@ func TestGetSSLConfigForListener(t *testing.T) { } -func TestGetCertificate(t *testing.T) { - RegisterTestingT(t) - c, _ := initsUtil() - mockClient, err := c.GetClient(&MockConfigGetter{}) - Expect(err).Should(BeNil()) - - certId := "id" - certId2 := "id2" - - certificate, err := GetCertificate(&certId, mockClient.GetCertClient()) - Expect(certificate != nil).Should(BeTrue()) - Expect(err).Should(BeNil()) - - // cache fetch - certificate, err = GetCertificate(&certId, mockClient.GetCertClient()) - Expect(certificate != nil).Should(BeTrue()) - Expect(err).Should(BeNil()) - - certificate, err = GetCertificate(&certId2, mockClient.GetCertClient()) - Expect(certificate != nil).Should(BeTrue()) - Expect(err).Should(BeNil()) -} - func TestGetTlsSecretContent(t *testing.T) { RegisterTestingT(t) @@ -313,28 +331,36 @@ func TestGetTlsSecretContent(t *testing.T) { secretWithWrongChain := util.GetSampleCertSecret("test", "secretWithWrongChain", "", testCaChain+testCert, testKey) secretWithoutCaCrt := util.GetSampleCertSecret("test", "secretWithoutCaCrt", "", testCert, testKey) - k8client := fakeclientset.NewSimpleClientset() - util.FakeClientGetCall(k8client, "get", "secrets", secretWithCaCrt) + secretList := &corev1.SecretList{ + Items: []corev1.Secret{ + *secretWithCaCrt, + *secretWithCorrectChain, + *secretWithWrongChain, + *secretWithoutCaCrt, + }, + } - secretData1, err := getTlsSecretContent("test", "secretWithCaCrt", k8client) + secretLister := getSecretListerForSecretList(secretList) + + secretData1, err := getTlsSecretContent("test", "secretWithCaCrt", secretLister) Expect(err).ToNot(HaveOccurred()) Expect(*secretData1.CaCertificateChain).To(Equal(testCaChain)) Expect(*secretData1.ServerCertificate).To(Equal(testCert)) Expect(*secretData1.PrivateKey).To(Equal(testKey)) - util.FakeClientGetCall(k8client, "get", "secrets", secretWithCorrectChain) - secretData2, err := getTlsSecretContent("test", "secretWithCorrectChain", k8client) + secretData2, err := getTlsSecretContent("test", "secretWithCorrectChain", secretLister) Expect(err).ToNot(HaveOccurred()) Expect(*secretData2.CaCertificateChain).To(Equal(testCaChain)) Expect(*secretData2.ServerCertificate).To(Equal(testCert)) Expect(*secretData2.PrivateKey).To(Equal(testKey)) - util.FakeClientGetCall(k8client, "get", "secrets", secretWithWrongChain) - _, err = getTlsSecretContent("test", "secretWithWrongChain", k8client) + _, err = getTlsSecretContent("test", "secretWithWrongChain", secretLister) + Expect(err).To(HaveOccurred()) + + _, err = getTlsSecretContent("test", "secretWithoutCaCrt", secretLister) Expect(err).To(HaveOccurred()) - util.FakeClientGetCall(k8client, "get", "secrets", secretWithoutCaCrt) - _, err = getTlsSecretContent("test", "secretWithoutCaCrt", k8client) + _, err = getTlsSecretContent("test", "nonexistent", secretLister) Expect(err).To(HaveOccurred()) } @@ -395,6 +421,58 @@ func TestBackendSetSslConfigNeedsUpdate(t *testing.T) { Expect(backendSetSslConfigNeedsUpdate(calculatedConfig1, backendSetWithNilSslConfig)).To(BeTrue()) } +func TestGetCertificateNameFromSecret(t *testing.T) { + RegisterTestingT(t) + + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "secret", + Namespace: "namespace", + UID: "uid", + }, + } + + secretList := &corev1.SecretList{ + Items: []corev1.Secret{ + *secret, + }, + } + + secretLister := getSecretListerForSecretList(secretList) + + secretName, err := getCertificateNameFromSecret("namespace", "", secretLister) + Expect(err).To(BeNil()) + Expect(secretName).To(Equal("")) + + secretName, err = getCertificateNameFromSecret("namespace", "nonexistent", secretLister) + Expect(err).ToNot(BeNil()) + Expect(secretName).To(Equal("")) + + secretName, err = getCertificateNameFromSecret("namespace", "secret", secretLister) + Expect(err).To(BeNil()) + Expect(secretName).To(Equal("oci-nic-uid")) +} + +func TestIsCertificateCurrentVersionLatest(t *testing.T) { + RegisterTestingT(t) + + cert := &certificatesmanagement.Certificate{ + CurrentVersion: &certificatesmanagement.CertificateVersionSummary{ + Stages: []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageCurrent, + certificatesmanagement.VersionStageFailed, + }, + }, + } + Expect(isCertificateCurrentVersionLatest(cert)).To(BeFalse()) + + cert.CurrentVersion.Stages = []certificatesmanagement.VersionStageEnum{ + certificatesmanagement.VersionStageCurrent, + certificatesmanagement.VersionStageLatest, + } + Expect(isCertificateCurrentVersionLatest(cert)).To(BeTrue()) +} + func generateTestCertsAndKey() (string, string, string) { caCert := &x509.Certificate{ SerialNumber: big.NewInt(1), @@ -439,13 +517,18 @@ type MockCertificateManagerClient struct { } func (m MockCertificateManagerClient) CreateCertificate(ctx context.Context, request certificatesmanagement.CreateCertificateRequest) (certificatesmanagement.CreateCertificateResponse, error) { + if *request.Name == "error" { + return certificatesmanagement.CreateCertificateResponse{}, errors.New("cert create error") + } + id := "id" + etag := "etag" return certificatesmanagement.CreateCertificateResponse{ RawResponse: nil, Certificate: certificatesmanagement.Certificate{ Id: &id, }, - Etag: nil, + Etag: &etag, OpcRequestId: &id, }, nil } @@ -458,9 +541,10 @@ func (m MockCertificateManagerClient) GetCertificate(ctx context.Context, reques id := "id" name := "cert" authorityId := "authId" + etag := "etag" var confType certificatesmanagement.CertificateConfigTypeEnum if *request.CertificateId == errorImportCert { - name = "error" + name = "errorImportCert" confType = certificatesmanagement.CertificateConfigTypeImported } else { confType, _ = certificatesmanagement.GetMappingCertificateConfigTypeEnum(*request.CertificateId) @@ -478,22 +562,49 @@ func (m MockCertificateManagerClient) GetCertificate(ctx context.Context, reques ConfigType: confType, IssuerCertificateAuthorityId: &authorityId, CurrentVersion: &certVersionSummary, + LifecycleState: certificatesmanagement.CertificateLifecycleStateActive, }, - Etag: nil, + Etag: &etag, OpcRequestId: nil, }, nil } func (m MockCertificateManagerClient) ListCertificates(ctx context.Context, request certificatesmanagement.ListCertificatesRequest) (certificatesmanagement.ListCertificatesResponse, error) { + if *request.Name == "error" { + return certificatesmanagement.ListCertificatesResponse{}, errors.New("cert list error") + } + + if *request.Name == "nonexistent" { + return certificatesmanagement.ListCertificatesResponse{}, nil + } + id := "id" return certificatesmanagement.ListCertificatesResponse{ - RawResponse: nil, - CertificateCollection: certificatesmanagement.CertificateCollection{}, - OpcRequestId: &id, - OpcNextPage: &id, + RawResponse: nil, + CertificateCollection: certificatesmanagement.CertificateCollection{ + Items: []certificatesmanagement.CertificateSummary{ + { + Id: common.String(id), + }, + }, + }, + OpcRequestId: &id, + OpcNextPage: &id, }, nil } +func (m MockCertificateManagerClient) UpdateCertificate(ctx context.Context, request certificatesmanagement.UpdateCertificateRequest) (certificatesmanagement.UpdateCertificateResponse, error) { + if *request.CertificateId == "error" { + return certificatesmanagement.UpdateCertificateResponse{}, errors.New("cert update error") + } + + if *request.CertificateId == "conflictError" { + return certificatesmanagement.UpdateCertificateResponse{}, &exception.ConflictServiceError{} + } + + return certificatesmanagement.UpdateCertificateResponse{}, nil +} + func (m MockCertificateManagerClient) ScheduleCertificateDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateDeletionRequest) (certificatesmanagement.ScheduleCertificateDeletionResponse, error) { var err error if *request.CertificateId == "error" { @@ -502,33 +613,86 @@ func (m MockCertificateManagerClient) ScheduleCertificateDeletion(ctx context.Co return certificatesmanagement.ScheduleCertificateDeletionResponse{}, err } +func (m MockCertificateManagerClient) ListCertificateVersions(ctx context.Context, request certificatesmanagement.ListCertificateVersionsRequest) (certificatesmanagement.ListCertificateVersionsResponse, error) { + if *request.CertificateId == "error" { + return certificatesmanagement.ListCertificateVersionsResponse{}, errors.New("list cert versions error") + } + + createCertificateVersionSummary := func(versionNumber int64, stages []certificatesmanagement.VersionStageEnum) certificatesmanagement.CertificateVersionSummary { + return certificatesmanagement.CertificateVersionSummary{ + VersionNumber: common.Int64(versionNumber), + Stages: stages, + } + } + + return certificatesmanagement.ListCertificateVersionsResponse{ + CertificateVersionCollection: certificatesmanagement.CertificateVersionCollection{ + Items: []certificatesmanagement.CertificateVersionSummary{ + createCertificateVersionSummary(int64(9), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageFailed, certificatesmanagement.VersionStageLatest}), + createCertificateVersionSummary(int64(8), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageCurrent}), + createCertificateVersionSummary(int64(7), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageFailed}), + createCertificateVersionSummary(int64(6), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStagePrevious}), + createCertificateVersionSummary(int64(5), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageDeprecated}), + createCertificateVersionSummary(int64(4), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageDeprecated}), + createCertificateVersionSummary(int64(3), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageDeprecated}), + createCertificateVersionSummary(int64(2), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageFailed}), + createCertificateVersionSummary(int64(1), []certificatesmanagement.VersionStageEnum{certificatesmanagement.VersionStageDeprecated}), + }, + }, + }, nil +} + +func (m MockCertificateManagerClient) ScheduleCertificateVersionDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateVersionDeletionRequest) (certificatesmanagement.ScheduleCertificateVersionDeletionResponse, error) { + if *request.CertificateId == "error" { + return certificatesmanagement.ScheduleCertificateVersionDeletionResponse{}, errors.New("cert version delete error") + } + + return certificatesmanagement.ScheduleCertificateVersionDeletionResponse{}, nil +} + func (m MockCertificateManagerClient) CreateCaBundle(ctx context.Context, request certificatesmanagement.CreateCaBundleRequest) (certificatesmanagement.CreateCaBundleResponse, error) { + if *request.Name == "error" { + return certificatesmanagement.CreateCaBundleResponse{}, errors.New("caBundle create error") + } + id := "id" + etag := "etag" return certificatesmanagement.CreateCaBundleResponse{ RawResponse: nil, CaBundle: certificatesmanagement.CaBundle{ Id: &id, }, - Etag: nil, + Etag: &etag, OpcRequestId: nil, }, nil } func (m MockCertificateManagerClient) GetCaBundle(ctx context.Context, request certificatesmanagement.GetCaBundleRequest) (certificatesmanagement.GetCaBundleResponse, error) { + if *request.CaBundleId == "error" { + return certificatesmanagement.GetCaBundleResponse{}, errors.New("no ca bundle found") + } + id := "id" name := "cabundle" + etag := "etag" return certificatesmanagement.GetCaBundleResponse{ RawResponse: nil, CaBundle: certificatesmanagement.CaBundle{ - Id: &id, - Name: &name, + Id: &id, + Name: &name, + LifecycleState: certificatesmanagement.CaBundleLifecycleStateActive, }, OpcRequestId: &id, + Etag: &etag, }, nil } func (m MockCertificateManagerClient) ListCaBundles(ctx context.Context, request certificatesmanagement.ListCaBundlesRequest) (certificatesmanagement.ListCaBundlesResponse, error) { if *request.Name == "error" { + return certificatesmanagement.ListCaBundlesResponse{}, errors.New("caBundle list error") + } + + if *request.Name == "nonexistent" { return certificatesmanagement.ListCaBundlesResponse{}, nil } @@ -551,6 +715,18 @@ func (m MockCertificateManagerClient) ListCaBundles(ctx context.Context, request }, nil } +func (m MockCertificateManagerClient) UpdateCaBundle(ctx context.Context, request certificatesmanagement.UpdateCaBundleRequest) (certificatesmanagement.UpdateCaBundleResponse, error) { + if *request.CaBundleId == "error" { + return certificatesmanagement.UpdateCaBundleResponse{}, errors.New("caBundle update error") + } + + if *request.CaBundleId == "conflictError" { + return certificatesmanagement.UpdateCaBundleResponse{}, &exception.ConflictServiceError{} + } + + return certificatesmanagement.UpdateCaBundleResponse{}, nil +} + func (m MockCertificateManagerClient) DeleteCaBundle(ctx context.Context, request certificatesmanagement.DeleteCaBundleRequest) (certificatesmanagement.DeleteCaBundleResponse, error) { res := http.Response{ Status: "200", diff --git a/pkg/exception/util.go b/pkg/exception/util.go index 5ce3616d..46f13bd1 100644 --- a/pkg/exception/util.go +++ b/pkg/exception/util.go @@ -34,3 +34,27 @@ func (e *NotFoundServiceError) GetOpcRequestID() string { func (e *NotFoundServiceError) Error() string { return "NotFound" } + +type ConflictServiceError struct { + common.ServiceError +} + +func (e *ConflictServiceError) GetHTTPStatusCode() int { + return 409 +} + +func (e *ConflictServiceError) GetMessage() string { + return "Conflict" +} + +func (e *ConflictServiceError) GetCode() string { + return "Conflict" +} + +func (e *ConflictServiceError) GetOpcRequestID() string { + return "fakeopcrequestid" +} + +func (e *ConflictServiceError) Error() string { + return "Conflict" +} diff --git a/pkg/loadbalancer/loadbalancer.go b/pkg/loadbalancer/loadbalancer.go index 2712d32f..a636e667 100644 --- a/pkg/loadbalancer/loadbalancer.go +++ b/pkg/loadbalancer/loadbalancer.go @@ -249,7 +249,7 @@ func (lbc *LoadBalancerClient) createRoutingPolicy( klog.Infof("Creating routing policy with request: %s", util.PrettyPrint(createPolicyRequest)) resp, err := lbc.LbClient.CreateRoutingPolicy(ctx, createPolicyRequest) - if isServiceError(err, 409) { + if util.IsServiceError(err, 409) { klog.Infof("Create routing policy operation returned code %d for load balancer %s. Routing policy %s may be already present.", 409, lbID, policyName) return nil } @@ -276,7 +276,7 @@ func (lbc *LoadBalancerClient) DeleteRoutingPolicy( klog.Infof("Delete routing policy with request %s ", util.PrettyPrint(deleteRoutingPolicyRequest)) resp, err := lbc.LbClient.DeleteRoutingPolicy(ctx, deleteRoutingPolicyRequest) - if isServiceError(err, 404) { + if util.IsServiceError(err, 404) { klog.Infof("Delete routing policy operation returned code %d for load balancer %s. Routing policy %s may be already deleted.", 404, lbID, policyName) return nil } @@ -311,7 +311,7 @@ func (lbc *LoadBalancerClient) DeleteBackendSet(ctx context.Context, lbID string klog.Infof("Deleting backend set with request %s", util.PrettyPrint(backendSetDeleteRequest)) resp, err := lbc.LbClient.DeleteBackendSet(ctx, backendSetDeleteRequest) - if isServiceError(err, 404) { + if util.IsServiceError(err, 404) { // it was already deleted so nothing to do. klog.Infof("Delete backend set operation returned code %d for load balancer %s. Backend set %s may be already deleted.", 404, lbID, backendSetName) return nil @@ -347,7 +347,7 @@ func (lbc *LoadBalancerClient) DeleteListener(ctx context.Context, lbID string, klog.Infof("Deleting listener with request %s", util.PrettyPrint(deleteListenerRequest)) resp, err := lbc.LbClient.DeleteListener(ctx, deleteListenerRequest) - if isServiceError(err, 404) { + if util.IsServiceError(err, 404) { // it was already deleted so nothing to do. klog.Infof("Delete listener operation returned code %d for load balancer %s. Listener %s may be already deleted.", 404, lbID, listenerName) return nil @@ -394,7 +394,7 @@ func (lbc *LoadBalancerClient) CreateBackendSet( klog.Infof("Creating backend set with request: %s", util.PrettyPrint(createBackendSetRequest)) resp, err := lbc.LbClient.CreateBackendSet(ctx, createBackendSetRequest) - if isServiceError(err, 409) { + if util.IsServiceError(err, 409) { klog.Infof("Create backend set operation returned code %d for load balancer %s. Backend set %s may be already present.", 409, lbID, backendSetName) return nil } @@ -714,7 +714,7 @@ func (lbc *LoadBalancerClient) CreateListener(ctx context.Context, lbID string, klog.Infof("Creating listener with request %s", util.PrettyPrint(createListenerRequest)) resp, err := lbc.LbClient.CreateListener(ctx, createListenerRequest) - if isServiceError(err, 409) { + if util.IsServiceError(err, 409) { klog.Infof("Create listener operation returned code %d for load balancer %s. Listener %s may be already present.", 409, lbID, listenerName) return nil } @@ -752,12 +752,3 @@ func (lbc *LoadBalancerClient) waitForWorkRequest(ctx context.Context, workReque time.Sleep(10 * time.Second) } } - -func isServiceError(err error, statusCode int) bool { - svcErr, ok := common.IsServiceError(err) - if !ok { - return false - } - - return svcErr.GetHTTPStatusCode() == statusCode -} diff --git a/pkg/oci/client/certificate.go b/pkg/oci/client/certificate.go index 170da389..b824b688 100644 --- a/pkg/oci/client/certificate.go +++ b/pkg/oci/client/certificate.go @@ -37,11 +37,13 @@ type CertificateInterface interface { type CertCacheObj struct { Cert *certificatesmanagement.Certificate Age time.Time + Etag string } type CaBundleCacheObj struct { CaBundle *certificatesmanagement.CaBundle Age time.Time + Etag string } type CertificateClient struct { diff --git a/pkg/oci/client/certificatemanagement.go b/pkg/oci/client/certificatemanagement.go index d3f33964..4ae01072 100644 --- a/pkg/oci/client/certificatemanagement.go +++ b/pkg/oci/client/certificatemanagement.go @@ -10,10 +10,14 @@ type CertificateManagementInterface interface { CreateCertificate(ctx context.Context, request certificatesmanagement.CreateCertificateRequest) (certificatesmanagement.CreateCertificateResponse, error) GetCertificate(ctx context.Context, request certificatesmanagement.GetCertificateRequest) (certificatesmanagement.GetCertificateResponse, error) ListCertificates(ctx context.Context, request certificatesmanagement.ListCertificatesRequest) (certificatesmanagement.ListCertificatesResponse, error) + UpdateCertificate(ctx context.Context, request certificatesmanagement.UpdateCertificateRequest) (certificatesmanagement.UpdateCertificateResponse, error) ScheduleCertificateDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateDeletionRequest) (certificatesmanagement.ScheduleCertificateDeletionResponse, error) + ListCertificateVersions(ctx context.Context, request certificatesmanagement.ListCertificateVersionsRequest) (certificatesmanagement.ListCertificateVersionsResponse, error) + ScheduleCertificateVersionDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateVersionDeletionRequest) (certificatesmanagement.ScheduleCertificateVersionDeletionResponse, error) CreateCaBundle(ctx context.Context, request certificatesmanagement.CreateCaBundleRequest) (certificatesmanagement.CreateCaBundleResponse, error) GetCaBundle(ctx context.Context, request certificatesmanagement.GetCaBundleRequest) (certificatesmanagement.GetCaBundleResponse, error) ListCaBundles(ctx context.Context, request certificatesmanagement.ListCaBundlesRequest) (certificatesmanagement.ListCaBundlesResponse, error) + UpdateCaBundle(ctx context.Context, request certificatesmanagement.UpdateCaBundleRequest) (certificatesmanagement.UpdateCaBundleResponse, error) DeleteCaBundle(ctx context.Context, request certificatesmanagement.DeleteCaBundleRequest) (certificatesmanagement.DeleteCaBundleResponse, error) } @@ -42,11 +46,26 @@ func (client CertificateManagementClient) ListCertificates(ctx context.Context, return client.managementClient.ListCertificates(ctx, request) } +func (client CertificateManagementClient) UpdateCertificate(ctx context.Context, + request certificatesmanagement.UpdateCertificateRequest) (certificatesmanagement.UpdateCertificateResponse, error) { + return client.managementClient.UpdateCertificate(ctx, request) +} + func (client CertificateManagementClient) ScheduleCertificateDeletion(ctx context.Context, request certificatesmanagement.ScheduleCertificateDeletionRequest) (certificatesmanagement.ScheduleCertificateDeletionResponse, error) { return client.managementClient.ScheduleCertificateDeletion(ctx, request) } +func (client CertificateManagementClient) ListCertificateVersions(ctx context.Context, + request certificatesmanagement.ListCertificateVersionsRequest) (certificatesmanagement.ListCertificateVersionsResponse, error) { + return client.managementClient.ListCertificateVersions(ctx, request) +} + +func (client CertificateManagementClient) ScheduleCertificateVersionDeletion(ctx context.Context, + request certificatesmanagement.ScheduleCertificateVersionDeletionRequest) (certificatesmanagement.ScheduleCertificateVersionDeletionResponse, error) { + return client.managementClient.ScheduleCertificateVersionDeletion(ctx, request) +} + func (client CertificateManagementClient) CreateCaBundle(ctx context.Context, request certificatesmanagement.CreateCaBundleRequest) (certificatesmanagement.CreateCaBundleResponse, error) { return client.managementClient.CreateCaBundle(ctx, request) @@ -62,6 +81,11 @@ func (client CertificateManagementClient) ListCaBundles(ctx context.Context, return client.managementClient.ListCaBundles(ctx, request) } +func (client CertificateManagementClient) UpdateCaBundle(ctx context.Context, + request certificatesmanagement.UpdateCaBundleRequest) (certificatesmanagement.UpdateCaBundleResponse, error) { + return client.managementClient.UpdateCaBundle(ctx, request) +} + func (client CertificateManagementClient) DeleteCaBundle(ctx context.Context, request certificatesmanagement.DeleteCaBundleRequest) (certificatesmanagement.DeleteCaBundleResponse, error) { return client.managementClient.DeleteCaBundle(ctx, request) diff --git a/pkg/server/server.go b/pkg/server/server.go index 6d742ff9..1c5a72f1 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -58,9 +58,9 @@ func BuildConfig(kubeconfig string) (*rest.Config, error) { } func SetUpControllers(opts types.IngressOpts, ingressClassInformer networkinginformers.IngressClassInformer, - ingressInformer networkinginformers.IngressInformer, k8client kubernetes.Interface, - serviceInformer v1.ServiceInformer, endpointInformer v1.EndpointsInformer, podInformer v1.PodInformer, nodeInformer v1.NodeInformer, serviceAccountInformer v1.ServiceAccountInformer, c ctrcache.Cache, - reg *prometheus.Registry) func(ctx context.Context) { + ingressInformer networkinginformers.IngressInformer, k8client kubernetes.Interface, serviceInformer v1.ServiceInformer, secretInformer v1.SecretInformer, + endpointInformer v1.EndpointsInformer, podInformer v1.PodInformer, nodeInformer v1.NodeInformer, serviceAccountInformer v1.ServiceAccountInformer, + c ctrcache.Cache, reg *prometheus.Registry) func(ctx context.Context) { return func(ctx context.Context) { klog.Info("Controller loop...") @@ -82,6 +82,7 @@ func SetUpControllers(opts types.IngressOpts, ingressClassInformer networkinginf ingressInformer, serviceAccountInformer, serviceInformer.Lister(), + secretInformer, client, reg, ) diff --git a/pkg/util/util.go b/pkg/util/util.go index bb73bd31..01f51764 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -828,3 +828,12 @@ func IsIngressProtocolTCP(ingress *networkingv1.Ingress) bool { func StringSlicesHaveSameElements(s1 []string, s2 []string) bool { return sets.New(s1...).Equal(sets.New(s2...)) } + +func IsServiceError(err error, statusCode int) bool { + svcErr, ok := common.IsServiceError(err) + if !ok { + return false + } + + return svcErr.GetHTTPStatusCode() == statusCode +}