@@ -1612,13 +1612,15 @@ func acrTestHTTPReply(t *testing.T, request *http.Request, code int, value any)
16121612
16131613func TestACRProjectOnlyProvisionPreservesPendingSelector (t * testing.T ) {
16141614 for _ , tt := range []struct {
1615- mode string
1616- down bool
1615+ mode string
1616+ down , refresh bool
16171617 }{
16181618 {mode : "" }, {mode : "reuse-connect" },
16191619 {mode : "" , down : true }, {mode : "reuse-connect" , down : true },
1620+ {mode : "" , refresh : true }, {mode : "reuse-connect" , refresh : true },
1621+ {mode : "" , down : true , refresh : true }, {mode : "reuse-connect" , down : true , refresh : true },
16201622 } {
1621- t .Run (tt . mode + " /down=" + strconv . FormatBool ( tt .down ), func (t * testing.T ) {
1623+ t .Run (fmt . Sprintf ( "%s /down=%t/refresh=%t" , tt . mode , tt .down , tt . refresh ), func (t * testing.T ) {
16221624 root := t .TempDir ()
16231625 manifest := "services:\n project:\n host: azure.ai.project\n "
16241626 require .NoError (t , os .WriteFile (filepath .Join (root , "azure.yaml" ), []byte (manifest ), 0o600 ))
@@ -1634,6 +1636,7 @@ func TestACRProjectOnlyProvisionPreservesPendingSelector(t *testing.T) {
16341636 & azdext.ProvisioningOptions {Provider : FoundryProviderName }))
16351637 require .Equal (t , "none" , p .greenfieldAcrMode )
16361638 p .credential = acrTestCredential {}
1639+ var deployedOutputs map [string ]any
16371640 p .armClientOptions = & arm.ClientOptions {ClientOptions : policy.ClientOptions {
16381641 Transport : acrTestTransport (func (request * http.Request ) (* http.Response , error ) {
16391642 if request .Method == http .MethodGet &&
@@ -1646,11 +1649,17 @@ func TestACRProjectOnlyProvisionPreservesPendingSelector(t *testing.T) {
16461649 }
16471650 require .Equal (t , "/subscriptions/sub/providers/Microsoft.Resources/deployments/" +
16481651 p .deploymentName (), request .URL .Path )
1652+ deployedOutputs = nil
16491653 return acrTestHTTPReply (t , request , http .StatusNoContent , nil )
16501654 }
16511655 require .Contains (t , request .URL .Path , "/providers/Microsoft.Resources/deployments/" )
16521656 if request .Method == http .MethodGet {
1653- return acrTestHTTPReply (t , request , http .StatusNotFound , nil )
1657+ if deployedOutputs == nil {
1658+ return acrTestHTTPReply (t , request , http .StatusNotFound , nil )
1659+ }
1660+ return acrTestHTTPReply (t , request , http .StatusOK , map [string ]any {
1661+ "properties" : map [string ]any {"provisioningState" : "Succeeded" , "outputs" : deployedOutputs },
1662+ })
16541663 }
16551664 require .Equal (t , http .MethodPut , request .Method )
16561665 outputs := map [string ]any {
@@ -1663,6 +1672,7 @@ func TestACRProjectOnlyProvisionPreservesPendingSelector(t *testing.T) {
16631672 } {
16641673 outputs [key ] = map [string ]any {"type" : "string" , "value" : "" }
16651674 }
1675+ deployedOutputs = outputs
16661676 return acrTestHTTPReply (t , request , http .StatusOK , map [string ]any {
16671677 "properties" : map [string ]any {"provisioningState" : "Succeeded" , "outputs" : outputs },
16681678 })
@@ -1686,6 +1696,28 @@ func TestACRProjectOnlyProvisionPreservesPendingSelector(t *testing.T) {
16861696 assert .Empty (t , env .config )
16871697 assert .Nil (t , p .acrBinding )
16881698
1699+ if tt .refresh {
1700+ refresher := & FoundryProvisioningProvider {
1701+ azdClient : p .azdClient , credential : p .credential , armClientOptions : p .armClientOptions ,
1702+ }
1703+ require .NoError (t , refresher .Initialize (t .Context (), root ,
1704+ & azdext.ProvisioningOptions {Provider : FoundryProviderName }))
1705+ env .configErr = errors .New ("refresh must not persist registry lifecycle state" )
1706+ state , err := refresher .State (t .Context (), & azdext.ProvisioningStateOptions {})
1707+ env .configErr = nil
1708+ require .NoError (t , err )
1709+ require .NotNil (t , state .State )
1710+ assert .Empty (t , env .config )
1711+ assert .Empty (t , env .set )
1712+ for key , output := range state .State .Outputs {
1713+ values [key ] = output .Value
1714+ }
1715+ require .NoError (t , godotenv .Write (values , envPath ))
1716+ env .get , err = godotenv .Read (envPath )
1717+ require .NoError (t , err )
1718+ require .Equal (t , testACRID , env .get ["AZURE_CONTAINER_REGISTRY_RESOURCE_ID" ])
1719+ }
1720+
16891721 if tt .down {
16901722 destroyed , err := p .Destroy (t .Context (),
16911723 & azdext.ProvisioningDestroyOptions {Force : true , Purge : true }, func (string ) {})
@@ -1715,6 +1747,134 @@ func TestACRProjectOnlyProvisionPreservesPendingSelector(t *testing.T) {
17151747 }
17161748}
17171749
1750+ func TestACROutputPreservationKeepsCurrentOutputs (t * testing.T ) {
1751+ for _ , tt := range []struct {
1752+ name , registry , endpoint , connection string
1753+ brownfield bool
1754+ }{
1755+ {
1756+ name : "current registry" , registry : testACRID ,
1757+ endpoint : "current.azurecr.io" , connection : "current-connection" ,
1758+ },
1759+ {
1760+ name : "current connection" , endpoint : "current.azurecr.io" , connection : "current-connection" ,
1761+ },
1762+ {name : "brownfield" , brownfield : true },
1763+ } {
1764+ t .Run (tt .name , func (t * testing.T ) {
1765+ p , env := acrTestProvider (t )
1766+ p .acrBinding = acrTestBinding ()
1767+ if tt .brownfield {
1768+ p .brownfieldEndpoint = "https://existing.services.ai.azure.com/api/projects/project"
1769+ }
1770+ env .getErr = map [string ]error {
1771+ "AZURE_CONTAINER_REGISTRY_ENDPOINT" : errors .New ("must not replace current outputs" ),
1772+ "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" : errors .New ("must not replace current outputs" ),
1773+ }
1774+ outputs := map [string ]* azdext.ProvisioningOutputParameter {
1775+ "AZURE_CONTAINER_REGISTRY_RESOURCE_ID" : {Type : "string" , Value : tt .registry },
1776+ "AZURE_CONTAINER_REGISTRY_ENDPOINT" : {Type : "string" , Value : tt .endpoint },
1777+ "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" : {Type : "string" , Value : tt .connection },
1778+ }
1779+ require .NoError (t , p .preserveACROutputs (t .Context (), outputs ))
1780+ registry := tt .registry
1781+ if ! tt .brownfield {
1782+ registry = testACRID
1783+ }
1784+ assert .Equal (t , registry , outputs ["AZURE_CONTAINER_REGISTRY_RESOURCE_ID" ].Value )
1785+ assert .Equal (t , tt .endpoint , outputs ["AZURE_CONTAINER_REGISTRY_ENDPOINT" ].Value )
1786+ assert .Equal (t , tt .connection , outputs ["AZURE_AI_PROJECT_ACR_CONNECTION_NAME" ].Value )
1787+ assert .Empty (t , env .set )
1788+ assert .Empty (t , env .config )
1789+ })
1790+ }
1791+ }
1792+
1793+ func TestACRStatePreservesDeployedRegistryOutputs (t * testing.T ) {
1794+ for _ , mode := range []string {"create" , "reuse-connect" } {
1795+ for _ , alias := range []bool {false , true } {
1796+ for _ , failRead := range []string {"" , "AZURE_CONTAINER_REGISTRY_ENDPOINT" , "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" } {
1797+ t .Run (fmt .Sprintf ("%s/alias=%t/read=%s" , mode , alias , failRead ), func (t * testing.T ) {
1798+ root := t .TempDir ()
1799+ require .NoError (t , os .WriteFile (filepath .Join (root , "azure.yaml" ), []byte (
1800+ "services:\n project:\n host: azure.ai.project\n " ), 0o600 ))
1801+ p , env := acrTestProvider (t )
1802+ binding := acrTestBinding ()
1803+ binding .Mode = mode
1804+ if mode == "create" {
1805+ binding .RegistryResourceID = strings .Replace (testACRID , "/shared/" , "/foundry/" , 1 )
1806+ }
1807+ data , err := json .Marshal (binding )
1808+ require .NoError (t , err )
1809+ env .config = map [string ][]byte {synthesis .ACRBindingConfigKey : data }
1810+ env .get = map [string ]string {
1811+ envKeySubscriptionID : "sub" , envKeyLocation : "eastus2" ,
1812+ envKeyResourceGroup : "foundry" , envKeyProjectName : "project" ,
1813+ "AZD_FOUNDRY_ACR_MODE" : mode ,
1814+ "AZURE_CONTAINER_REGISTRY_ENDPOINT" : "registry.azurecr.io" ,
1815+ "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" : "registry-connection" ,
1816+ }
1817+ if alias {
1818+ env .get ["AZURE_CONTAINER_REGISTRY_RESOURCE_ID" ] = binding .RegistryResourceID
1819+ }
1820+ require .NoError (t , p .Initialize (t .Context (), root ,
1821+ & azdext.ProvisioningOptions {Provider : FoundryProviderName }))
1822+ beforeValues , beforeConfig := maps .Clone (env .get ), maps .Clone (env .config )
1823+ env .configErr = errors .New ("state must not persist registry lifecycle state" )
1824+ if failRead != "" {
1825+ env .getErr = map [string ]error {failRead : errors .New ("registry output unavailable" )}
1826+ }
1827+ p .credential = acrTestCredential {}
1828+ reads := 0
1829+ p .armClientOptions = & arm.ClientOptions {ClientOptions : policy.ClientOptions {
1830+ Transport : acrTestTransport (func (request * http.Request ) (* http.Response , error ) {
1831+ require .Equal (t , http .MethodGet , request .Method )
1832+ require .Equal (t , "/subscriptions/sub/providers/Microsoft.Resources/deployments/" +
1833+ p .deploymentName (), request .URL .Path )
1834+ reads ++
1835+ outputs := map [string ]any {"AZURE_AI_PROJECT_ID" : map [string ]any {
1836+ "type" : "string" , "value" : testACRProject ,
1837+ }}
1838+ for _ , key := range []string {
1839+ "AZURE_CONTAINER_REGISTRY_RESOURCE_ID" , "AZURE_CONTAINER_REGISTRY_ENDPOINT" ,
1840+ "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" ,
1841+ } {
1842+ outputs [key ] = map [string ]any {"type" : "string" , "value" : "" }
1843+ }
1844+ return acrTestHTTPReply (t , request , http .StatusOK , map [string ]any {
1845+ "properties" : map [string ]any {"provisioningState" : "Succeeded" , "outputs" : outputs },
1846+ })
1847+ }),
1848+ }}
1849+ state , err := p .State (t .Context (), & azdext.ProvisioningStateOptions {})
1850+ if failRead != "" {
1851+ require .ErrorContains (t , err , "preserve deployed registry output" )
1852+ require .ErrorContains (t , err , "registry output unavailable" )
1853+ assert .Nil (t , state )
1854+ } else {
1855+ require .NoError (t , err )
1856+ require .NotNil (t , state .State )
1857+ for key , value := range map [string ]string {
1858+ "AZURE_CONTAINER_REGISTRY_RESOURCE_ID" : binding .RegistryResourceID ,
1859+ "AZURE_CONTAINER_REGISTRY_ENDPOINT" : "registry.azurecr.io" ,
1860+ "AZURE_AI_PROJECT_ACR_CONNECTION_NAME" : "registry-connection" ,
1861+ } {
1862+ require .Contains (t , state .State .Outputs , key )
1863+ assert .Equal (t , value , state .State .Outputs [key ].Value )
1864+ }
1865+ }
1866+ assert .Equal (t , 1 , reads )
1867+ assert .Equal (t , beforeValues , env .get )
1868+ assert .Equal (t , beforeConfig , env .config )
1869+ assert .Empty (t , env .set )
1870+ assert .Equal (t , binding , p .acrBinding )
1871+ assert .Nil (t , p .acrPending )
1872+ })
1873+ }
1874+ }
1875+ }
1876+ }
1877+
17181878func TestACRFailedDeploymentRecoversIndependentOwnership (t * testing.T ) {
17191879 for _ , tt := range []struct {
17201880 name string
0 commit comments