@@ -116,6 +116,35 @@ func skipIfDockerLinkUnsupportedT(t *testing.T) {
116116 }
117117}
118118
119+ // getCurrentContainerIDs reads the container IDs from dokku's internal
120+ // CONTAINER files (e.g., /home/dokku/APP/CONTAINER.web.1) which are the
121+ // authoritative source for the current deployment's containers.
122+ func getCurrentContainerIDs (appName , processType string ) ([]string , error ) {
123+ scale , err := getPsScale (appName )
124+ if err != nil {
125+ return nil , err
126+ }
127+ count , ok := scale [processType ]
128+ if ! ok || count == 0 {
129+ return nil , nil
130+ }
131+ var ids []string
132+ for i := 1 ; i <= count ; i ++ {
133+ result , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
134+ Command : "cat" ,
135+ Args : []string {fmt .Sprintf ("/home/dokku/%s/CONTAINER.%s.%d" , appName , processType , i )},
136+ })
137+ if err != nil {
138+ continue
139+ }
140+ id := strings .TrimSpace (result .StdoutContents ())
141+ if id != "" {
142+ ids = append (ids , id )
143+ }
144+ }
145+ return ids , nil
146+ }
147+
119148func TestIntegrationAppCreateAndDestroy (t * testing.T ) {
120149 skipIfNoDokkuT (t )
121150
@@ -699,7 +728,7 @@ func TestIntegrationGitFromImage(t *testing.T) {
699728
700729 task := GitFromImageTask {
701730 App : appName ,
702- Image : "nginx:latest " ,
731+ Image : "dokku/smoke-test-app:dockerfile " ,
703732 State : StateDeployed ,
704733 }
705734 result := task .Execute ()
@@ -976,6 +1005,179 @@ func TestIntegrationResourceReserveProcessType(t *testing.T) {
9761005 }
9771006}
9781007
1008+ func TestIntegrationPsScale (t * testing.T ) {
1009+ skipIfNoDokkuT (t )
1010+
1011+ appName := "omakase-test-psscale"
1012+
1013+ // ensure clean state
1014+ destroyApp (appName )
1015+ createApp (appName )
1016+ defer destroyApp (appName )
1017+
1018+ // deploy the smoke test app so we have running containers to scale
1019+ deployTask := GitFromImageTask {
1020+ App : appName ,
1021+ Image : "dokku/smoke-test-app:dockerfile" ,
1022+ State : StateDeployed ,
1023+ }
1024+ deployResult := deployTask .Execute ()
1025+ if deployResult .Error != nil {
1026+ t .Fatalf ("failed to deploy app: %v" , deployResult .Error )
1027+ }
1028+
1029+ // verify initial web container count is 1 via docker ps
1030+ initialContainers , err := getCurrentContainerIDs (appName , "web" )
1031+ if err != nil {
1032+ t .Fatalf ("failed to list containers: %v" , err )
1033+ }
1034+ if len (initialContainers ) != 1 {
1035+ t .Fatalf ("expected 1 initial web container, got %d" , len (initialContainers ))
1036+ }
1037+
1038+ // verify the initial container is running via docker inspect
1039+ inspectResult , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
1040+ Command : "docker" ,
1041+ Args : []string {"inspect" , "--format" , "{{.State.Running}}" , initialContainers [0 ]},
1042+ })
1043+ if err != nil {
1044+ t .Fatalf ("failed to inspect initial container: %v" , err )
1045+ }
1046+ if strings .TrimSpace (inspectResult .StdoutContents ()) != "true" {
1047+ t .Errorf ("expected initial container to be running" )
1048+ }
1049+
1050+ // scale web to 2
1051+ scaleTask := PsScaleTask {
1052+ App : appName ,
1053+ Scale : map [string ]int {"web" : 2 },
1054+ State : StatePresent ,
1055+ }
1056+ result := scaleTask .Execute ()
1057+ if result .Error != nil {
1058+ t .Fatalf ("failed to scale app: %v" , result .Error )
1059+ }
1060+ if result .State != StatePresent {
1061+ t .Errorf ("expected state 'present', got '%s'" , result .State )
1062+ }
1063+ if ! result .Changed {
1064+ t .Error ("expected changed=true for scaling up" )
1065+ }
1066+
1067+ // clean up old containers and verify 2 web containers via docker ps
1068+ scaledContainers , err := getCurrentContainerIDs (appName , "web" )
1069+ if err != nil {
1070+ t .Fatalf ("failed to list containers after scale: %v" , err )
1071+ }
1072+ if len (scaledContainers ) != 2 {
1073+ t .Fatalf ("expected 2 web containers after scaling, got %d" , len (scaledContainers ))
1074+ }
1075+
1076+ // verify each container is running via docker inspect
1077+ for _ , containerID := range scaledContainers {
1078+ inspectResult , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
1079+ Command : "docker" ,
1080+ Args : []string {"inspect" , "--format" , "{{.State.Running}}" , containerID },
1081+ })
1082+ if err != nil {
1083+ t .Fatalf ("failed to inspect container %s: %v" , containerID , err )
1084+ }
1085+ if strings .TrimSpace (inspectResult .StdoutContents ()) != "true" {
1086+ t .Errorf ("expected container %s to be running" , containerID )
1087+ }
1088+ }
1089+
1090+ // scaling again should be idempotent
1091+ result = scaleTask .Execute ()
1092+ if result .Error != nil {
1093+ t .Fatalf ("idempotent scale failed: %v" , result .Error )
1094+ }
1095+ if result .Changed {
1096+ t .Error ("expected changed=false for unchanged scale" )
1097+ }
1098+
1099+ // scale back to 1
1100+ scaleDownTask := PsScaleTask {
1101+ App : appName ,
1102+ Scale : map [string ]int {"web" : 1 },
1103+ State : StatePresent ,
1104+ }
1105+ result = scaleDownTask .Execute ()
1106+ if result .Error != nil {
1107+ t .Fatalf ("failed to scale down: %v" , result .Error )
1108+ }
1109+ if ! result .Changed {
1110+ t .Error ("expected changed=true for scaling down" )
1111+ }
1112+
1113+ // clean up old containers and verify 1 web container after scale down
1114+ finalContainers , err := getCurrentContainerIDs (appName , "web" )
1115+ if err != nil {
1116+ t .Fatalf ("failed to list containers after scale down: %v" , err )
1117+ }
1118+ if len (finalContainers ) != 1 {
1119+ t .Fatalf ("expected 1 web container after scale down, got %d" , len (finalContainers ))
1120+ }
1121+
1122+ // verify the final container is running via docker inspect
1123+ inspectResult , err = subprocess .CallExecCommand (subprocess.ExecCommandInput {
1124+ Command : "docker" ,
1125+ Args : []string {"inspect" , "--format" , "{{.State.Running}}" , finalContainers [0 ]},
1126+ })
1127+ if err != nil {
1128+ t .Fatalf ("failed to inspect final container: %v" , err )
1129+ }
1130+ if strings .TrimSpace (inspectResult .StdoutContents ()) != "true" {
1131+ t .Errorf ("expected final container to be running" )
1132+ }
1133+ }
1134+
1135+ func TestIntegrationPsScaleSkipDeploy (t * testing.T ) {
1136+ skipIfNoDokkuT (t )
1137+
1138+ appName := "omakase-test-psscale-sd"
1139+
1140+ destroyApp (appName )
1141+ createApp (appName )
1142+ defer destroyApp (appName )
1143+
1144+ // scale with skip_deploy on an undeployed app
1145+ scaleTask := PsScaleTask {
1146+ App : appName ,
1147+ Scale : map [string ]int {"web" : 2 , "worker" : 1 },
1148+ SkipDeploy : true ,
1149+ State : StatePresent ,
1150+ }
1151+ result := scaleTask .Execute ()
1152+ if result .Error != nil {
1153+ t .Fatalf ("failed to scale with skip_deploy: %v" , result .Error )
1154+ }
1155+ if ! result .Changed {
1156+ t .Error ("expected changed=true for initial scale" )
1157+ }
1158+
1159+ // verify the scale was set correctly
1160+ scale , err := getPsScale (appName )
1161+ if err != nil {
1162+ t .Fatalf ("failed to get ps scale: %v" , err )
1163+ }
1164+ if scale ["web" ] != 2 {
1165+ t .Errorf ("expected web=2, got web=%d" , scale ["web" ])
1166+ }
1167+ if scale ["worker" ] != 1 {
1168+ t .Errorf ("expected worker=1, got worker=%d" , scale ["worker" ])
1169+ }
1170+
1171+ // idempotent
1172+ result = scaleTask .Execute ()
1173+ if result .Error != nil {
1174+ t .Fatalf ("idempotent scale failed: %v" , result .Error )
1175+ }
1176+ if result .Changed {
1177+ t .Error ("expected changed=false for unchanged scale" )
1178+ }
1179+ }
1180+
9791181func TestIntegrationMultiTaskWorkflow (t * testing.T ) {
9801182 skipIfNoDokkuT (t )
9811183
@@ -1170,6 +1372,58 @@ func TestIntegrationServiceLinkAndUnlink(t *testing.T) {
11701372 t .Error ("expected service container to have a hostname set" )
11711373 }
11721374
1375+ // deploy the smoke test app so we can verify the link inside a running container
1376+ deployTask := GitFromImageTask {
1377+ App : appName ,
1378+ Image : "dokku/smoke-test-app:dockerfile" ,
1379+ State : StateDeployed ,
1380+ }
1381+ deployResult := deployTask .Execute ()
1382+ if deployResult .Error != nil {
1383+ t .Fatalf ("failed to deploy app: %v" , deployResult .Error )
1384+ }
1385+
1386+ // find the running app container
1387+ appContainerResult , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
1388+ Command : "docker" ,
1389+ Args : []string {"ps" , "--filter" , fmt .Sprintf ("label=com.dokku.app-name=%s" , appName ), "--filter" , "label=com.dokku.process-type=web" , "--format" , "{{.ID}}" },
1390+ })
1391+ if err != nil {
1392+ t .Fatalf ("failed to find app container: %v" , err )
1393+ }
1394+ appContainerID := strings .TrimSpace (appContainerResult .StdoutContents ())
1395+ if appContainerID == "" {
1396+ t .Fatal ("expected at least one running app container after deploy" )
1397+ }
1398+ // take the first container if multiple lines
1399+ appContainerIDs := strings .Split (appContainerID , "\n " )
1400+ appContainerID = appContainerIDs [0 ]
1401+
1402+ // verify the app container is running via docker inspect
1403+ appInspectResult , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
1404+ Command : "docker" ,
1405+ Args : []string {"inspect" , "--format" , "{{.State.Running}}" , appContainerID },
1406+ })
1407+ if err != nil {
1408+ t .Fatalf ("failed to inspect app container: %v" , err )
1409+ }
1410+ if strings .TrimSpace (appInspectResult .StdoutContents ()) != "true" {
1411+ t .Error ("expected app container to be running" )
1412+ }
1413+
1414+ // verify REDIS_URL is present inside the running container via docker exec
1415+ execResult , err := subprocess .CallExecCommand (subprocess.ExecCommandInput {
1416+ Command : "docker" ,
1417+ Args : []string {"exec" , appContainerID , "env" },
1418+ })
1419+ if err != nil {
1420+ t .Fatalf ("failed to exec env in app container: %v" , err )
1421+ }
1422+ envOutput := execResult .StdoutContents ()
1423+ if ! strings .Contains (envOutput , "REDIS_URL=redis://" ) {
1424+ t .Error ("expected REDIS_URL=redis://... to be present in app container environment" )
1425+ }
1426+
11731427 // linking again should be idempotent
11741428 result = linkTask .Execute ()
11751429 if result .Error != nil {
0 commit comments