|
| 1 | +import { LONG_TIMEOUT, uniqueName, makeClientSDK, cleanUpPolicy } from '../utils'; |
| 2 | +import { Blueprint, Devbox, NetworkPolicy, StorageObject } from '@runloop/api-client/sdk'; |
| 3 | + |
| 4 | +const sdk = makeClientSDK(); |
| 5 | + |
| 6 | +describe('smoketest: object-oriented blueprint', () => { |
| 7 | + describe('blueprint build context with object storage and .dockerignore', () => { |
| 8 | + test.concurrent( |
| 9 | + 'creates blueprint with COPY using object-based build context honoring .dockerignore', |
| 10 | + async () => { |
| 11 | + const fs = require('fs/promises'); |
| 12 | + const path = require('path'); |
| 13 | + const os = require('os'); |
| 14 | + |
| 15 | + let contextDir: string | undefined; |
| 16 | + let storageObject: StorageObject | undefined; |
| 17 | + let blueprint: Blueprint | undefined; |
| 18 | + let devbox: Devbox | undefined; |
| 19 | + |
| 20 | + try { |
| 21 | + // Create temporary build context directory |
| 22 | + contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-')); |
| 23 | + const includedPath = path.join(contextDir, 'included.txt'); |
| 24 | + const ignoredPath = path.join(contextDir, 'ignored.txt'); |
| 25 | + const dockerignorePath = path.join(contextDir, '.dockerignore'); |
| 26 | + |
| 27 | + await fs.writeFile(includedPath, 'Hello from context'); |
| 28 | + await fs.writeFile(ignoredPath, 'This file should be ignored'); |
| 29 | + await fs.writeFile(dockerignorePath, 'ignored.txt\n'); |
| 30 | + |
| 31 | + // Upload directory as build context object (tgz) |
| 32 | + if (!contextDir) { |
| 33 | + throw new Error('Context directory not created before uploadFromDir'); |
| 34 | + } |
| 35 | + storageObject = await sdk.storageObject.uploadFromDir(contextDir, { |
| 36 | + name: uniqueName('sdk-context-object'), |
| 37 | + ttl_ms: 3600000, // 1 hour |
| 38 | + }); |
| 39 | + |
| 40 | + // Create blueprint that uses the uploaded object as build context |
| 41 | + blueprint = await sdk.blueprint.create( |
| 42 | + { |
| 43 | + name: uniqueName('sdk-blueprint-context'), |
| 44 | + dockerfile: `FROM ubuntu:22.04 |
| 45 | +WORKDIR /app |
| 46 | +COPY . .`, |
| 47 | + build_context: storageObject, |
| 48 | + }, |
| 49 | + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, |
| 50 | + ); |
| 51 | + |
| 52 | + expect(blueprint).toBeDefined(); |
| 53 | + expect(blueprint.id).toBeTruthy(); |
| 54 | + |
| 55 | + // Create devbox from the blueprint and verify copied files |
| 56 | + devbox = await blueprint.createDevbox({ |
| 57 | + name: uniqueName('devbox-from-context'), |
| 58 | + launch_parameters: { |
| 59 | + resource_size_request: 'X_SMALL', |
| 60 | + keep_alive_time_seconds: 60 * 5, // 5 minutes |
| 61 | + }, |
| 62 | + }); |
| 63 | + |
| 64 | + expect(devbox).toBeDefined(); |
| 65 | + expect(devbox.id).toBeTruthy(); |
| 66 | + |
| 67 | + // included.txt should be present |
| 68 | + const includeCheck = await devbox.cmd.exec('test -f /app/included.txt'); |
| 69 | + expect(includeCheck.exitCode).toBe(0); |
| 70 | + |
| 71 | + // Optionally assert content |
| 72 | + const includeContentResult = await devbox.cmd.exec('cat /app/included.txt'); |
| 73 | + const includeContent = await includeContentResult.stdout(); |
| 74 | + expect(includeContent.trim()).toBe('Hello from context'); |
| 75 | + |
| 76 | + // ignored.txt should be absent due to .dockerignore |
| 77 | + const ignoredCheck = await devbox.cmd.exec('test ! -f /app/ignored.txt'); |
| 78 | + expect(ignoredCheck.exitCode).toBe(0); |
| 79 | + |
| 80 | + // .dockerignore itself should also not be present in the image |
| 81 | + const dockerignoreCheck = await devbox.cmd.exec('test ! -f /app/.dockerignore'); |
| 82 | + expect(dockerignoreCheck.exitCode).toBe(0); |
| 83 | + } finally { |
| 84 | + if (devbox) { |
| 85 | + await devbox.shutdown().catch(() => {}); |
| 86 | + } |
| 87 | + if (blueprint) { |
| 88 | + await blueprint.delete().catch(() => {}); |
| 89 | + } |
| 90 | + if (storageObject) { |
| 91 | + await storageObject.delete().catch(() => {}); |
| 92 | + } |
| 93 | + if (contextDir) { |
| 94 | + await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {}); |
| 95 | + } |
| 96 | + } |
| 97 | + }, |
| 98 | + LONG_TIMEOUT, |
| 99 | + ); |
| 100 | + |
| 101 | + test.concurrent( |
| 102 | + 'creates blueprint with build_context_dir parameter (string path)', |
| 103 | + async () => { |
| 104 | + const fs = require('fs/promises'); |
| 105 | + const path = require('path'); |
| 106 | + const os = require('os'); |
| 107 | + |
| 108 | + let contextDir: string | undefined; |
| 109 | + let blueprint: Blueprint | undefined; |
| 110 | + let devbox: Devbox | undefined; |
| 111 | + |
| 112 | + try { |
| 113 | + // Create temporary build context directory |
| 114 | + contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-context-dir-')); |
| 115 | + const filePath = path.join(contextDir, 'hello.txt'); |
| 116 | + await fs.writeFile(filePath, 'Hello from build_context_dir!'); |
| 117 | + |
| 118 | + // Create blueprint using build_context_dir parameter (string form) |
| 119 | + // This tests the build_context_dir branch in Blueprint.create() |
| 120 | + if (!contextDir) { |
| 121 | + throw new Error('Context directory not created'); |
| 122 | + } |
| 123 | + blueprint = await sdk.blueprint.create( |
| 124 | + { |
| 125 | + name: uniqueName('sdk-blueprint-context-dir'), |
| 126 | + dockerfile: `FROM ubuntu:22.04 |
| 127 | +WORKDIR /app |
| 128 | +COPY . .`, |
| 129 | + build_context_dir: contextDir, |
| 130 | + }, |
| 131 | + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, |
| 132 | + ); |
| 133 | + |
| 134 | + expect(blueprint).toBeDefined(); |
| 135 | + expect(blueprint.id).toBeTruthy(); |
| 136 | + |
| 137 | + // Verify instance methods work |
| 138 | + const info = await blueprint.getInfo(); |
| 139 | + expect(info.id).toBe(blueprint.id); |
| 140 | + |
| 141 | + const logs = await blueprint.logs(); |
| 142 | + expect(logs.logs).toBeDefined(); |
| 143 | + |
| 144 | + // Create devbox to verify the build context was used |
| 145 | + devbox = await blueprint.createDevbox({ |
| 146 | + name: uniqueName('devbox-from-context-dir'), |
| 147 | + launch_parameters: { |
| 148 | + resource_size_request: 'X_SMALL', |
| 149 | + keep_alive_time_seconds: 60 * 5, |
| 150 | + }, |
| 151 | + }); |
| 152 | + |
| 153 | + expect(devbox).toBeDefined(); |
| 154 | + |
| 155 | + // Verify the file from build context is present |
| 156 | + const result = await devbox.cmd.exec('cat /app/hello.txt'); |
| 157 | + expect(result.exitCode).toBe(0); |
| 158 | + const content = await result.stdout(); |
| 159 | + expect(content.trim()).toBe('Hello from build_context_dir!'); |
| 160 | + } finally { |
| 161 | + if (devbox) { |
| 162 | + await devbox.shutdown().catch(() => {}); |
| 163 | + } |
| 164 | + if (blueprint) { |
| 165 | + await blueprint.delete().catch(() => {}); |
| 166 | + } |
| 167 | + if (contextDir) { |
| 168 | + await fs.rm(contextDir, { recursive: true, force: true }).catch(() => {}); |
| 169 | + } |
| 170 | + } |
| 171 | + }, |
| 172 | + LONG_TIMEOUT, |
| 173 | + ); |
| 174 | + }); |
| 175 | + |
| 176 | + describe('blueprint list and retrieval', () => { |
| 177 | + test.concurrent('list blueprints', async () => { |
| 178 | + const blueprints = await sdk.blueprint.list({ limit: 10 }); |
| 179 | + expect(Array.isArray(blueprints)).toBe(true); |
| 180 | + }); |
| 181 | + |
| 182 | + test.concurrent( |
| 183 | + 'get blueprint by ID', |
| 184 | + async () => { |
| 185 | + // First create a blueprint |
| 186 | + let blueprint: Blueprint | undefined; |
| 187 | + try { |
| 188 | + blueprint = await sdk.blueprint.create( |
| 189 | + { |
| 190 | + name: uniqueName('sdk-blueprint-retrieve'), |
| 191 | + dockerfile: 'FROM ubuntu:22.04', |
| 192 | + }, |
| 193 | + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, |
| 194 | + ); |
| 195 | + expect(blueprint?.id).toBeTruthy(); |
| 196 | + |
| 197 | + // Retrieve it byID |
| 198 | + const retrieved = sdk.blueprint.fromId(blueprint.id); |
| 199 | + expect(retrieved.id).toBe(blueprint.id); |
| 200 | + } finally { |
| 201 | + if (blueprint) { |
| 202 | + await blueprint.delete(); |
| 203 | + } |
| 204 | + } |
| 205 | + }, |
| 206 | + LONG_TIMEOUT, |
| 207 | + ); |
| 208 | + }); |
| 209 | + |
| 210 | + describe('blueprint with network policy', () => { |
| 211 | + test.concurrent( |
| 212 | + 'create blueprint with network_policy_id for build', |
| 213 | + async () => { |
| 214 | + let policy: NetworkPolicy | undefined; |
| 215 | + let blueprint: Blueprint | undefined; |
| 216 | + try { |
| 217 | + // Create a network policy for the build process |
| 218 | + policy = await sdk.networkPolicy.create({ |
| 219 | + name: uniqueName('sdk-policy-for-blueprint-build'), |
| 220 | + allow_all: true, // Allow all for build to download packages |
| 221 | + }); |
| 222 | + expect(policy.id).toBeTruthy(); |
| 223 | + |
| 224 | + // Create blueprint with network_policy_id at top level (for build) |
| 225 | + blueprint = await sdk.blueprint.create( |
| 226 | + { |
| 227 | + name: uniqueName('sdk-blueprint-with-build-policy'), |
| 228 | + dockerfile: 'FROM ubuntu:22.04\nRUN apt-get update', |
| 229 | + network_policy_id: policy.id, |
| 230 | + }, |
| 231 | + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, |
| 232 | + ); |
| 233 | + |
| 234 | + expect(blueprint).toBeDefined(); |
| 235 | + expect(blueprint.id).toBeTruthy(); |
| 236 | + |
| 237 | + // Verify blueprint was created |
| 238 | + const info = await blueprint.getInfo(); |
| 239 | + expect(info.id).toBe(blueprint.id); |
| 240 | + } finally { |
| 241 | + if (blueprint) { |
| 242 | + await blueprint.delete().catch(() => {}); |
| 243 | + } |
| 244 | + await cleanUpPolicy(policy); |
| 245 | + } |
| 246 | + }, |
| 247 | + LONG_TIMEOUT, |
| 248 | + ); |
| 249 | + |
| 250 | + test.concurrent( |
| 251 | + 'create blueprint with launch_parameters.network_policy_id', |
| 252 | + async () => { |
| 253 | + let policy: NetworkPolicy | undefined; |
| 254 | + let blueprint: Blueprint | undefined; |
| 255 | + let devbox: Devbox | undefined; |
| 256 | + try { |
| 257 | + // Create a network policy for devboxes launched from this blueprint |
| 258 | + policy = await sdk.networkPolicy.create({ |
| 259 | + name: uniqueName('sdk-policy-for-blueprint-launch'), |
| 260 | + allow_all: false, |
| 261 | + allowed_hostnames: ['github.com'], |
| 262 | + }); |
| 263 | + expect(policy.id).toBeTruthy(); |
| 264 | + |
| 265 | + // Create blueprint with launch_parameters including network_policy_id |
| 266 | + blueprint = await sdk.blueprint.create( |
| 267 | + { |
| 268 | + name: uniqueName('sdk-blueprint-with-launch-policy'), |
| 269 | + dockerfile: 'FROM ubuntu:22.04', |
| 270 | + launch_parameters: { |
| 271 | + resource_size_request: 'X_SMALL', |
| 272 | + keep_alive_time_seconds: 60 * 5, |
| 273 | + network_policy_id: policy.id, |
| 274 | + }, |
| 275 | + }, |
| 276 | + { longPoll: { timeoutMs: 10 * 60 * 1000 } }, |
| 277 | + ); |
| 278 | + |
| 279 | + expect(blueprint).toBeDefined(); |
| 280 | + expect(blueprint.id).toBeTruthy(); |
| 281 | + |
| 282 | + // Verify blueprint was created |
| 283 | + const info = await blueprint.getInfo(); |
| 284 | + expect(info.id).toBe(blueprint.id); |
| 285 | + |
| 286 | + // Create a devbox from the blueprint to verify it works |
| 287 | + devbox = await blueprint.createDevbox({ |
| 288 | + name: uniqueName('devbox-from-policy-blueprint'), |
| 289 | + }); |
| 290 | + expect(devbox).toBeDefined(); |
| 291 | + expect(devbox.id).toBeTruthy(); |
| 292 | + } finally { |
| 293 | + if (devbox) { |
| 294 | + await devbox.shutdown().catch(() => {}); |
| 295 | + } |
| 296 | + if (blueprint) { |
| 297 | + await blueprint.delete().catch(() => {}); |
| 298 | + } |
| 299 | + await cleanUpPolicy(policy); |
| 300 | + } |
| 301 | + }, |
| 302 | + LONG_TIMEOUT, |
| 303 | + ); |
| 304 | + }); |
| 305 | +}); |
0 commit comments