-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathApp.java
More file actions
807 lines (672 loc) · 34.8 KB
/
Copy pathApp.java
File metadata and controls
807 lines (672 loc) · 34.8 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
/*
* Copyright 2023-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* This Java source file was generated by the Gradle 'init' task.
*/
package io.seqera.wave.cli;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import io.seqera.wave.api.BuildCompression;
import io.seqera.wave.api.BuildContext;
import io.seqera.wave.api.ContainerConfig;
import io.seqera.wave.api.ContainerInspectRequest;
import io.seqera.wave.api.ContainerInspectResponse;
import io.seqera.wave.api.ContainerLayer;
import io.seqera.wave.api.ContainerStatusResponse;
import io.seqera.wave.api.ImageNameStrategy;
import io.seqera.wave.api.PackagesSpec;
import io.seqera.wave.api.ScanLevel;
import io.seqera.wave.api.ScanMode;
import io.seqera.wave.api.ServiceInfo;
import io.seqera.wave.api.SubmitContainerTokenRequest;
import io.seqera.wave.api.SubmitContainerTokenResponse;
import io.seqera.wave.cli.exception.BadClientResponseException;
import io.seqera.wave.cli.exception.ClientConnectionException;
import io.seqera.wave.cli.exception.IllegalCliArgumentException;
import io.seqera.wave.cli.exception.ReadyTimeoutException;
import io.seqera.wave.cli.json.JsonHelper;
import io.seqera.wave.cli.model.ContainerInspectResponseEx;
import io.seqera.wave.cli.model.SubmitContainerTokenResponseEx;
import io.seqera.wave.cli.util.BuildInfo;
import io.seqera.wave.cli.util.CliVersionProvider;
import io.seqera.wave.cli.util.DurationConverter;
import io.seqera.wave.cli.util.YamlHelper;
import io.seqera.wave.config.CondaOpts;
import io.seqera.wave.config.CranOpts;
import io.seqera.wave.config.PixiOpts;
import io.seqera.wave.util.DockerIgnoreFilter;
import io.seqera.wave.util.Packer;
import org.apache.commons.lang3.StringUtils;
import org.semver4j.Semver;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import static io.seqera.wave.cli.util.Checkers.isEmpty;
import static io.seqera.wave.cli.util.Checkers.isEnvVar;
import static picocli.CommandLine.Command;
import static picocli.CommandLine.Option;
/**
* Wave cli main class
*/
@Command(name = "wave",
description = "Wave command line tool",
mixinStandardHelpOptions = true,
versionProvider = CliVersionProvider.class,
usageHelpAutoWidth = true)
public class App implements Runnable {
static public final String DEFAULT_CONDA_CHANNELS = "conda-forge,bioconda";
private static final org.slf4j.Logger log = LoggerFactory.getLogger(App.class);
private static final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
private static final String DEFAULT_TOWER_ENDPOINT = "https://api.cloud.seqera.io";
private static final List<String> VALID_PLATFORMS = List.of("amd64", "x86_64", "linux/amd64", "linux/x86_64", "arm64", "linux/arm64");
private static final long _1MB = 1024 * 1024;
@Option(names = {"-i", "--image"}, paramLabel = "''", description = "Container image name to be provisioned e.g alpine:latest.")
private String image;
@Option(names = {"-f", "--containerfile"}, paramLabel = "''", description = "Container file to be used to build the image e.g. ./Dockerfile.")
private String containerFile;
@Option(names = {"--tower-token"}, paramLabel = "''", description = "Tower service access token.")
private String towerToken;
@Option(names = {"--tower-endpoint"}, paramLabel = "''", description = "Tower service endpoint e.g. https://api.cloud.seqera.io.")
private String towerEndpoint;
@Option(names = {"--tower-workspace-id"}, paramLabel = "''", description = "Tower service workspace ID e.g. 1234567890.")
private Long towerWorkspaceId;
@Option(names = {"--build-repo", "--build-repository"}, paramLabel = "''", description = "The container repository where image build by Wave will stored e.g. docker.io/user/build.")
private String buildRepository;
@Option(names = {"--cache-repo", "--cache-repository"}, paramLabel = "''", description = "The container repository where image layer created by Wave will stored e.g. docker.io/user/cache.")
private String cacheRepository;
@Option(names = {"--wave-endpoint"}, paramLabel = "''", description = "Wave service endpoint e.g. https://wave.seqera.io.")
private String waveEndpoint;
@Option(names = {"--freeze", "-F"}, paramLabel = "false", description = "Request a container freeze.")
private boolean freeze;
@Option(names = {"--platform"}, paramLabel = "''", description = "Platform to be used for the container build. One of: linux/amd64, linux/arm64.")
private String platform;
@Option(names = {"--await"}, paramLabel = "false", arity = "0..1", description = "Await the container build to be available. you can provide a timeout like --await 10m or 2s, by default its 15 minutes.")
private Duration await;
@Option(names = {"--context"}, paramLabel = "''", description = "Directory path where the build context is stored e.g. /some/context/path.")
private String contextDir;
@Option(names = {"--layer"}, paramLabel = "''", description = "Directory path where a layer content is stored e.g. /some/layer/path")
private List<String> layerDirs;
@Option(names = {"--config-env"}, paramLabel = "''", description = "Overwrite the environment of the image e.g. NAME=VALUE")
private List<String> environment;
@Option(names = {"--config-cmd"}, paramLabel = "''", description = "Overwrite the default CMD (command) of the image.")
private String command;
@Option(names = {"--config-entrypoint"}, paramLabel = "''", description = "Overwrite the default ENTRYPOINT of the image.")
private String entrypoint;
@Option(names = {"--config-file"}, paramLabel = "''", description = "Configuration file in JSON format to overwrite the default configuration of the image.")
private String configFile;
@Option(names = {"--config-working-dir"}, paramLabel = "''", description = "Overwrite the default WORKDIR of the image e.g. /some/work/dir.")
private String workingDir;
@Option(names = {"--conda-file"}, paramLabel = "''", description = "A Conda file used to build the container e.g. /some/path/conda.yaml.")
private String condaFile;
@Option(names = {"--conda-package", "--conda"}, paramLabel = "''", description = "One or more Conda packages used to build the container e.g. bioconda::samtools=1.17.")
private List<String> condaPackages;
@Option(names = {"--conda-base-image"}, paramLabel = "''", description = "Conda base image used to to build the container (default: ${DEFAULT-VALUE}).")
private String condaBaseImage = CondaOpts.DEFAULT_MAMBA_IMAGE;
@Option(names = {"--conda-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.")
private List<String> condaRunCommands;
@Option(names = {"--conda-channels"}, paramLabel = "''", description = "Conda channels used to build the container (default: ${DEFAULT-VALUE}).")
private String condaChannels = DEFAULT_CONDA_CHANNELS;
@Option(names = {"--cran-package", "--cran"}, paramLabel = "''", description = "One or more CRAN packages used to build the container e.g. dplyr=1.1.0.")
private List<String> cranPackages;
@Option(names = {"--cran-base-image"}, paramLabel = "''", description = "R base image used to build the container (default: ${DEFAULT-VALUE}).")
private String cranBaseImage = CranOpts.DEFAULT_R_IMAGE;
@Option(names = {"--cran-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.")
private List<String> cranRunCommands;
@Option(names = {"--pixi-image"}, paramLabel = "''", description = "Pixi builder image used to build the container (default: ${DEFAULT-VALUE}).")
private String pixiImage = PixiOpts.DEFAULT_PIXI_IMAGE;
@Option(names = {"--pixi-base-image"}, paramLabel = "''", description = "Base image for the final Pixi container (default: ${DEFAULT-VALUE}).")
private String pixiBaseImage = PixiOpts.DEFAULT_BASE_IMAGE;
@Option(names = {"--pixi-base-packages"}, paramLabel = "''", description = "Base packages to be installed in the Pixi container (default: ${DEFAULT-VALUE}).")
private String pixiBasePackages = PixiOpts.DEFAULT_PACKAGES;
@Option(names = {"--pixi-run-command"}, paramLabel = "''", description = "Dockerfile RUN commands used to build the container.")
private List<String> pixiRunCommands;
@Option(names = {"--log-level"}, paramLabel = "''", description = "Set the application log level. One of: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL")
private String logLevel;
@Option(names = {"-o","--output"}, paramLabel = "json|yaml", description = "Output format. One of: json, yaml.")
private String outputFormat;
@Option(names = {"-s","--singularity"}, paramLabel = "false", description = "Enable Singularity build (experimental)")
private boolean singularity;
@Option(names = {"--dry-run"}, paramLabel = "false", description = "Simulate a request switching off the build container images")
private boolean dryRun;
@Option(names = {"--preserve-timestamp"}, paramLabel = "false", description = "Preserve timestamp of files in the build context and layers created by Wave")
private boolean preserveTimestamp;
@Option(names = {"--info"}, paramLabel = "false", description = "Show Wave client & service information")
private boolean info;
private BuildContext buildContext;
private ContainerConfig containerConfig;
@Option(names = {"--inspect"}, paramLabel = "false", description = "Inspect specified container image")
private boolean inspect;
@Option(names = {"--include"}, paramLabel = "''", description = "Include one or more containers in the specified base image")
private List<String> includes;
@Option(names = {"--name-strategy"}, paramLabel = "<value>", description = "Specify the name strategy for the container name, it can be 'none' or 'tagPrefix' or 'imageSuffix'")
private ImageNameStrategy nameStrategy;
@Option(names = {"-m","--mirror"}, paramLabel = "false", description = "Enable container mirror mode")
private boolean mirror;
@Option(names = {"--scan-mode"}, paramLabel = "<value>", description = "Specify container security scan mode, it can be 'none', 'async' or 'required'")
private ScanMode scanMode;
@Option(names = {"--scan-level"}, paramLabel = "<value>", description = "Specify one or more security scan vulnerabilities level allowed in the container e.g. low,medium,high,critical")
private List<ScanLevel> scanLevels;
@Option(names = {"--build-compression"}, paramLabel = "<value>", description = "Specify the compression algorithm to be used for the build context, it can be 'gzip', 'zstd' or 'estargz'")
private BuildCompression.Mode buildCompression;
@Option(names = {"--build-template"}, paramLabel = "<value>", description = "Specify the build template to be used for building the container, e.g. 'conda/pixi:v1', 'conda/micromamba:v1', 'conda/micromamba:v2', 'cran/installr:v1'")
private String buildTemplate;
public static void main(String[] args) {
try {
final App app = new App();
final CommandLine cli = new CommandLine(app);
//register duration converter
cli.registerConverter(Duration.class, new DurationConverter());
// add examples in help
cli
.getCommandSpec()
.usageMessage()
.footer(readExamples("usage-examples.txt"));
final CommandLine.ParseResult result = cli.parseArgs(args);
if( result.matchedArgs().size()==0 || result.isUsageHelpRequested() ) {
cli.usage(System.out);
}
else if( result.isVersionHelpRequested() ) {
System.out.println(BuildInfo.getFullVersion());
return;
}
app.setLogLevel();
app.defaultArgs();
if( app.info ) {
app.printInfo();
}
else if( app.inspect ) {
app.inspect();
}
else {
app.run();
}
}
catch (IllegalCliArgumentException | CommandLine.ParameterException | BadClientResponseException |
ReadyTimeoutException | ClientConnectionException e) {
System.err.println(e.getMessage());
System.exit(1);
}
catch (Throwable e) {
e.printStackTrace(System.err);
System.exit(1);
}
}
private static String readExamples(String exampleFile) {
try(InputStream stream = App.class.getResourceAsStream(exampleFile)) {
return new String(stream.readAllBytes());
}
catch (Exception e) {
throw new IllegalStateException("Unable to read usge examples", e);
}
}
protected void setLogLevel() {
if( !isEmpty(logLevel) ) {
Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.valueOf(logLevel));
}
}
protected void defaultArgs() {
if( "null".equals(towerEndpoint) ) {
towerEndpoint = null;
}
else if( isEmpty(towerEndpoint) && System.getenv().containsKey("TOWER_API_ENDPOINT") ) {
towerEndpoint = System.getenv("TOWER_API_ENDPOINT");
}
else if( isEmpty(towerEndpoint) ) {
towerEndpoint = DEFAULT_TOWER_ENDPOINT;
}
if( "null".equals(towerToken) ) {
towerToken = null;
}
else if( isEmpty(towerToken) && System.getenv().containsKey("TOWER_ACCESS_TOKEN") ) {
towerToken = System.getenv("TOWER_ACCESS_TOKEN");
}
if( towerWorkspaceId==null && System.getenv().containsKey("TOWER_WORKSPACE_ID") ) {
towerWorkspaceId = Long.valueOf(System.getenv("TOWER_WORKSPACE_ID"));
}
if( isEmpty(waveEndpoint) && System.getenv().containsKey("WAVE_ENDPOINT") ) {
waveEndpoint = System.getenv("WAVE_ENDPOINT");
}
else if( isEmpty(waveEndpoint) ) {
waveEndpoint = Client.DEFAULT_ENDPOINT;
}
if ( Duration.ofMinutes(15).equals(await) && !isEmpty(towerToken) ) {
await = Duration.ofMinutes(25);
}
}
protected void validateArgs() {
if( !isEmpty(image) && !isEmpty(containerFile) )
throw new IllegalCliArgumentException("Argument --image and --containerfile conflict each other - Specify an image name or a container file for the container to be provisioned");
if( isEmpty(image) && isEmpty(containerFile) && isEmpty(condaFile) && condaPackages==null && cranPackages==null )
throw new IllegalCliArgumentException("Provide either a image name or a container file for the Wave container to be provisioned");
if( isEmpty(towerToken) && !isEmpty(buildRepository) )
throw new IllegalCliArgumentException("Specify the Tower access token required to authenticate the access to the build repository either by using the --tower-token option or the TOWER_ACCESS_TOKEN environment variable");
// -- check conda options
if( !isEmpty(condaFile) && condaPackages!=null )
throw new IllegalCliArgumentException("Option --conda-file and --conda-package conflict each other");
if( !isEmpty(condaFile) && !isEmpty(image) )
throw new IllegalCliArgumentException("Option --conda-file and --image conflict each other");
if( !isEmpty(condaFile) && !isEmpty(containerFile) )
throw new IllegalCliArgumentException("Option --conda-file and --containerfile conflict each other");
if( condaPackages!=null && !isEmpty(image) )
throw new IllegalCliArgumentException("Option --conda-package and --image conflict each other");
if( condaPackages!=null && !isEmpty(containerFile) )
throw new IllegalCliArgumentException("Option --conda-package and --containerfile conflict each other");
// -- check CRAN options
if( cranPackages!=null && !isEmpty(image) )
throw new IllegalCliArgumentException("Option --cran-package and --image conflict each other");
if( cranPackages!=null && !isEmpty(containerFile) )
throw new IllegalCliArgumentException("Option --cran-package and --containerfile conflict each other");
// -- mutual exclusivity between conda and CRAN
if( !isEmpty(condaFile) && cranPackages!=null )
throw new IllegalCliArgumentException("Option --conda-file and --cran-package conflict each other - conda packages and CRAN packages cannot be specified in the same command");
if( condaPackages!=null && cranPackages!=null )
throw new IllegalCliArgumentException("Option --conda-package and --cran-package conflict each other - conda packages and CRAN packages cannot be specified in the same command");
if( !isEmpty(outputFormat) && !List.of("json","yaml").contains(outputFormat) ) {
final String msg = String.format("Invalid output format: '%s' - expected value: json, yaml", outputFormat);
throw new IllegalCliArgumentException(msg);
}
if( !isEmpty(condaFile) && !Files.exists(Path.of(condaFile)) )
throw new IllegalCliArgumentException("The specified Conda file path cannot be accessed - offending file path: " + condaFile);
if( !isEmpty(contextDir) && isEmpty(containerFile) )
throw new IllegalCliArgumentException("Option --context requires the use of a container file");
if( singularity && !freeze )
throw new IllegalCliArgumentException("Singularity build requires enabling freeze mode");
if( !isEmpty(contextDir) ) {
// check that a container file has been provided
if( isEmpty(containerFile) )
throw new IllegalCliArgumentException("Container context directory is only allowed when a build container file is provided");
Path location = Path.of(contextDir);
// check it exist
if( !Files.exists(location) )
throw new IllegalCliArgumentException("Context path does not exists - offending value: " + contextDir);
// check it's a directory
if( !Files.isDirectory(location) )
throw new IllegalCliArgumentException("Context path is not a directory - offending value: " + contextDir);
}
if( mirror && !isEmpty(containerFile) )
throw new IllegalCliArgumentException("Argument --mirror and --containerfile conflict each other");
if( mirror && !isEmpty(condaFile) )
throw new IllegalCliArgumentException("Argument --mirror and --conda-file conflict each other");
if( mirror && !isEmpty(condaPackages) )
throw new IllegalCliArgumentException("Argument --mirror and --conda-package conflict each other");
if( mirror && !isEmpty(contextDir) )
throw new IllegalCliArgumentException("Argument --mirror and --context conflict each other");
if( mirror && freeze )
throw new IllegalCliArgumentException("Argument --mirror and --freeze conflict each other");
if( mirror && isEmpty(buildRepository) )
throw new IllegalCliArgumentException("Option --mirror and requires the use of a build repository");
if( dryRun && await != null )
throw new IllegalCliArgumentException("Options --dry-run and --await conflicts each other");
if( !isEmpty(platform) && !VALID_PLATFORMS.contains(platform) )
throw new IllegalCliArgumentException(String.format("Unsupported container platform: '%s'", platform));
}
protected Client client() {
return new Client().withEndpoint(waveEndpoint);
}
protected SubmitContainerTokenRequest createRequest() {
return new SubmitContainerTokenRequest()
.withContainerImage(image)
.withContainerFile(containerFileBase64())
.withPackages(packagesSpec())
.withContainerPlatform(platform)
.withTimestamp(OffsetDateTime.now())
.withBuildRepository(buildRepository)
.withCacheRepository(cacheRepository)
.withBuildContext(buildContext)
.withContainerConfig(containerConfig)
.withTowerAccessToken(towerToken)
.withTowerWorkspaceId(towerWorkspaceId)
.withTowerEndpoint(towerEndpoint)
.withFormat( singularity ? "sif" : null )
.withFreezeMode(freeze)
.withDryRun(dryRun)
.withContainerIncludes(includes)
.withNameStrategy(nameStrategy)
.withMirror(mirror)
.withScanMode(scanMode)
.withScanLevels(scanLevels)
.withBuildCompression(compression(buildCompression))
.withBuildTemplate(buildTemplate)
;
}
BuildCompression compression(BuildCompression.Mode mode) {
if( mode==null )
return null;
return new BuildCompression().withMode(mode);
}
public void inspect() {
final Client client = client();
final ContainerInspectRequest req = new ContainerInspectRequest()
.withContainerImage(image)
.withTowerAccessToken(towerToken)
.withTowerWorkspaceId(towerWorkspaceId)
.withTowerEndpoint(towerEndpoint)
;
final ContainerInspectResponse resp = client.inspect(req);
System.out.println(dumpOutput(new ContainerInspectResponseEx(resp)));
}
@Override
public void run() {
// validate the command line args
validateArgs();
// prepare the request
buildContext = prepareContext();
containerConfig = prepareConfig();
// create the wave request
SubmitContainerTokenRequest request = createRequest();
// creat the client
final Client client = client();
// submit it
SubmitContainerTokenResponse resp = client.submit(request);
// await container request to be completed
if( await != null && resp.requestId!=null && resp.succeeded==null ) {
ContainerStatusResponse status = client.awaitCompletion(resp.requestId, await);
// print the wave container name
System.out.println(dumpOutput(new SubmitContainerTokenResponseEx(resp, status)));
}
else {
// print the wave container name
System.out.println(dumpOutput(resp));
}
}
private String encodePathBase64(String value) {
try {
if( isEmpty(value) )
return null;
// read the text from a URI resource and encode to base64
if( value.startsWith("file:/") || value.startsWith("http://") || value.startsWith("https://")) {
try(InputStream stream=new URI(value).toURL().openStream()) {
return Base64.getEncoder().encodeToString(stream.readAllBytes());
}
}
// read the text from a local file and encode to base64
return Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of(value)));
}
catch (URISyntaxException e) {
throw new IllegalCliArgumentException("Invalid container file URI path - offending value: " + value, e);
}
catch (NoSuchFileException | FileNotFoundException e) {
throw new IllegalCliArgumentException("File not found: " + value, e);
}
catch (IOException e) {
String msg = String.format("Unable to read resource: %s - reason: %s" + value, e.getMessage());
throw new IllegalCliArgumentException(msg, e);
}
}
private String encodeStringBase64(String value) {
if( isEmpty(value) )
return null;
else
return Base64.getEncoder().encodeToString(value.getBytes());
}
protected BuildContext prepareContext() {
if( isEmpty(contextDir) )
return null;
BuildContext result;
try {
if( isWindows )
log.warn("Build context file permission may not be honoured when using Windows OS");
//check for .dockerignore file in context directory
final Path dockerIgnorePath = Path.of(contextDir).resolve(".dockerignore");
final DockerIgnoreFilter filter = Files.exists(dockerIgnorePath)
? DockerIgnoreFilter.fromFile(dockerIgnorePath)
: null;
final Packer packer = new Packer()
.withFilter(filter)
.withPreserveFileTimestamp(preserveTimestamp);
result = BuildContext.of(packer.layer(Path.of(contextDir)));
}
catch (IOException e) {
throw new IllegalCliArgumentException("Unexpected error while preparing build context - cause: "+e.getMessage(), e);
}
if( result.gzipSize > 5*_1MB )
throw new IllegalCliArgumentException("Build context cannot be bigger of 5 MiB");
return result;
}
protected ContainerConfig prepareConfig() {
ContainerConfig result = new ContainerConfig();
// add configuration from config file if specified
if( configFile != null ){
if( "".equals(configFile.trim()) ) throw new IllegalCliArgumentException("The specified config file is an empty string");
result = readConfig(configFile);
}
// add the entrypoint if specified
if( entrypoint!=null )
result.entrypoint = List.of(entrypoint);
// add the command if specified
if( command != null ){
if( "".equals(command.trim()) ) throw new IllegalCliArgumentException("The command cannot be an empty string");
result.cmd = List.of(command);
}
//add environment variables if specified
if( environment!=null ) {
for( String it : environment ) {
if( !isEnvVar(it) ) throw new IllegalCliArgumentException("Invalid environment variable syntax - offending value: " + it);
}
result.env = environment;
}
//add the working directory if specified
if( workingDir != null ){
if( "".equals(workingDir.trim()) ) throw new IllegalCliArgumentException("The working directory cannot be empty string");
result.workingDir = workingDir;
}
// add the layers to the resulting config if specified
if( layerDirs!=null ) for( String it : layerDirs ) {
final Path loc = Path.of(it);
if( !Files.isDirectory(loc) ) throw new IllegalCliArgumentException("Not a valid container layer directory - offering path: "+loc);
ContainerLayer layer;
try {
layer = new Packer()
.withPreserveFileTimestamp(preserveTimestamp)
.layer(loc);
result.layers.add(layer);
}
catch (IOException e ) {
throw new RuntimeException("Unexpected error while packing container layer at path: " + loc, e);
}
if( layer.gzipSize > _1MB )
throw new RuntimeException("Container layer cannot be bigger of 1 MiB - offending path: " + loc);
}
// check all size
long size = 0;
for(ContainerLayer it : result.layers ) {
if( it.location.startsWith("data:"))
size += it.gzipSize;
}
if( size>=10 * _1MB )
throw new RuntimeException("Compressed container layers cannot exceed 10 MiB");
// return the result
return !result.empty() ? result : null;
}
private ContainerInspectRequest inspectRequest(String image) {
return new ContainerInspectRequest()
.withContainerImage(image)
.withTowerEndpoint(towerEndpoint)
.withTowerAccessToken(towerToken)
.withTowerWorkspaceId(towerWorkspaceId);
}
private CondaOpts condaOpts() {
return new CondaOpts()
.withMambaImage(condaBaseImage)
.withCommands(condaRunCommands)
;
}
private CranOpts cranOpts() {
return new CranOpts()
.withRImage(cranBaseImage)
.withCommands(cranRunCommands)
;
}
private PixiOpts pixiOpts() {
return new PixiOpts()
.withPixiImage(pixiImage)
.withBaseImage(pixiBaseImage)
.withBasePackages(pixiBasePackages)
.withCommands(pixiRunCommands)
;
}
protected String containerFileBase64() {
return !isEmpty(containerFile)
? encodePathBase64(containerFile)
: null;
}
protected PackagesSpec packagesSpec() {
if( !isEmpty(condaFile) ) {
return new PackagesSpec()
.withType(PackagesSpec.Type.CONDA)
.withCondaOpts(condaOpts())
.withPixiOpts(pixiOpts())
.withEnvironment(encodePathBase64(condaFile))
.withChannels(condaChannels())
;
}
if( !isEmpty(condaPackages) ) {
return new PackagesSpec()
.withType(PackagesSpec.Type.CONDA)
.withCondaOpts(condaOpts())
.withPixiOpts(pixiOpts())
.withEntries(condaPackages)
.withChannels(condaChannels())
;
}
if( !isEmpty(cranPackages) ) {
return new PackagesSpec()
.withType(PackagesSpec.Type.CRAN)
.withCranOpts(cranOpts())
.withEntries(cranPackages)
;
}
return null;
}
protected String dumpOutput(SubmitContainerTokenResponseEx resp) {
if( outputFormat==null && !resp.succeeded ) {
String message = "Container provisioning did not complete successfully";
if( !StringUtils.isEmpty(resp.reason) )
message += "\n- Reason: " + resp.reason;
if( !StringUtils.isEmpty(resp.detailsUri) )
message += "\n- Find out more here: " + resp.detailsUri;
throw new BadClientResponseException(message);
}
return dumpOutput((SubmitContainerTokenResponse)resp);
}
protected String dumpOutput(SubmitContainerTokenResponse resp) {
if( "yaml".equals(outputFormat) ) {
return YamlHelper.toYaml(resp);
}
if( "json".equals(outputFormat) ) {
return JsonHelper.toJson(resp);
}
if( outputFormat!=null )
throw new IllegalArgumentException("Unexpected output format: "+outputFormat);
return resp.targetImage;
}
protected String dumpOutput(ContainerInspectResponseEx resp) {
if( "json".equals(outputFormat) || outputFormat==null ) {
return JsonHelper.toJson(resp);
}
if( "yaml".equals(outputFormat) ) {
return YamlHelper.toYaml(resp);
}
throw new IllegalArgumentException("Unexpected output format: "+outputFormat);
}
protected ContainerConfig readConfig(String path) {
try {
if( path.startsWith("http://") || path.startsWith("https://") || path.startsWith("file:/")) {
try (InputStream stream=new URL(path).openStream()) {
return JsonHelper.fromJson(new String(stream.readAllBytes()), ContainerConfig.class);
}
}
else {
return JsonHelper.fromJson(Files.readString(Path.of(path)), ContainerConfig.class);
}
}
catch (FileNotFoundException | NoSuchFileException e) {
throw new IllegalCliArgumentException("Invalid container config file - File not found: " + path);
}
catch (IOException e) {
String msg = String.format("Unable to read container config file: %s - Cause: %s", path, e.getMessage());
throw new IllegalCliArgumentException(msg, e);
}
}
protected List<String> condaChannels() {
if( condaChannels==null )
return null;
// parse channels
return Arrays.stream(condaChannels.split("[, ]"))
.map(String::trim)
.filter(it -> !isEmpty(it))
.collect(Collectors.toList());
}
void printInfo() {
System.out.println(String.format("Client:"));
System.out.println(String.format(" Version : %s", BuildInfo.getVersion()));
System.out.println(String.format(" System : %s", System. getProperty("os.name")));
System.out.println(String.format("Server:"));
System.out.println(String.format(" Version : %s", serviceVersion()));
System.out.println(String.format(" Endpoint : %s", waveEndpoint));
}
protected String serviceVersion() {
return serviceVersion0(getServiceVersion(), "1.31.0");
}
protected String serviceVersion0(String current, String required) {
Semver current0 = new Semver(current);
Semver required0 = new Semver(required);
return current0.compareTo(required0) >= 0
? current
: current + " (required: " + required0 + ")";
}
protected String getServiceVersion() {
fixServiceInfoConstructor();
try {
return client().serviceInfo().version;
}
catch (Throwable e) {
log.debug("Unexpected error while retrieving Wave service info", e);
return "-";
}
}
private void fixServiceInfoConstructor() {
/*
dirty hack to force Graal compiler to recognise the access via reflection of the
ServiceInfo constructor
*/
try {
Class<?> myClass = ServiceInfo.class;
Constructor<?> constructor = myClass.getConstructor(String.class, String.class);
constructor.newInstance("Foo", "Bar");
}
catch (Exception e) {
log.debug("Unable to load constructor", e);
}
}
}