Repository navigation
Expand file tree
/
Copy pathFlywayDockerContainer.php
More file actions
285 lines (263 loc) · 12.3 KB
/
Copy pathFlywayDockerContainer.php
File metadata and controls
285 lines (263 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
<?php
declare(strict_types=1);
namespace TinyBlocks\DockerContainer;
use TinyBlocks\DockerContainer\Exceptions\ContainerExitUnsuccessful;
use TinyBlocks\DockerContainer\Exceptions\ContainerWaitTimeout;
use TinyBlocks\DockerContainer\Exceptions\DockerCommandExecutionFailed;
use TinyBlocks\DockerContainer\Exceptions\ImageNameEmpty;
use TinyBlocks\DockerContainer\Internal\Client\CommandLineDockerClient;
use TinyBlocks\DockerContainer\Internal\Containers\Drivers\Flyway\FlywaySettings;
use TinyBlocks\Time\Duration;
/**
* Immutable definition of a Flyway Docker container, whose commands each run Flyway once and block until it exits.
*
* <p>Each command runs in a fresh container with a unique generated name, removed once Flyway exits, so parallel
* test runs never collide on it. Every <code>with*</code> method returns a new definition and leaves the receiver
* untouched.</p>
*/
final readonly class FlywayDockerContainer
{
private const string MIGRATIONS_ON_CONTAINER = '/flyway/migrations';
private const int DEFAULT_TIMEOUT_IN_SECONDS = 120;
private function __construct(
private Duration $timeout,
private FlywaySettings $settings,
private GenericDockerContainer $container
) {
}
/**
* Creates a FlywayDockerContainer from a Flyway Docker image.
*
* @param string $image The Flyway Docker image name (e.g., "flyway/flyway:12-alpine").
* @param DockerClient $client The client that executes the Docker commands, the local docker executable by
* default.
* @return FlywayDockerContainer The created container definition.
* @throws ImageNameEmpty If the image name is empty.
*/
public static function from(
string $image,
DockerClient $client = new CommandLineDockerClient()
): FlywayDockerContainer {
return new FlywayDockerContainer(
timeout: Duration::fromSeconds(seconds: self::DEFAULT_TIMEOUT_IN_SECONDS),
settings: FlywaySettings::create(),
container: GenericDockerContainer::from(image: $image, client: $client)
);
}
/**
* Runs the Flyway repair command and blocks until Flyway exits.
*
* @return ExecutionCompleted The successful outcome, carrying the Flyway output.
* @throws DockerCommandExecutionFailed If a Docker command fails.
* @throws ContainerWaitTimeout If the MySQL source is not ready or Flyway does not exit within the timeout.
* @throws ContainerExitUnsuccessful If Flyway exits with a non-zero status, carrying the Flyway error output.
*/
public function repair(): ExecutionCompleted
{
return $this->settings
->applyTo(container: $this->container)
->runToCompletion(timeout: $this->timeout, commands: ['repair']);
}
/**
* Runs the Flyway migrate command and blocks until Flyway exits.
*
* @return ExecutionCompleted The successful outcome, carrying the Flyway output.
* @throws DockerCommandExecutionFailed If a Docker command fails.
* @throws ContainerWaitTimeout If the MySQL source is not ready or Flyway does not exit within the timeout.
* @throws ContainerExitUnsuccessful If Flyway exits with a non-zero status, carrying the Flyway error output.
*/
public function migrate(): ExecutionCompleted
{
return $this->settings
->applyTo(container: $this->container)
->runToCompletion(timeout: $this->timeout, commands: ['migrate']);
}
/**
* Runs the Flyway validate command and blocks until Flyway exits.
*
* @return ExecutionCompleted The successful outcome, carrying the Flyway output.
* @throws DockerCommandExecutionFailed If a Docker command fails.
* @throws ContainerWaitTimeout If the MySQL source is not ready or Flyway does not exit within the timeout.
* @throws ContainerExitUnsuccessful If Flyway exits with a non-zero status, including a failed validation.
*/
public function validate(): ExecutionCompleted
{
return $this->settings
->applyTo(container: $this->container)
->runToCompletion(timeout: $this->timeout, commands: ['validate']);
}
/**
* Returns a copy of the FlywayDockerContainer with the history table replaced.
*
* @param string $table The history table name, "schema_history" by default once withSource() is called.
* @return FlywayDockerContainer The copy with the new history table.
*/
public function withTable(string $table): FlywayDockerContainer
{
return $this->withEnvironmentVariable(key: 'FLYWAY_TABLE', value: $table);
}
/**
* Returns a copy of the FlywayDockerContainer with the target schema replaced.
*
* @param string $schema The schema name, the source database by default once withSource() is called.
* @return FlywayDockerContainer The copy with the new target schema.
*/
public function withSchema(string $schema): FlywayDockerContainer
{
return $this->withEnvironmentVariable(key: 'FLYWAY_SCHEMAS', value: $schema);
}
/**
* Returns a copy of the FlywayDockerContainer that migrates the database of a MySQL container.
*
* <p>The MySQL container is started by the first command that runs, through
* {@see MySQLDockerContainer::runIfNotExists()}, so it is reused when a container with its name is already
* running. Defining the source starts nothing. Flyway then connects with the JDBC URL of the started container,
* the root credentials (from <code>MYSQL_ROOT_PASSWORD</code>), the target schema (from
* <code>MYSQL_DATABASE</code>) and the history table (<code>schema_history</code>). withCredentials(),
* withSchema() and withTable() override these defaults, whether they are called before or after this method.</p>
*
* @param MySQLDockerContainer $container The MySQL container definition holding the database to migrate.
* @return FlywayDockerContainer The copy bound to the MySQL container.
*/
public function withSource(MySQLDockerContainer $container): FlywayDockerContainer
{
return new FlywayDockerContainer(
timeout: $this->timeout,
settings: $this->settings->withSource(source: $container),
container: $this->container
);
}
/**
* Returns a copy of the FlywayDockerContainer with the Docker network replaced.
*
* @param string $name The name of the Docker network.
* @return FlywayDockerContainer The copy with the new network.
*/
public function withNetwork(string $name): FlywayDockerContainer
{
return new FlywayDockerContainer(
timeout: $this->timeout,
settings: $this->settings,
container: $this->container->withNetwork(name: $name)
);
}
/**
* Returns a copy of the FlywayDockerContainer with the timeout of each command replaced.
*
* <p>The timeout bounds how long a command waits for Flyway to exit. It defaults to 120 seconds.</p>
*
* @param Duration $timeout The longest time a command waits for Flyway to exit.
* @return FlywayDockerContainer The copy with the new timeout.
*/
public function withTimeout(Duration $timeout): FlywayDockerContainer
{
return new FlywayDockerContainer(timeout: $timeout, settings: $this->settings, container: $this->container);
}
/**
* Returns a copy of the FlywayDockerContainer that applies the migrations of a host directory.
*
* <p>The directory is copied into the container at <code>/flyway/migrations</code> before Flyway starts.</p>
*
* @param string $pathOnHost The absolute path on the host holding the migration files.
* @return FlywayDockerContainer The copy with the migrations.
*/
public function withMigrations(string $pathOnHost): FlywayDockerContainer
{
$template = 'filesystem:%s';
return new FlywayDockerContainer(
timeout: $this->timeout,
settings: $this->settings->with(
key: 'FLYWAY_LOCATIONS',
value: sprintf($template, self::MIGRATIONS_ON_CONTAINER)
),
container: $this->container->copyToContainer(
pathOnHost: $pathOnHost,
pathOnContainer: self::MIGRATIONS_ON_CONTAINER
)
);
}
/**
* Runs the Flyway clean command followed by migrate, and blocks until Flyway exits.
*
* <p>Enables the Flyway clean command for this run, which Flyway disables by default. The migrations are
* applied when this method returns.</p>
*
* @return ExecutionCompleted The successful outcome, carrying the Flyway output.
* @throws DockerCommandExecutionFailed If a Docker command fails.
* @throws ContainerWaitTimeout If the MySQL source is not ready or Flyway does not exit within the timeout.
* @throws ContainerExitUnsuccessful If Flyway exits with a non-zero status, carrying the Flyway error output.
*/
public function cleanAndMigrate(): ExecutionCompleted
{
return $this->settings
->applyTo(container: $this->container)
->withEnvironmentVariable(key: 'FLYWAY_CLEAN_DISABLED', value: 'false')
->runToCompletion(timeout: $this->timeout, commands: ['clean', 'migrate']);
}
/**
* Returns a gated view of the FlywayDockerContainer whose commands run only while an environment
* variable is enabled.
*
* <p>The variable is enabled when its value is <code>1</code>, and it is read each time a command is called.
* While it is not enabled, a command returns <code>null</code> without issuing any Docker command, so neither
* Flyway nor its MySQL source is started. This keeps the migrations of a test run in the single process that
* sets the variable.</p>
*
* @param string $variable The name of the environment variable that enables the commands.
* @return FlywayContainerGated The gated view of this definition.
*/
public function onlyWhenEnabled(string $variable): FlywayContainerGated
{
return FlywayContainerGated::from(flyway: $this, variable: $variable);
}
/**
* Returns a copy of the FlywayDockerContainer with the database credentials replaced.
*
* @param string $password The database password.
* @param string $username The database username.
* @return FlywayDockerContainer The copy with the new credentials.
*/
public function withCredentials(string $password, string $username): FlywayDockerContainer
{
return $this
->withEnvironmentVariable(key: 'FLYWAY_USER', value: $username)
->withEnvironmentVariable(key: 'FLYWAY_PASSWORD', value: $password);
}
/**
* Returns a copy of the FlywayDockerContainer with the number of database connection retries replaced.
*
* @param int $retries The number of connection retries, one second apart.
* @return FlywayDockerContainer The copy with the new number of retries.
*/
public function withConnectRetries(int $retries): FlywayDockerContainer
{
return $this->withEnvironmentVariable(key: 'FLYWAY_CONNECT_RETRIES', value: (string)$retries);
}
/**
* Returns a copy of the FlywayDockerContainer with an environment variable added.
*
* <p>Any Flyway setting can be configured through its <code>FLYWAY_*</code> environment variable.</p>
*
* @param string $key The environment variable name.
* @param string $value The environment variable value.
* @return FlywayDockerContainer The copy with the added environment variable.
*/
public function withEnvironmentVariable(string $key, string $value): FlywayDockerContainer
{
return new FlywayDockerContainer(
timeout: $this->timeout,
settings: $this->settings->with(key: $key, value: $value),
container: $this->container
);
}
/**
* Returns a copy of the FlywayDockerContainer that rejects migrations whose file names break the Flyway
* naming convention, which Flyway otherwise skips silently.
*
* @return FlywayDockerContainer The copy with migration naming validation enabled.
*/
public function withMigrationNamingValidation(): FlywayDockerContainer
{
return $this->withEnvironmentVariable(key: 'FLYWAY_VALIDATE_MIGRATION_NAMING', value: 'true');
}
}