@@ -1649,6 +1649,225 @@ func TestAPI(t *testing.T) {
1649
1649
assert .Empty (t , fakeSAC .agents )
1650
1650
})
1651
1651
1652
+ t .Run ("Error" , func (t * testing.T ) {
1653
+ t .Parallel ()
1654
+
1655
+ if runtime .GOOS == "windows" {
1656
+ t .Skip ("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)" )
1657
+ }
1658
+
1659
+ t .Run ("DuringUp" , func (t * testing.T ) {
1660
+ t .Parallel ()
1661
+
1662
+ var (
1663
+ ctx = testutil .Context (t , testutil .WaitMedium )
1664
+ logger = slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
1665
+ mClock = quartz .NewMock (t )
1666
+ fCCLI = & fakeContainerCLI {arch : "<none>" }
1667
+ fDCCLI = & fakeDevcontainerCLI {
1668
+ upErrC : make (chan error , 1 ),
1669
+ }
1670
+ fSAC = & fakeSubAgentClient {
1671
+ logger : logger .Named ("fakeSubAgentClient" ),
1672
+ }
1673
+
1674
+ testDevcontainer = codersdk.WorkspaceAgentDevcontainer {
1675
+ ID : uuid .New (),
1676
+ Name : "test-devcontainer" ,
1677
+ WorkspaceFolder : "/workspaces/project" ,
1678
+ ConfigPath : "/workspaces/project/.devcontainer/devcontainer.json" ,
1679
+ Status : codersdk .WorkspaceAgentDevcontainerStatusStopped ,
1680
+ }
1681
+ )
1682
+
1683
+ mClock .Set (time .Now ()).MustWait (ctx )
1684
+ tickerTrap := mClock .Trap ().TickerFunc ("updaterLoop" )
1685
+ nowRecreateErrorTrap := mClock .Trap ().Now ("recreate" , "errorTimes" )
1686
+ nowRecreateSuccessTrap := mClock .Trap ().Now ("recreate" , "successTimes" )
1687
+
1688
+ api := agentcontainers .NewAPI (logger ,
1689
+ agentcontainers .WithClock (mClock ),
1690
+ agentcontainers .WithContainerCLI (fCCLI ),
1691
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1692
+ agentcontainers .WithDevcontainers (
1693
+ []codersdk.WorkspaceAgentDevcontainer {testDevcontainer },
1694
+ []codersdk.WorkspaceAgentScript {{ID : testDevcontainer .ID , LogSourceID : uuid .New ()}},
1695
+ ),
1696
+ agentcontainers .WithSubAgentClient (fSAC ),
1697
+ agentcontainers .WithSubAgentURL ("test-subagent-url" ),
1698
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
1699
+ )
1700
+ api .Start ()
1701
+ defer func () {
1702
+ close (fDCCLI .upErrC )
1703
+ api .Close ()
1704
+ }()
1705
+
1706
+ r := chi .NewRouter ()
1707
+ r .Mount ("/" , api .Routes ())
1708
+
1709
+ tickerTrap .MustWait (ctx ).MustRelease (ctx )
1710
+ tickerTrap .Close ()
1711
+
1712
+ // Given: We send a 'recreate' request.
1713
+ req := httptest .NewRequest (http .MethodPost , "/devcontainers/" + testDevcontainer .ID .String ()+ "/recreate" , nil )
1714
+ rec := httptest .NewRecorder ()
1715
+ r .ServeHTTP (rec , req )
1716
+ require .Equal (t , http .StatusAccepted , rec .Code )
1717
+
1718
+ // Given: We simulate an error running `devcontainer up`
1719
+ simulatedError := xerrors .New ("simulated error" )
1720
+ testutil .RequireSend (ctx , t , fDCCLI .upErrC , simulatedError )
1721
+
1722
+ nowRecreateErrorTrap .MustWait (ctx ).MustRelease (ctx )
1723
+ nowRecreateErrorTrap .Close ()
1724
+
1725
+ req = httptest .NewRequest (http .MethodGet , "/" , nil )
1726
+ rec = httptest .NewRecorder ()
1727
+ r .ServeHTTP (rec , req )
1728
+ require .Equal (t , http .StatusOK , rec .Code )
1729
+
1730
+ var response codersdk.WorkspaceAgentListContainersResponse
1731
+ err := json .NewDecoder (rec .Body ).Decode (& response )
1732
+ require .NoError (t , err )
1733
+
1734
+ // Then: We expect that there will be an error associated with the devcontainer.
1735
+ require .Len (t , response .Devcontainers , 1 )
1736
+ require .Equal (t , "simulated error" , response .Devcontainers [0 ].Error )
1737
+
1738
+ // Given: We send another 'recreate' request.
1739
+ req = httptest .NewRequest (http .MethodPost , "/devcontainers/" + testDevcontainer .ID .String ()+ "/recreate" , nil )
1740
+ rec = httptest .NewRecorder ()
1741
+ r .ServeHTTP (rec , req )
1742
+ require .Equal (t , http .StatusAccepted , rec .Code )
1743
+
1744
+ // Given: We allow `devcontainer up` to succeed.
1745
+ testutil .RequireSend (ctx , t , fDCCLI .upErrC , nil )
1746
+
1747
+ nowRecreateSuccessTrap .MustWait (ctx ).MustRelease (ctx )
1748
+ nowRecreateSuccessTrap .Close ()
1749
+
1750
+ req = httptest .NewRequest (http .MethodGet , "/" , nil )
1751
+ rec = httptest .NewRecorder ()
1752
+ r .ServeHTTP (rec , req )
1753
+ require .Equal (t , http .StatusOK , rec .Code )
1754
+
1755
+ response = codersdk.WorkspaceAgentListContainersResponse {}
1756
+ err = json .NewDecoder (rec .Body ).Decode (& response )
1757
+ require .NoError (t , err )
1758
+
1759
+ // Then: We expect that there will be no error associated with the devcontainer.
1760
+ require .Len (t , response .Devcontainers , 1 )
1761
+ require .Equal (t , "" , response .Devcontainers [0 ].Error )
1762
+ })
1763
+
1764
+ t .Run ("DuringInjection" , func (t * testing.T ) {
1765
+ t .Parallel ()
1766
+
1767
+ var (
1768
+ ctx = testutil .Context (t , testutil .WaitMedium )
1769
+ logger = slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
1770
+ mClock = quartz .NewMock (t )
1771
+ mCCLI = acmock .NewMockContainerCLI (gomock .NewController (t ))
1772
+ fDCCLI = & fakeDevcontainerCLI {}
1773
+ fSAC = & fakeSubAgentClient {
1774
+ logger : logger .Named ("fakeSubAgentClient" ),
1775
+ createErrC : make (chan error , 1 ),
1776
+ }
1777
+
1778
+ containerCreatedAt = time .Now ()
1779
+ testContainer = codersdk.WorkspaceAgentContainer {
1780
+ ID : "test-container-id" ,
1781
+ FriendlyName : "test-container" ,
1782
+ Image : "test-image" ,
1783
+ Running : true ,
1784
+ CreatedAt : containerCreatedAt ,
1785
+ Labels : map [string ]string {
1786
+ agentcontainers .DevcontainerLocalFolderLabel : "/workspaces" ,
1787
+ agentcontainers .DevcontainerConfigFileLabel : "/workspace/.devcontainer/devcontainer.json" ,
1788
+ },
1789
+ }
1790
+ )
1791
+
1792
+ coderBin , err := os .Executable ()
1793
+ require .NoError (t , err )
1794
+
1795
+ // Mock the `List` function to always return the test container.
1796
+ mCCLI .EXPECT ().List (gomock .Any ()).Return (codersdk.WorkspaceAgentListContainersResponse {
1797
+ Containers : []codersdk.WorkspaceAgentContainer {testContainer },
1798
+ }, nil ).AnyTimes ()
1799
+
1800
+ // We're going to force the container CLI to fail, which will allow us to test the
1801
+ // error handling.
1802
+ simulatedError := xerrors .New ("simulated error" )
1803
+ mCCLI .EXPECT ().DetectArchitecture (gomock .Any (), testContainer .ID ).Return ("" , simulatedError ).Times (1 )
1804
+
1805
+ mClock .Set (containerCreatedAt ).MustWait (ctx )
1806
+ tickerTrap := mClock .Trap ().TickerFunc ("updaterLoop" )
1807
+
1808
+ api := agentcontainers .NewAPI (logger ,
1809
+ agentcontainers .WithClock (mClock ),
1810
+ agentcontainers .WithContainerCLI (mCCLI ),
1811
+ agentcontainers .WithDevcontainerCLI (fDCCLI ),
1812
+ agentcontainers .WithSubAgentClient (fSAC ),
1813
+ agentcontainers .WithSubAgentURL ("test-subagent-url" ),
1814
+ agentcontainers .WithWatcher (watcher .NewNoop ()),
1815
+ )
1816
+ api .Start ()
1817
+ defer func () {
1818
+ close (fSAC .createErrC )
1819
+ api .Close ()
1820
+ }()
1821
+
1822
+ r := chi .NewRouter ()
1823
+ r .Mount ("/" , api .Routes ())
1824
+
1825
+ // Given: We allow an attempt at creation to occur.
1826
+ tickerTrap .MustWait (ctx ).MustRelease (ctx )
1827
+ tickerTrap .Close ()
1828
+
1829
+ req := httptest .NewRequest (http .MethodGet , "/" , nil )
1830
+ rec := httptest .NewRecorder ()
1831
+ r .ServeHTTP (rec , req )
1832
+ require .Equal (t , http .StatusOK , rec .Code )
1833
+
1834
+ var response codersdk.WorkspaceAgentListContainersResponse
1835
+ err = json .NewDecoder (rec .Body ).Decode (& response )
1836
+ require .NoError (t , err )
1837
+
1838
+ // Then: We expect that there will be an error associated with the devcontainer.
1839
+ require .Len (t , response .Devcontainers , 1 )
1840
+ require .Equal (t , "detect architecture: simulated error" , response .Devcontainers [0 ].Error )
1841
+
1842
+ gomock .InOrder (
1843
+ mCCLI .EXPECT ().DetectArchitecture (gomock .Any (), testContainer .ID ).Return (runtime .GOARCH , nil ),
1844
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "mkdir" , "-p" , "/.coder-agent" ).Return (nil , nil ),
1845
+ mCCLI .EXPECT ().Copy (gomock .Any (), testContainer .ID , coderBin , "/.coder-agent/coder" ).Return (nil ),
1846
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "chmod" , "0755" , "/.coder-agent" , "/.coder-agent/coder" ).Return (nil , nil ),
1847
+ mCCLI .EXPECT ().ExecAs (gomock .Any (), testContainer .ID , "root" , "/bin/sh" , "-c" , "chown $(id -u):$(id -g) /.coder-agent/coder" ).Return (nil , nil ),
1848
+ )
1849
+
1850
+ // Given: We allow creation to succeed.
1851
+ testutil .RequireSend (ctx , t , fSAC .createErrC , nil )
1852
+
1853
+ _ , aw := mClock .AdvanceNext ()
1854
+ aw .MustWait (ctx )
1855
+
1856
+ req = httptest .NewRequest (http .MethodGet , "/" , nil )
1857
+ rec = httptest .NewRecorder ()
1858
+ r .ServeHTTP (rec , req )
1859
+ require .Equal (t , http .StatusOK , rec .Code )
1860
+
1861
+ response = codersdk.WorkspaceAgentListContainersResponse {}
1862
+ err = json .NewDecoder (rec .Body ).Decode (& response )
1863
+ require .NoError (t , err )
1864
+
1865
+ // Then: We expect that the error will be gone
1866
+ require .Len (t , response .Devcontainers , 1 )
1867
+ require .Equal (t , "" , response .Devcontainers [0 ].Error )
1868
+ })
1869
+ })
1870
+
1652
1871
t .Run ("Create" , func (t * testing.T ) {
1653
1872
t .Parallel ()
1654
1873
0 commit comments