@@ -477,6 +477,39 @@ var _ = Describe("ACME Issuer DNS01 solver", Ordered, func() {
477477 return subscriptionID , resourceGroupName , hostedZoneName
478478 }
479479
480+ // setupCCOAzureCredentials creates a CredentialsRequest for Azure with fine-grained
481+ // DNS Zone Contributor permissions and returns the CCO-provisioned credentials.
482+ // Note: Unlike setupAmbientAWSCredentials/setupAmbientGCPCredentials, this does NOT patch
483+ // the subscription with 'CLOUD_CREDENTIALS_SECRET_NAME' because the operator does not yet
484+ // support mounting Azure credentials into the cert-manager pod. Once Azure support is added to
485+ // 'withCloudCredentials' in credentials_request.go, it can be adapted to follow the AWS/GCP pattern.
486+ setupCCOAzureCredentials := func (ctx context.Context ) (clientID , clientSecret , tenantID []byte ) {
487+ By ("creating CredentialsRequest object for Azure" )
488+ loader .CreateFromFile (testassets .ReadFile , filepath .Join ("testdata" , "credentials" , "credentialsrequest_azure.yaml" ), "" )
489+ DeferCleanup (func () {
490+ loader .DeleteFromFile (testassets .ReadFile , filepath .Join ("testdata" , "credentials" , "credentialsrequest_azure.yaml" ), "" )
491+ })
492+
493+ By ("waiting for cloud secret to be available" )
494+ var ccoSecret * corev1.Secret
495+ err := wait .PollUntilContextTimeout (context .TODO (), slowPollInterval , highTimeout , true , func (context.Context ) (bool , error ) {
496+ var getErr error
497+ ccoSecret , getErr = loader .KubeClient .CoreV1 ().Secrets ("cert-manager" ).Get (ctx , "azure-credentials" , metav1.GetOptions {})
498+ return getErr == nil , nil
499+ })
500+ Expect (err ).NotTo (HaveOccurred (), "timeout waiting for Azure credentials secret" )
501+
502+ By ("reading CCO-provisioned credentials" )
503+ clientID = ccoSecret .Data ["azure_client_id" ]
504+ clientSecret = ccoSecret .Data ["azure_client_secret" ]
505+ tenantID = ccoSecret .Data ["azure_tenant_id" ]
506+ Expect (clientID ).NotTo (BeEmpty (), "azure_client_id should not be empty" )
507+ Expect (clientSecret ).NotTo (BeEmpty (), "azure_client_secret should not be empty" )
508+ Expect (tenantID ).NotTo (BeEmpty (), "azure_tenant_id should not be empty" )
509+
510+ return clientID , clientSecret , tenantID
511+ }
512+
480513 // copyAzureSecretToNamespace creates a secret in the test namespace with Azure client secret
481514 copyAzureSecretToNamespace := func (ctx context.Context , namespace , secretName , secretKey string , clientSecret []byte ) {
482515 By (fmt .Sprintf ("copying Azure client secret to namespace %s" , namespace ))
@@ -1305,6 +1338,52 @@ var _ = Describe("ACME Issuer DNS01 solver", Ordered, func() {
13051338 dnsName := fmt .Sprintf ("adaze-%s.%s" , randomStr (3 ), appsDomain ) // acronym for "ACME DNS01 AzureDNS Explicit"
13061339 createAndVerifyACMECertificate (ctx , certName , ns .Name , dnsName , issuerName , "Issuer" )
13071340 })
1341+
1342+ It ("should obtain a valid certificate using CCO-provisioned credentials" , func () {
1343+
1344+ // Setup CCO-provisioned credentials for Azure (fine-grained DNS Zone Contributor)
1345+ clientID , clientSecret , tenantID := setupCCOAzureCredentials (ctx )
1346+
1347+ // Get DNS zone subscription, resource group, and zone name from the DNS config object
1348+ subscriptionID , resourceGroupName , hostedZoneName := getAzureDNSZoneInfo (ctx )
1349+
1350+ // Copy client secret to test namespace for Issuer reference
1351+ secretName := "azure-client-secret"
1352+ secretKey := "client-secret"
1353+ copyAzureSecretToNamespace (ctx , ns .Name , secretName , secretKey , clientSecret )
1354+
1355+ By ("creating ACME Issuer with AzureDNS DNS-01 solver using CCO-provisioned credentials" )
1356+ issuerName := "letsencrypt-dns01-cco"
1357+ solver := acmev1.ACMEChallengeSolver {
1358+ DNS01 : & acmev1.ACMEChallengeSolverDNS01 {
1359+ AzureDNS : & acmev1.ACMEIssuerDNS01ProviderAzureDNS {
1360+ SubscriptionID : subscriptionID ,
1361+ ResourceGroupName : resourceGroupName ,
1362+ HostedZoneName : hostedZoneName ,
1363+ TenantID : string (tenantID ),
1364+ ClientID : string (clientID ),
1365+ ClientSecret : & certmanagermetav1.SecretKeySelector {
1366+ LocalObjectReference : certmanagermetav1.LocalObjectReference {
1367+ Name : secretName ,
1368+ },
1369+ Key : secretKey ,
1370+ },
1371+ },
1372+ },
1373+ }
1374+ issuer := createACMEIssuer (issuerName , solver )
1375+ _ , err := certmanagerClient .CertmanagerV1 ().Issuers (ns .Name ).Create (ctx , issuer , metav1.CreateOptions {})
1376+ Expect (err ).NotTo (HaveOccurred (), "failed to create Issuer" )
1377+
1378+ By ("waiting for Issuer to become ready" )
1379+ err = waitForIssuerReadiness (ctx , issuerName , ns .Name )
1380+ Expect (err ).NotTo (HaveOccurred (), "timeout waiting for Issuer to become Ready" )
1381+
1382+ // Create and verify certificate
1383+ certName := "letsencrypt-cert"
1384+ dnsName := fmt .Sprintf ("adazc-%s.%s" , randomStr (3 ), appsDomain ) // acronym for "ACME DNS01 AzureDNS CCO"
1385+ createAndVerifyACMECertificate (ctx , certName , ns .Name , dnsName , issuerName , "Issuer" )
1386+ })
13081387 })
13091388
13101389 Context ("with IBM Cloud Internet Service Webhook" , Label ("Platform:IBM" ), func () {
0 commit comments