⋯ 911 lines Show the 911 lines above this hunk @@ -912,9 +912,11 @@ describe('deployAllComputeSites attach-mode database ensure', () => { 912 912 const { ok , driver } = await deploy ( attachConfig ( 'canonical' , 'stacks' ) ) 913 913 expect ( ok ) . toBe ( true ) 914 914 const calls = ( driver . runRemoteDeploy as ReturnType < typeof mock > ) . mock . calls 915 - 916 - expect ( calls . length ) . toBe ( 3 ) 917 - const ensure = calls [ 1 ] [ 0 ] 915 + 916 + 917 + expect ( calls . length ) . toBe ( 4 ) 918 + expect ( calls [ 1 ] [ 0 ] . commands . join ( '\n' ) ) . toContain ( 'ts_cloud_probe postgres' ) 919 + const ensure = calls [ 2 ] [ 0 ] 918 920 const sql = ensure . commands . join ( '\n' ) 919 921 920 922 expect ( sql ) . toContain ( "IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'training')" ) ⋯ 6 lines Show the 6 lines above this hunk @@ -927,15 +929,15 @@ describe('deployAllComputeSites attach-mode database ensure', () => { 927 929 expect ( ensure . comment ) . toBe ( 'ts-cloud ensure database training/training' ) 928 930 929 931 930 - expect ( calls [ 2 ] [ 0 ] . commands . join ( '\n' ) ) . toContain ( 'systemctl restart training-web@abc.service' ) 932 + expect ( calls [ 3 ] [ 0 ] . commands . join ( '\n' ) ) . toContain ( 'systemctl restart training-web@abc.service' ) 931 933 } ) 932 934 933 935 it ( 'honors the deprecated infrastructure.compute.database alias (bughq shape)' , async ( ) => { 934 936 const { ok , driver } = await deploy ( attachConfig ( 'legacy' , 'stacks' ) ) 935 937 expect ( ok ) . toBe ( true ) 936 938 const calls = ( driver . runRemoteDeploy as ReturnType < typeof mock > ) . mock . calls 937 - expect ( calls . length ) . toBe ( 3 ) 938 - const sql = calls [ 1 ] [ 0 ] . commands . join ( '\n' ) 939 + expect ( calls . length ) . toBe ( 4 ) 940 + const sql = calls [ 2 ] [ 0 ] . commands . join ( '\n' ) 939 941 expect ( sql ) . toContain ( 'CREATE ROLE "training" LOGIN PASSWORD \'pw\'' ) 940 942 expect ( sql ) . toContain ( 'CREATE DATABASE "training" OWNER "training"' ) 941 943 } ) ⋯ 11 lines Show the 11 lines above this hunk @@ -953,8 +955,10 @@ describe('deployAllComputeSites attach-mode database ensure', () => { 953 955 const { ok , driver } = await deploy ( attachConfig ( 'none' , 'stacks' ) ) 954 956 expect ( ok ) . toBe ( true ) 955 957 const calls = ( driver . runRemoteDeploy as ReturnType < typeof mock > ) . mock . calls 956 - expect ( calls . length ) . toBe ( 2 ) 957 - expect ( calls [ 0 ] [ 0 ] . commands . join ( '\n' ) ) . not . toContain ( 'CREATE DATABASE' ) 958 + 959 + 960 + expect ( calls . length ) . toBe ( 3 ) 961 + expect ( calls . map ( ( call : any [ ] ) => call [ 0 ] . commands . join ( '\n' ) ) . join ( '\n' ) ) . not . toContain ( 'CREATE DATABASE' ) 958 962 } ) 959 963 960 964 it ( 'fails the deploy (shipping nothing) when the database ensure fails' , async ( ) => { ⋯ 16 lines Show the 16 lines above this hunk @@ -977,3 +981,122 @@ describe('deployAllComputeSites attach-mode database ensure', () => { 977 981 expect ( ( driver . runRemoteDeploy as ReturnType < typeof mock > ) . mock . calls . length ) . toBe ( 1 ) 978 982 } ) 979 983 } ) 984 + 985 + 986 + 987 + 988 + 989 + 990 + 991 + 992 + describe ( 'deployAllComputeSites attach-mode service preflight' , ( ) => { 993 + function attachedConfig ( ) : CloudConfig { 994 + return { 995 + project : { name : 'Log HQ' , slug : 'loghq' , region : 'fsn1' } , 996 + environments : { production : { type : 'production' } } , 997 + cloud : { provider : 'hetzner' , attachTo : 'uptime-status' } , 998 + sites : { web : { domain : 'loghq.example.com' , port : 3000 , root : '.output' , start : 'bun run server.ts' } } , 999 + infrastructure : { 1000 + appDatabase : { engine : 'postgres' , name : 'loghq' , username : 'loghq' , password : 'secret' } , 1001 + compute : { runtime : 'bun' , managedServices : { postgres : true } , proxy : { engine : 'rpx' } } , 1002 + } , 1003 + } 1004 + } 1005 + 1006 + function run ( driver : CloudDriver , config : CloudConfig , errors : string [ ] ) : Promise < boolean > { 1007 + const tempDir = mkdtempSync ( join ( tmpdir ( ) , 'ts-cloud-attach-' ) ) 1008 + const tarball = join ( tempDir , 'release.tar.gz' ) 1009 + writeFileSync ( tarball , 'fake tarball' ) 1010 + process . env . TS_CLOUD_UI_DISABLE = '1' 1011 + return deployAllComputeSites ( { 1012 + config , 1013 + environment : 'production' , 1014 + driver , 1015 + sha : 'abc' , 1016 + runtime : 'bun' , 1017 + tarballForSite : ( ) => tarball , 1018 + logger : { 1019 + info : ( ) => { } , 1020 + warn : ( ) => { } , 1021 + error : ( message : string ) => errors . push ( message ) , 1022 + step : ( ) => { } , 1023 + success : ( ) => { } , 1024 + } , 1025 + } ) . finally ( ( ) => { 1026 + delete process . env . TS_CLOUD_UI_DISABLE 1027 + rmSync ( tempDir , { recursive : true , force : true } ) 1028 + } ) 1029 + } 1030 + 1031 + it ( 'refuses before shipping anything when the host has no postgres' , async ( ) => { 1032 + const driver = createMockDriver ( { 1033 + name : 'hetzner' , 1034 + usesCloudFormation : false , 1035 + runRemoteDeploy : mock ( async ( options : { commands : string [ ] } ) => ( { 1036 + success : true , 1037 + instanceCount : 1 , 1038 + perInstance : [ 1039 + { 1040 + instanceId : 'i-abc123' , 1041 + status : 'Success' , 1042 + output : options . commands . join ( '\n' ) . includes ( 'ts_cloud_probe postgres' ) 1043 + ? 'ts-cloud-service:postgres:missing' 1044 + : '' , 1045 + } , 1046 + ] , 1047 + } ) ) , 1048 + } ) 1049 + const errors : string [ ] = [ ] 1050 + expect ( await run ( driver , attachedConfig ( ) , errors ) ) . toBe ( false ) 1051 + expect ( errors . join ( '\n' ) ) . toContain ( 'managedServices.postgres' ) 1052 + expect ( errors . join ( '\n' ) ) . toContain ( "'uptime-status'" ) 1053 + 1054 + expect ( driver . uploadRelease ) . not . toHaveBeenCalled ( ) 1055 + } ) 1056 + 1057 + it ( 'proceeds when the host provides what was declared' , async ( ) => { 1058 + const driver = createMockDriver ( { 1059 + name : 'hetzner' , 1060 + usesCloudFormation : false , 1061 + runRemoteDeploy : mock ( async ( ) => ( { 1062 + success : true , 1063 + instanceCount : 1 , 1064 + perInstance : [ { instanceId : 'i-abc123' , status : 'Success' , output : 'ts-cloud-service:postgres:present' } ] , 1065 + } ) ) , 1066 + } ) 1067 + const errors : string [ ] = [ ] 1068 + expect ( await run ( driver , attachedConfig ( ) , errors ) ) . toBe ( true ) 1069 + expect ( errors ) . toEqual ( [ ] ) 1070 + } ) 1071 + 1072 + 1073 + 1074 + 1075 + 1076 + 1077 + it ( 'does not block when the probe returns nothing' , async ( ) => { 1078 + const driver = createMockDriver ( { 1079 + name : 'hetzner' , 1080 + usesCloudFormation : false , 1081 + runRemoteDeploy : mock ( async ( ) => ( { 1082 + success : true , 1083 + instanceCount : 1 , 1084 + perInstance : [ { instanceId : 'i-abc123' , status : 'Success' } ] , 1085 + } ) ) , 1086 + } ) 1087 + const errors : string [ ] = [ ] 1088 + expect ( await run ( driver , attachedConfig ( ) , errors ) ) . toBe ( true ) 1089 + } ) 1090 + 1091 + it ( 'probes nothing for a project that owns its box' , async ( ) => { 1092 + const config = attachedConfig ( ) 1093 + delete config . cloud 1094 + const driver = createMockDriver ( { name : 'hetzner' , usesCloudFormation : false } ) 1095 + const errors : string [ ] = [ ] 1096 + expect ( await run ( driver , config , errors ) ) . toBe ( true ) 1097 + const commands = ( driver . runRemoteDeploy as ReturnType < typeof mock > ) . mock . calls 1098 + . map ( ( call : any [ ] ) => call [ 0 ] . commands . join ( '\n' ) ) 1099 + . join ( '\n' ) 1100 + expect ( commands ) . not . toContain ( 'ts_cloud_probe' ) 1101 + } ) 1102 + } )