Repository navigation
Expand file tree
/
Copy pathdocker.go
More file actions
1350 lines (1201 loc) · 45.6 KB
/
Copy pathdocker.go
File metadata and controls
1350 lines (1201 loc) · 45.6 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
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package docker
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/drone-plugins/drone-buildx/config/docker"
"github.com/drone-plugins/drone-plugin-lib/drone"
)
type (
// Daemon defines Docker daemon parameters.
Daemon struct {
Registry string // Docker registry
Mirror string // Docker registry mirror
Insecure bool // Docker daemon enable insecure registries
StorageDriver string // Docker daemon storage driver
StoragePath string // Docker daemon storage path
Disabled bool // DOcker daemon is disabled (already running)
Debug bool // Docker daemon started in debug mode
Bip string // Docker daemon network bridge IP address
DNS []string // Docker daemon dns server
DNSSearch []string // Docker daemon dns search domain
MTU string // Docker daemon mtu setting
IPv6 bool // Docker daemon IPv6 networking
RegistryType drone.RegistryType // Docker registry type
ArtifactRegistry string // Docker registry where artifact can be viewed
RetryCount int // Number of retry attempts to reach Docker daemon
}
Builder struct {
Name string // Buildx builder name
DaemonConfig string // Buildx daemon config file path
Driver string // Buildx driver type
DriverOpts []string // Buildx driver opts
DriverOptsNew []string // Buildx driver opts new
RemoteConn string // Buildx remote connection endpoint
UseLoadedBuildkit bool // Use loaded buildkit or no
AssestsDir string // Assets directory
BuildkitVersion string // Buildkit version
BuildkitTLSHandshakeTimeout string // Buildkit TLS handshake timeout
BuildkitResponseHeaderTimeout string // Buildkit response header timeout
}
// Login defines Docker login parameters.
Login struct {
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
Config string // Docker Auth Config
AccessToken string // External Access Token
}
// Build defines Docker build parameters.
Build struct {
Remote string // Git remote URL
Name string // Docker build using default named tag
Dockerfile string // Docker build Dockerfile
Context string // Docker build context
Tags []string // Docker build tags
Args []string // Docker build args
ArgsEnv []string // Docker build args from env
ArgsNew []string // Docker build args with comma seperated values
IsMultipleBuildArgs bool // env variable for fall back
Target string // Docker build target
Squash bool // Docker build squash
Pull bool // Docker build pull
CacheFrom []string // Docker buildx cache-from
CacheTo []string // Docker buildx cache-to
CacheTlsInsecure bool // Docker buildx cache-tls-insecure
PathStyle bool // Docker buildx path-style for s3 DLC
Compress bool // Docker build compress
Repo string // Docker build repository
LabelSchema []string // label-schema Label map
AutoLabel bool // auto-label bool
Labels []string // Label map
Link string // Git repo link
NoCache bool // Docker build no-cache
Secret string // secret keypair
SecretEnvs []string // Docker build secrets with env var as source
SecretFiles []string // Docker build secrets with file as source
AddHost []string // Docker build add-host
Quiet bool // Docker build quiet
Platform string // Docker build platform
SSHAgentKey string // Docker build ssh agent key
SSHKeyPath string // Docker build ssh key path
BuildxLoad bool // Docker buildx --load
HarnessSelfHostedS3AccessKey string // Harness self-hosted s3 access key
HarnessSelfHostedS3SecretKey string // Harness self-hosted s3 secret key
HarnessSelfHostedGcpJsonKey string // Harness self hosted gcp json key
HarnessSelfHostedAzureAccountKey string // Harness self-hosted azure account key
HarnessSelfHostedAzureTenantID string // Harness self-hosted azure tenant id
HarnessSelfHostedAzureClientID string // Harness self-hosted azure client id
HarnessSelfHostedAzureClientSecret string // Harness self-hosted azure client secret
HarnessSelfHostedAzureOidcToken string // Harness self-hosted azure oidc token
BuildxOptions []string // Generic buildx options passed directly to the buildx command
BuildxOptionsSemicolon string // Buildx options separated by semicolons instead of commas
// Buildx Bake (opt-in)
BakeFile string // Buildx Bake definition file (HCL/JSON/Compose). If set, Bake mode is active
BakeOptions string // Semicolon-delimited Bake options and/or target names
}
// Plugin defines the Docker plugin parameters.
Plugin struct {
Login Login // Docker login configuration
Build Build // Docker build configuration
Builder Builder // Docker Buildx builder configuration
Daemon Daemon // Docker daemon configuration
Dryrun bool // Docker push is skipped
Cleanup bool // Docker purge is enabled
CardPath string // Card path to write file to
MetadataFile string // Location to write the metadata file
ArtifactFile string // Artifact path to write file to
CacheMetricsFile string // Location to write the cache metrics file
BaseImageRegistry string // Docker registry to pull base image
BaseImageUsername string // Docker registry username to pull base image
BaseImagePassword string // Docker registry password to pull base image
PushOnly bool // Push only mode, skips build process
SourceTarPath string // Path to Docker image tar file to load and push
TarPath string // Path to save Docker image as tar file
BuildxOutputFormat string // Buildx output format for direct tar output (docker, oci)
SourceImage string // Source image to push (optional)
BuildkitInheritAuth bool // Inherit auth from docker daemon
}
Card []struct {
ID string `json:"Id"`
RepoTags []string `json:"RepoTags"`
ParsedRepoTags []TagStruct `json:"ParsedRepoTags"`
RepoDigests []interface{} `json:"RepoDigests"`
Parent string `json:"Parent"`
Comment string `json:"Comment"`
Created time.Time `json:"Created"`
Container string `json:"Container"`
DockerVersion string `json:"DockerVersion"`
Author string `json:"Author"`
Architecture string `json:"Architecture"`
Os string `json:"Os"`
Size int `json:"Size"`
VirtualSize int `json:"VirtualSize"`
Metadata struct {
LastTagTime time.Time `json:"LastTagTime"`
} `json:"Metadata"`
SizeString string
VirtualSizeString string
Time string
URL string `json:"URL"`
}
TagStruct struct {
Tag string `json:"Tag"`
}
BuildKitConfig struct {
BuildkitVersion string `json:"buildkit_version"`
}
)
const (
v2HubRegistryURL string = "https://registry.hub.docker.com/v2/"
v1RegistryURL string = "https://index.docker.io/v1/" // Default registry
v2RegistryURL string = "https://index.docker.io/v2/" // v2 registry is not supported
)
// Exec executes the plugin step
func (p Plugin) Exec() error {
// start the Docker daemon server
if !p.Daemon.Disabled {
p.startDaemon()
}
// poll the docker daemon until it is started. This ensures the daemon is
// ready to accept connections before we proceed.
maxAttempts := p.Daemon.RetryCount
if maxAttempts <= 0 {
maxAttempts = 15 // default value
}
for i := 0; ; i++ {
cmd := commandInfo()
err := cmd.Run()
if err == nil {
break
}
if i == maxAttempts-1 {
fmt.Printf("Unable to reach Docker Daemon after %d attempts.\n", maxAttempts)
break
}
time.Sleep(time.Second * 1)
}
// for debugging purposes, log the type of authentication
// credentials that have been provided.
switch {
case p.Login.Password != "" && p.Login.Config != "":
fmt.Println("Detected registry credentials and registry credentials file")
case p.Login.Password != "":
fmt.Println("Detected registry credentials")
case p.Login.Config != "":
fmt.Println("Detected registry credentials file")
case p.Login.AccessToken != "":
fmt.Println("Detected access token")
default:
fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.")
}
// create Auth Config File
if p.Login.Config != "" {
os.MkdirAll(dockerHome, 0600)
path := filepath.Join(dockerHome, "config.json")
var content []byte
// Try to parse as JSON first to determine if it's content or file path
var jsonTest interface{}
if json.Unmarshal([]byte(p.Login.Config), &jsonTest) == nil {
// Valid JSON, treat as content
content = []byte(p.Login.Config)
} else if _, err := os.Stat(p.Login.Config); err == nil {
// Not JSON but file exists, read it
data, err := os.ReadFile(p.Login.Config)
if err != nil {
return fmt.Errorf("Error reading docker config file '%s': %s", p.Login.Config, err)
}
content = data
} else {
// Neither valid JSON nor existing file
return fmt.Errorf("Docker config must be either valid JSON content or a path to an existing config file")
}
if err := os.WriteFile(path, content, 0600); err != nil {
return fmt.Errorf("Error writing config.json: %s", err)
}
}
// add base image docker credentials to the existing config file, else create new
// instead of writing to config file directly, using docker's login func
if p.BaseImageRegistry != "" {
if p.BaseImageUsername == "" {
fmt.Printf("Username cannot be empty. The base image connector requires authenticated access. Please either use an authenticated connector, or remove the base image connector.")
}
if p.BaseImagePassword == "" {
fmt.Printf("Password cannot be empty. The base image connector requires authenticated access. Please either use an authenticated connector, or remove the base image connector.")
}
var baseConnectorLogin Login
baseConnectorLogin.Registry = p.BaseImageRegistry
baseConnectorLogin.Username = p.BaseImageUsername
baseConnectorLogin.Password = p.BaseImagePassword
cmd := commandLogin(baseConnectorLogin)
raw, err := cmd.CombinedOutput()
if err != nil {
out := string(raw)
out = strings.Replace(out, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.", "", -1)
fmt.Println(out)
return fmt.Errorf("Error authenticating base connector: exit status 1")
}
} else {
fmt.Println("\033[33mTo ensure consistent and reliable pipeline execution, we recommend setting up a Base Image Connector.\033[0m\n" +
"\033[33mWhile optional at this time, configuring it helps prevent failures caused by Docker Hub's rate limits.\033[0m")
}
// login to the Docker registry
if p.Login.Password != "" {
cmd := commandLogin(p.Login)
raw, err := cmd.CombinedOutput()
if err != nil {
out := string(raw)
out = strings.Replace(out, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.", "", -1)
fmt.Println(out)
return fmt.Errorf("Error authenticating: exit status 1")
}
} else if p.Login.AccessToken != "" {
cmd := commandLoginAccessToken(p.Login, p.Login.AccessToken)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error logging in to Docker registry: %s", err)
}
if strings.Contains(string(output), "Login Succeeded") {
fmt.Println("Login successful")
} else {
return fmt.Errorf("login did not succeed")
}
}
// cache export feature is currently not supported for docker driver hence we have to create docker-container driver
// NOTE: skip this auto-switch when Bake mode is active
if p.Build.BakeFile == "" && len(p.Build.CacheTo) > 0 && (p.Builder.Driver == "" || p.Builder.Driver == defaultDriver) {
p.Builder.Driver = dockerContainerDriver
}
loadedBuildkitVersion := true
loadedBuildkitTarball := true
var config BuildKitConfig
if p.Builder.UseLoadedBuildkit {
// Build paths for the version.json and buildkit.tar
versionFilePath := filepath.Join(p.Builder.AssestsDir, "version.json")
tarballFilePath := filepath.Join(p.Builder.AssestsDir, "buildkit.tar")
// Read the version.json file from the filesystem
configData, err := os.ReadFile(versionFilePath)
if err != nil {
loadedBuildkitVersion = false
} else if err := json.Unmarshal(configData, &config); err != nil {
loadedBuildkitVersion = false
}
// Read the tarball file from the filesystem
data, err := os.ReadFile(tarballFilePath)
if err != nil {
loadedBuildkitTarball = false
} else {
loadCmd := commandLoad()
loadCmd.Stdin = bytes.NewReader(data)
// Attempt to load the tarball
if err := loadCmd.Run(); err != nil {
fmt.Printf("Error while loading buildkit image: %s\n", err)
loadedBuildkitTarball = false
}
}
} else {
loadedBuildkitVersion = false
loadedBuildkitTarball = false
}
if p.Builder.Driver != "" && p.Builder.Driver != defaultDriver {
var (
raw []byte
err error
)
tarballLoaded := loadedBuildkitTarball && loadedBuildkitVersion
shouldFallback := true
if len(p.Builder.DriverOptsNew) != 0 {
if version := resolveBuildkitImage(p.Builder, tarballLoaded, config); version != "" {
fmt.Printf("Using BuildKit Version with new driver opts: %s\n", version)
updateImageVersion(&p.Builder.DriverOptsNew, version)
}
createCmd := cmdSetupBuildx(p.Builder, p.Builder.DriverOptsNew, p.BuildkitInheritAuth)
raw, err = createCmd.Output()
if err != nil {
fmt.Printf("Unable to setup buildx with new driver opts: %s\n", err)
// Mark that the fallback will be used
shouldFallback = true
} else {
p.Builder.Name = strings.TrimSuffix(string(raw), "\n")
// If builder creation is successful, inspect the builder
inspectCmd := cmdInspectBuildx(p.Builder.Name)
if out, err := inspectCmd.CombinedOutput(); err != nil {
fmt.Printf("Error while inspecting buildx builder with new driver opts: %s\noutput:\n%s\n", err, string(out))
// Mark that the fallback will be used
shouldFallback = true
p.Builder.Name = ""
} else {
if p.Daemon.Debug {
fmt.Printf("Buildx builder inspection result:\n%s\n", string(out))
}
shouldFallback = false
}
}
}
if shouldFallback {
// Main code block
if version := resolveBuildkitImage(p.Builder, tarballLoaded, config); version != "" {
fmt.Printf("Using BuildKit Version: %s\n", version)
updateImageVersion(&p.Builder.DriverOpts, version)
}
createCmd := cmdSetupBuildx(p.Builder, p.Builder.DriverOpts, p.BuildkitInheritAuth)
raw, err = createCmd.Output()
if err != nil {
return fmt.Errorf("error while creating buildx builder: %s and err: %s", string(raw), err)
}
p.Builder.Name = strings.TrimSuffix(string(raw), "\n")
inspectCmd := cmdInspectBuildx(p.Builder.Name)
if out, err := inspectCmd.CombinedOutput(); err != nil {
// Do not fail the build on a bootstrap inspect error: the
// underlying issues (e.g. buildx's GPU capability probe or a
// registry freshness pull) are typically non-fatal and buildkit
// still boots. Surface the output and continue.
fmt.Printf("Error while bootstraping buildx builder: %s\noutput:\n%s\n", err, string(out))
} else if p.Daemon.Debug {
fmt.Printf("Buildx builder inspection result:\n%s\n", string(out))
}
}
removeCmd := cmdRemoveBuildx(p.Builder.Name)
defer func() {
removeCmd.Run()
}()
}
// Enforce mutual exclusivity: Bake mode and Push-only mode cannot be used together
if p.Build.BakeFile != "" && p.PushOnly {
return fmt.Errorf("conflict: Bake mode (PLUGIN_BAKE_FILE) and Push-only mode (PLUGIN_PUSH_ONLY) cannot be used together")
}
// Handle push-only mode if requested
if p.PushOnly {
return p.pushOnly()
}
var cmds []*exec.Cmd
cmds = append(cmds, commandVersion()) // docker version
cmds = append(cmds, commandInfo()) // docker info
// Determine execution path: Bake mode vs Classic buildx build
if p.Build.BakeFile != "" {
// Inform about ignored classic cache settings
if len(p.Build.CacheFrom) > 0 || len(p.Build.CacheTo) > 0 || p.Build.NoCache {
fmt.Println("Bake mode: ignoring PLUGIN_CACHE_*; define cache in the bake file.")
}
// Tar export is not applied in Bake mode
if p.TarPath != "" {
fmt.Println("Bake mode: ignoring PLUGIN_TAR_PATH; define outputs in the bake file.")
}
// Command to run buildx bake
cmds = append(cmds, commandBuildxBake(p.Build, p.Builder, p.Dryrun, p.MetadataFile))
} else {
// Classic path: add proxy build args and run buildx build
addProxyBuildArgs(&p.Build)
// Ensure tar output directory exists before buildx writes to it
if p.TarPath != "" && p.Dryrun && p.BuildxOutputFormat != "" {
dir := filepath.Dir(p.TarPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error: failed to create directory for tar file: %v", err)
}
fmt.Printf("Using direct buildx output (format: %s) to: %s\n", p.BuildxOutputFormat, p.TarPath)
}
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile, p.TarPath, p.BuildxOutputFormat)) // docker build
}
// execute all commands in batch mode.
for _, cmd := range cmds {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
trace(cmd)
var err error
if isCommandBuildxBuild(cmd.Args) && p.CacheMetricsFile != "" {
// Create a tee writer and get the channel
teeWriter, statusCh := Tee(os.Stdout)
var goroutineErr error
var wg sync.WaitGroup
wg.Add(1)
// Run the command in a goroutine
go func() {
defer teeWriter.Close()
defer wg.Done()
cmd.Stdout = teeWriter
cmd.Stderr = teeWriter
goroutineErr = cmd.Run()
}()
// Run the parseCacheMetrics function and handle errors
cacheMetrics, err := parseCacheMetrics(statusCh)
if err != nil {
fmt.Printf("Could not parse cache metrics: %s\n", err)
} else {
if err := writeCacheMetrics(cacheMetrics, p.CacheMetricsFile); err != nil {
fmt.Printf("Could not write cache metrics: %s\n", err)
}
}
wg.Wait()
if goroutineErr != nil {
return goroutineErr
}
} else {
err = cmd.Run()
}
if err != nil && isCommandPrune(cmd.Args) {
fmt.Printf("Could not prune system containers. Ignoring...\n")
} else if err != nil && isCommandRmi(cmd.Args) {
fmt.Printf("Could not remove image %s. Ignoring...\n", cmd.Args[2])
} else if err != nil {
return err
}
}
if p.Build.BakeFile == "" && p.TarPath != "" && p.Dryrun && p.BuildxOutputFormat == "" {
if len(p.Build.Tags) > 0 {
tag := p.Build.Tags[0]
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
if !imageExists(fullImageName) {
return fmt.Errorf("error: image %s not found in local daemon, cannot save to tar", fullImageName)
}
// Make sure the directory exists
dir := filepath.Dir(p.TarPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error: failed to create directory for tar file: %v", err)
}
// Save the image
fmt.Println("Saving image to tar:", p.TarPath)
saveCmd := commandSaveTar(fullImageName, p.TarPath)
saveCmd.Stdout = os.Stdout
saveCmd.Stderr = os.Stderr
trace(saveCmd)
if err := saveCmd.Run(); err != nil {
return fmt.Errorf("error: failed to save image to tar: %v", err)
}
fmt.Printf("Successfully saved image to %s\n", p.TarPath)
} else {
return fmt.Errorf("error: cannot save image to tar, no tags specified")
}
}
// output the adaptive card (skipped in Bake mode)
if p.Build.BakeFile == "" && p.Builder.Driver == defaultDriver {
if err := p.writeCard(); err != nil {
fmt.Printf("Could not create adaptive card. %s\n", err)
}
} else if p.Build.BakeFile != "" {
fmt.Println("Bake mode: skipping adaptive card output.")
}
// write to artifact file
if p.ArtifactFile != "" {
// ArtifactRegistry here will be read from env variable ARTIFACT_REGISTRY (valid for ACR). If this env
// variable is not present, it'll be read from PLUGIN_REGISTRY which is valid for docker / ecr / gcr.
if digest, err := getDigest(p.MetadataFile); err == nil {
if err = drone.WritePluginArtifactFile(p.Daemon.RegistryType, p.ArtifactFile, p.Daemon.ArtifactRegistry, p.Build.Repo, digest, p.Build.Tags); err != nil {
fmt.Printf("Failed to write plugin artifact file at path: %s with error: %s\n", p.ArtifactFile, err)
}
} else {
fmt.Printf("Could not fetch the digest. %s\n", err)
}
}
// execute cleanup routines in batch mode
if p.Cleanup {
// clear the slice
cmds = nil
cmds = append(cmds, commandRmi(p.Build.Name)) // docker rmi
cmds = append(cmds, commandPrune()) // docker system prune -f
for _, cmd := range cmds {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
trace(cmd)
}
}
return nil
}
func getDigest(metadataFile string) (string, error) {
file, err := os.Open(metadataFile)
if err != nil {
return "", fmt.Errorf("unable to open the metadata file %s with error: %s", metadataFile, err)
}
defer file.Close()
var metadata map[string]interface{}
if err = json.NewDecoder(file).Decode(&metadata); err != nil {
return "", fmt.Errorf("unable to decode the metadata with error: %s", err)
}
if d, found := metadata["containerimage.digest"]; found {
if digest, ok := d.(string); ok {
return digest, nil
}
return "", fmt.Errorf("unable to parse containerimage.digest from metadata json")
}
return "", fmt.Errorf("containerimage.digest not found in metadata json")
}
// helper function to create the docker login command.
func commandLogin(login Login) *exec.Cmd {
loginCopy := login
// update v2 docker registry to v1
if loginCopy.Registry == v2RegistryURL || loginCopy.Registry == v2HubRegistryURL {
fmt.Fprintf(os.Stdout, "Found dockerhub v2 registry, overriding with v1 registry instead: %s\n", v1RegistryURL)
loginCopy.Registry = v1RegistryURL
}
if loginCopy.Email != "" {
return commandLoginEmail(loginCopy)
}
return exec.Command(
dockerExe, "login",
"-u", loginCopy.Username,
"-p", loginCopy.Password,
loginCopy.Registry,
)
}
// helper function to set the credentials
func setDockerAuth(username, password, registry, baseImageUsername,
baseImagePassword, baseImageRegistry string) ([]byte, error) {
var credentials []docker.RegistryCredentials
// add only docker registry to the config
dockerConfig := docker.NewConfig()
if password != "" {
pushToRegistryCreds := docker.RegistryCredentials{
Registry: registry,
Username: username,
Password: password,
}
// push registry auth
credentials = append(credentials, pushToRegistryCreds)
}
if baseImageRegistry != "" {
pullFromRegistryCreds := docker.RegistryCredentials{
Registry: baseImageRegistry,
Username: baseImageUsername,
Password: baseImagePassword,
}
// base image registry auth
credentials = append(credentials, pullFromRegistryCreds)
}
// Creates docker config for both the registries used for authentication
return dockerConfig.CreateDockerConfigJson(credentials)
}
// helper to login via access token
func commandLoginAccessToken(login Login, accessToken string) *exec.Cmd {
cmd := exec.Command(dockerExe,
"login",
"-u",
"oauth2accesstoken",
"--password-stdin",
login.Registry)
cmd.Stdin = strings.NewReader(accessToken)
return cmd
}
// helper to check if args match "docker pull <image>"
func isCommandPull(args []string) bool {
return len(args) > 2 && args[1] == "pull"
}
func commandPull(repo string) *exec.Cmd {
return exec.Command(dockerExe, "pull", repo)
}
func commandLoginEmail(login Login) *exec.Cmd {
return exec.Command(
dockerExe, "login",
"-u", login.Username,
"-p", login.Password,
"-e", login.Email,
login.Registry,
)
}
// helper function to create the docker info command.
func commandVersion() *exec.Cmd {
return exec.Command(dockerExe, "version")
}
// helper function to create the docker info command.
func commandInfo() *exec.Cmd {
return exec.Command(dockerExe, "info")
}
// helper function to create the docker buildx command.
func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile string, tarPath string, outputFormat string) *exec.Cmd {
args := []string{
"buildx",
"build",
"--rm=true",
"-f", build.Dockerfile,
}
sanitizeCacheCommand(&build)
if builder.Name != "" {
args = append(args, "--builder", builder.Name)
}
for _, t := range build.Tags {
args = append(args, "-t", fmt.Sprintf("%s:%s", build.Repo, t))
}
if dryrun {
if tarPath != "" && outputFormat != "" {
args = append(args, fmt.Sprintf("--output=type=%s,dest=%s", outputFormat, tarPath))
} else if build.BuildxLoad || tarPath != "" {
args = append(args, "--load")
}
} else {
args = append(args, "--push")
}
if len(build.BuildxOptions) > 0 {
args = append(args, build.BuildxOptions...)
}
if build.BuildxOptionsSemicolon != "" {
splitOptions := strings.Split(build.BuildxOptionsSemicolon, ";")
for _, opt := range splitOptions {
if opt = strings.TrimSpace(opt); opt != "" {
args = append(args, opt)
}
}
}
args = append(args, build.Context)
if metadataFile != "" {
args = append(args, "--metadata-file", metadataFile)
}
if build.Squash {
args = append(args, "--squash")
}
if build.Compress {
args = append(args, "--compress")
}
if build.Pull {
args = append(args, "--pull=true")
}
if build.NoCache {
args = append(args, "--no-cache")
}
for _, arg := range build.CacheFrom {
args = append(args, "--cache-from", arg)
}
for _, arg := range build.CacheTo {
args = append(args, "--cache-to", arg)
}
for _, arg := range build.ArgsEnv {
addProxyValue(&build, arg)
}
if build.IsMultipleBuildArgs {
for _, arg := range build.ArgsNew {
args = append(args, "--build-arg", arg)
}
} else {
for _, arg := range build.Args {
args = append(args, "--build-arg", arg)
}
}
for _, host := range build.AddHost {
args = append(args, "--add-host", host)
}
if build.Secret != "" {
args = append(args, "--secret", build.Secret)
}
for _, secret := range build.SecretEnvs {
if arg, err := getSecretStringCmdArg(secret); err == nil {
args = append(args, "--secret", arg)
}
}
for _, secret := range build.SecretFiles {
if arg, err := getSecretFileCmdArg(secret); err == nil {
args = append(args, "--secret", arg)
}
}
if build.Target != "" {
args = append(args, "--target", build.Target)
}
if build.Quiet {
args = append(args, "--quiet")
}
if build.Platform != "" {
args = append(args, "--platform", build.Platform)
}
if build.SSHKeyPath != "" {
args = append(args, "--ssh", build.SSHKeyPath)
}
if build.AutoLabel {
labelSchema := []string{
fmt.Sprintf("created=%s", time.Now().Format(time.RFC3339)),
fmt.Sprintf("revision=%s", build.Name),
fmt.Sprintf("source=%s", build.Remote),
fmt.Sprintf("url=%s", build.Link),
}
labelPrefix := "org.opencontainers.image"
if len(build.LabelSchema) > 0 {
labelSchema = append(labelSchema, build.LabelSchema...)
}
for _, label := range labelSchema {
args = append(args, "--label", fmt.Sprintf("%s.%s", labelPrefix, label))
}
}
if len(build.Labels) > 0 {
for _, label := range build.Labels {
args = append(args, "--label", label)
}
}
return exec.Command(dockerExe, args...)
}
// helper function to create the docker buildx bake command.
func commandBuildxBake(build Build, builder Builder, dryrun bool, metadataFile string) *exec.Cmd {
args := []string{"buildx", "bake"}
if build.BakeFile != "" {
args = append(args, "-f", build.BakeFile)
}
if builder.Name != "" {
args = append(args, "--builder", builder.Name)
}
if dryrun {
args = append(args, "--load")
} else {
args = append(args, "--push")
}
if metadataFile != "" {
args = append(args, "--metadata-file", metadataFile)
}
if build.BakeOptions != "" {
tokens := strings.Split(build.BakeOptions, ";")
for _, t := range tokens {
t = strings.TrimSpace(t)
if t == "" {
continue
}
if t == "--push" || t == "--load" {
continue
}
args = append(args, t)
}
}
return exec.Command(dockerExe, args...)
}
func replaceOrRemoveAzureCacheAttr(arg, attr, value string) string {
placeholder := attr + "=harness_placeholder_azure_creds"
if !strings.Contains(arg, placeholder) {
return arg
}
if value != "" {
return strings.Replace(arg, placeholder, attr+"="+value, 1)
}
arg = strings.Replace(arg, ","+placeholder, "", 1)
arg = strings.Replace(arg, placeholder+",", "", 1)
return strings.Replace(arg, placeholder, "", 1)
}
// harnessScopeAttrs maps type=harness cache attrs to the step env they come from.
// BuildKit sends them to cache-service as orgId/projectId/pipelineId/stageId.
var harnessScopeAttrs = []struct{ attr, env string }{
{"org_id", "HARNESS_ORG_ID"},
{"project_id", "HARNESS_PROJECT_ID"},
{"pipeline_id", "HARNESS_PIPELINE_ID"},
{"stage_id", "HARNESS_STAGE_ID"},
}
// appendHarnessScopeAttrs adds the Harness scope attrs to a type=harness cache arg.
// Any other cache type is returned unchanged; empty env values and attrs already set are skipped.
func appendHarnessScopeAttrs(arg string) string {
if !strings.Contains(","+arg+",", ",type=harness,") {
return arg
}
for _, a := range harnessScopeAttrs {
if v := os.Getenv(a.env); v != "" && !strings.Contains(arg, a.attr+"=") {
arg += "," + a.attr + "=" + v
}
}
return arg
}
func sanitizeCacheCommand(build *Build) {
// Helper function to sanitize cache arguments
sanitizeCacheArgs := func(args []string) []string {
for i, arg := range args {
// Replace access_key_id if placeholder exists and the actual key is not empty
if strings.Contains(arg, "access_key_id=harness_placeholder_aws_creds") && build.HarnessSelfHostedS3AccessKey != "" {
arg = strings.Replace(arg, "access_key_id=harness_placeholder_aws_creds", "access_key_id="+build.HarnessSelfHostedS3AccessKey, 1)
}
// Replace secret_access_key if placeholder exists and the actual key is not empty
if strings.Contains(arg, "secret_access_key=harness_placeholder_aws_creds") && build.HarnessSelfHostedS3SecretKey != "" {
arg = strings.Replace(arg, "secret_access_key=harness_placeholder_aws_creds", "secret_access_key="+build.HarnessSelfHostedS3SecretKey, 1)
}
// Handle gcp_json_key
if strings.Contains(arg, "gcp_json_key=harness_placeholder_gcp_creds") {
if build.HarnessSelfHostedGcpJsonKey != "" {
// Base64 encode the GCP JSON key
encodedGCPJsonKey := base64.StdEncoding.EncodeToString([]byte(build.HarnessSelfHostedGcpJsonKey))
// Replace the placeholder with the base64-encoded GCP JSON key
arg = strings.Replace(arg, "gcp_json_key=harness_placeholder_gcp_creds", "gcp_json_key="+encodedGCPJsonKey, 1)
} else {
// Remove the gcp_json_key substring if the actual key is empty
arg = strings.Replace(arg, ",gcp_json_key=harness_placeholder_gcp_creds", "", 1)
arg = strings.Replace(arg, "gcp_json_key=harness_placeholder_gcp_creds,", "", 1)
arg = strings.Replace(arg, "gcp_json_key=harness_placeholder_gcp_creds", "", 1)
}
}
// Handle azure cache credential placeholders
arg = replaceOrRemoveAzureCacheAttr(arg, "account_key", build.HarnessSelfHostedAzureAccountKey)
arg = replaceOrRemoveAzureCacheAttr(arg, "tenant_id", build.HarnessSelfHostedAzureTenantID)
arg = replaceOrRemoveAzureCacheAttr(arg, "client_id", build.HarnessSelfHostedAzureClientID)
arg = replaceOrRemoveAzureCacheAttr(arg, "client_secret", build.HarnessSelfHostedAzureClientSecret)
arg = replaceOrRemoveAzureCacheAttr(arg, "oidc_token", build.HarnessSelfHostedAzureOidcToken)
if build.PathStyle && strings.Contains(arg, "type=s3") {
if strings.Contains(arg, "use_path_style=false") {
fmt.Printf("use_path_style is set to false in cache-from or cache-to but env var PLUGIN_PATH_STYLE is true\n")
} else if !strings.Contains(arg, "use_path_style=") {
// Add use_path_style=true, assuming comma-delimited key=val pairs
arg = arg + ",use_path_style=true"
}
}
if build.CacheTlsInsecure {
if strings.Contains(arg, "tls_insecure_skip_verify=false") {
fmt.Printf("tls_insecure_skip_verify is set to false in cache-from or cache-to but env var PLUGIN_PATH_STYLE is true\n")
} else if !strings.Contains(arg, "tls_insecure_skip_verify=") {
// Add use_path_style=true, assuming comma-delimited key=val pairs
arg = arg + ",tls_insecure_skip_verify=true"
}
}
arg = appendHarnessScopeAttrs(arg)
// Update the argument
args[i] = arg
}
return args
}
build.CacheFrom = sanitizeCacheArgs(build.CacheFrom)
build.CacheTo = sanitizeCacheArgs(build.CacheTo)
}
func getSecretStringCmdArg(kvp string) (string, error) {
return getSecretCmdArg(kvp, false)
}
func getSecretFileCmdArg(kvp string) (string, error) {
return getSecretCmdArg(kvp, true)
}
func getSecretCmdArg(kvp string, file bool) (string, error) {
delimIndex := strings.IndexByte(kvp, '=')
if delimIndex == -1 {
return "", fmt.Errorf("%s is not a valid secret", kvp)
}
key := kvp[:delimIndex]
value := kvp[delimIndex+1:]
if key == "" || value == "" {
return "", fmt.Errorf("%s is not a valid secret", kvp)
}
if file {
return fmt.Sprintf("id=%s,src=%s", key, value), nil
}
return fmt.Sprintf("id=%s,env=%s", key, value), nil
}
// helper function to add proxy values from the environment
func addProxyBuildArgs(build *Build) {
addProxyValue(build, "http_proxy")
addProxyValue(build, "https_proxy")
addProxyValue(build, "no_proxy")
}
// helper function to add the upper and lower case version of a proxy value.
func addProxyValue(build *Build, key string) {
value := getProxyValue(key)
if len(value) > 0 && !hasProxyBuildArg(build, key) {
build.Args = append(build.Args, fmt.Sprintf("%s=%s", key, value))
build.Args = append(build.Args, fmt.Sprintf("%s=%s", strings.ToUpper(key), value))
}
if len(value) > 0 && !hasProxyBuildArgNew(build, key) {
build.ArgsNew = append(build.ArgsNew, fmt.Sprintf("%s=%s", key, value))
build.ArgsNew = append(build.ArgsNew, fmt.Sprintf("%s=%s", strings.ToUpper(key), value))
}
}
// helper function to get a proxy value from the environment.
//
// Checks in order: lowercase key, uppercase key, then HARNESS_<UPPERCASE_KEY>.
// Assumes that the upper and lower case versions are the same value.
func getProxyValue(key string) string {
value := os.Getenv(key)
if len(value) > 0 {
return value
}