-
Notifications
You must be signed in to change notification settings - Fork 248
/
main.go
2245 lines (1998 loc) · 80.5 KB
/
main.go
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 main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/bombsimon/logrusr/v3"
"github.com/go-logr/logr"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
authapi "k8s.io/api/authorization/v1"
coreapi "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
rbacapi "k8s.io/api/rbac/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes/scheme"
authclientset "k8s.io/client-go/kubernetes/typed/authorization/v1"
coreclientset "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
prowapi "k8s.io/test-infra/prow/apis/prowjobs/v1"
"k8s.io/test-infra/prow/config/secret"
"k8s.io/test-infra/prow/logrusutil"
"k8s.io/test-infra/prow/pod-utils/downwardapi"
"k8s.io/test-infra/prow/version"
utilpointer "k8s.io/utils/pointer"
controllerruntime "sigs.k8s.io/controller-runtime"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
crcontrollerutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
ctrlruntimelog "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/yaml"
buildv1 "github.com/openshift/api/build/v1"
imageapi "github.com/openshift/api/image/v1"
projectapi "github.com/openshift/api/project/v1"
routev1 "github.com/openshift/api/route/v1"
templateapi "github.com/openshift/api/template/v1"
buildclientset "github.com/openshift/client-go/build/clientset/versioned/typed/build/v1"
projectclientset "github.com/openshift/client-go/project/clientset/versioned"
templatescheme "github.com/openshift/client-go/template/clientset/versioned/scheme"
templateclientset "github.com/openshift/client-go/template/clientset/versioned/typed/template/v1"
hivev1 "github.com/openshift/hive/apis/hive/v1"
"github.com/openshift/ci-tools/pkg/api"
"github.com/openshift/ci-tools/pkg/api/configresolver"
"github.com/openshift/ci-tools/pkg/api/nsttl"
"github.com/openshift/ci-tools/pkg/defaults"
"github.com/openshift/ci-tools/pkg/interrupt"
"github.com/openshift/ci-tools/pkg/junit"
"github.com/openshift/ci-tools/pkg/lease"
"github.com/openshift/ci-tools/pkg/load"
"github.com/openshift/ci-tools/pkg/registry"
"github.com/openshift/ci-tools/pkg/registry/server"
"github.com/openshift/ci-tools/pkg/results"
"github.com/openshift/ci-tools/pkg/secrets"
"github.com/openshift/ci-tools/pkg/steps"
"github.com/openshift/ci-tools/pkg/util"
"github.com/openshift/ci-tools/pkg/util/gzip"
"github.com/openshift/ci-tools/pkg/validation"
)
const usage = `Orchestrate multi-stage image-based builds
The ci-operator reads a declarative configuration YAML file and executes a set of build
steps on an OpenShift cluster for image-based components. By default, all steps are run,
but a caller may select one or more targets (image names or test names) to limit to only
steps that those targets depend on. The build creates a new project to run the builds in
and can automatically clean up the project when the build completes.
ci-operator leverages declarative OpenShift builds and images to reuse previously compiled
artifacts. It makes building multiple images that share one or more common base layers
simple as well as running tests that depend on those images.
Since the command is intended for use in CI environments it requires an input environment
variable called the JOB_SPEC that defines the GitHub project to execute and the commit,
branch, and any PRs to merge onto the branch. See the kubernetes/test-infra project for
a description of JOB_SPEC.
The inputs of the build (source code, tagged images, configuration) are combined to form
a consistent name for the target namespace that will change if any of the inputs change.
This allows multiple test jobs to share common artifacts and still perform retries.
The standard build steps are designed for simple command-line actions (like invoking
"make test") but can be extended by passing one or more templates via the --template flag.
The name of the template defines the stage and the template must contain at least one
pod. The parameters passed to the template are the current process environment and a set
of dynamic parameters that are inferred from previous steps. These parameters are:
NAMESPACE
The namespace generated by the operator for the given inputs or the value of
--namespace.
IMAGE_FORMAT
A string that points to the public image repository URL of the image stream(s)
created by the tag step. Example:
registry.svc.ci.openshift.org/ci-op-9o8bacu/stable:${component}
Will cause the template to depend on all image builds.
IMAGE_<component>
The public image repository URL for an output image. If specified the template
will depend on the image being built.
LOCAL_IMAGE_<component>
The public image repository URL for an image that was built during this run but
was not part of the output (such as pipeline cache images). If specified the
template will depend on the image being built.
JOB_NAME
The job name from the JOB_SPEC
JOB_NAME_SAFE
The job name in a form safe for use as a Kubernetes resource name.
JOB_NAME_HASH
A short hash of the job name for making tasks unique. This will not account for the target-additional-suffix.
UNIQUE_HASH
A hash for making tasks unique, even when the job name may be the same due to using the target-additional-suffix.
RPM_REPO_<org>_<repo>
If the job creates RPMs this will be the public URL that can be used as the
baseurl= value of an RPM repository. The value of org and repo are uppercased
and dashes are replaced with underscores.
Dynamic environment variables are overridden by process environment variables.
Both test and template jobs can gather artifacts created by pods. Set
--artifact-dir to define the top level artifact directory, and any test task
that defines artifact_dir or template that has an "artifacts" volume mounted
into a container will have artifacts extracted after the container has completed.
Errors in artifact extraction will not cause build failures.
In CI environments the inputs to a job may be different than what a normal
development workflow would use. The --override file will override fields
defined in the config file, such as base images and the release tag configuration.
After a successful build the --promote will tag each built image (in "images")
to the image stream(s) identified by the "promotion" config. You may add
additional images to promote and their target names via the "additional_images"
map.
`
const (
leaseAcquireTimeout = 120 * time.Minute
)
var (
// leaseServerAddress is the default lease server in app.ci
leaseServerAddress = api.URLForService(api.ServiceBoskos)
// configResolverAddress is the default configresolver address in app.ci
configResolverAddress = api.URLForService(api.ServiceConfig)
)
// CustomProwMetadata the name of the custom prow metadata file that's expected to be found in the artifacts directory.
const CustomProwMetadata = "custom-prow-metadata.json"
func main() {
censor, closer, err := setupLogger()
if err != nil {
logrus.WithError(err).Fatal("Could not set up logging.")
}
if closer != nil {
defer func() {
if err := closer.Close(); err != nil {
logrus.WithError(err).Warn("Could not close ci-operator log file.")
}
}()
}
// "i just don't want spam"
klog.LogToStderr(false)
logrus.Infof("%s version %s", version.Name, version.Version)
flagSet := flag.NewFlagSet("", flag.ExitOnError)
opt := bindOptions(flagSet)
opt.censor = censor
if err := flagSet.Parse(os.Args[1:]); err != nil {
logrus.WithError(err).Fatal("failed to parse flags")
}
ctrlruntimelog.SetLogger(logr.New(ctrlruntimelog.NullLogSink{}))
if opt.verbose {
fs := flag.NewFlagSet("", flag.ExitOnError)
klog.InitFlags(fs)
if err := fs.Set("alsologtostderr", "true"); err != nil {
logrus.WithError(err).Fatal("could not set klog alsologtostderr")
}
if err := fs.Set("v", "10"); err != nil {
logrus.WithError(err).Fatal("could not set klog v")
}
if err := fs.Parse([]string{}); err != nil {
logrus.WithError(err).Fatal("failed to parse klog flags")
}
logrus.SetLevel(logrus.TraceLevel)
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetReportCaller(true)
controllerruntime.SetLogger(logrusr.New(logrus.StandardLogger()))
}
if opt.help {
fmt.Print(usage)
flagSet.SetOutput(os.Stdout)
flagSet.Usage()
os.Exit(0)
}
flagSet.Visit(func(f *flag.Flag) {
switch f.Name {
case "delete-when-idle":
opt.idleCleanupDurationSet = true
case "delete-after":
opt.cleanupDurationSet = true
}
})
if err := addSchemes(); err != nil {
logrus.WithError(err).Fatal("failed to set up scheme")
}
rand.Seed(time.Now().UnixNano())
if err := opt.Complete(); err != nil {
logrus.WithError(err).Error("Failed to load arguments.")
opt.Report(results.ForReason("loading_args").ForError(err))
os.Exit(1)
}
if errs := opt.Run(); len(errs) > 0 {
var defaulted []error
for _, err := range errs {
defaulted = append(defaulted, results.DefaultReason(err))
}
message := bytes.Buffer{}
for _, err := range errs {
message.WriteString(fmt.Sprintf("\n * %s", err.Error()))
}
logrus.Error("Some steps failed:")
logrus.Error(message.String())
opt.Report(defaulted...)
os.Exit(1)
}
opt.Report()
}
// setupLogger sets up logrus to print all logs to a file and user-friendly logs to stdout
func setupLogger() (*secrets.DynamicCensor, io.Closer, error) {
logrus.SetLevel(logrus.TraceLevel)
censor := secrets.NewDynamicCensor()
logrus.SetFormatter(logrusutil.NewFormatterWithCensor(logrus.StandardLogger().Formatter, &censor))
logrus.SetOutput(io.Discard)
logrus.AddHook(&formattingHook{
formatter: logrusutil.NewFormatterWithCensor(&logrus.TextFormatter{
ForceColors: true,
DisableQuote: true,
FullTimestamp: true,
TimestampFormat: time.RFC3339,
}, &censor),
writer: os.Stdout,
logLevels: []logrus.Level{
logrus.InfoLevel,
logrus.WarnLevel,
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
},
})
artifactDir, set := api.Artifacts()
if !set {
return &censor, nil, nil
}
if err := os.MkdirAll(artifactDir, 0777); err != nil {
return nil, nil, err
}
verboseFile, err := os.Create(filepath.Join(artifactDir, "ci-operator.log"))
if err != nil {
return nil, nil, err
}
logrus.AddHook(&formattingHook{
formatter: logrusutil.NewFormatterWithCensor(&logrus.JSONFormatter{}, &censor),
writer: verboseFile,
logLevels: logrus.AllLevels,
})
return &censor, verboseFile, nil
}
type formattingHook struct {
formatter logrus.Formatter
writer io.Writer
logLevels []logrus.Level
}
func (hook *formattingHook) Fire(entry *logrus.Entry) error {
line, err := hook.formatter.Format(entry)
if err != nil {
return err
}
_, err = hook.writer.Write(line)
return err
}
func (hook *formattingHook) Levels() []logrus.Level {
return hook.logLevels
}
type stringSlice struct {
values []string
}
func (s *stringSlice) String() string {
return strings.Join(s.values, string(filepath.Separator))
}
func (s *stringSlice) Set(value string) error {
s.values = append(s.values, value)
return nil
}
type options struct {
configSpecPath string
unresolvedConfigPath string
templatePaths stringSlice
secretDirectories stringSlice
sshKeyPath string
oauthTokenPath string
targets stringSlice
promote bool
verbose bool
help bool
printGraph bool
writeParams string
artifactDir string
gitRef string
namespace string
baseNamespace string
extraInputHash stringSlice
idleCleanupDuration time.Duration
idleCleanupDurationSet bool
cleanupDuration time.Duration
cleanupDurationSet bool
inputHash string
secrets []*coreapi.Secret
templates []*templateapi.Template
graphConfig api.GraphConfiguration
configSpec *api.ReleaseBuildConfiguration
jobSpec *api.JobSpec
clusterConfig *rest.Config
podPendingTimeout time.Duration
consoleHost string
nodeName string
leaseServer string
leaseServerCredentialsFile string
leaseAcquireTimeout time.Duration
leaseClient lease.Client
givePrAuthorAccessToNamespace bool
impersonateUser string
authors []string
resolverAddress string
resolverClient server.ResolverClient
registryPath string
org string
repo string
branch string
variant string
injectTest string
metadataRevision int
pullSecretPath string
pullSecret *coreapi.Secret
pushSecretPath string
pushSecret *coreapi.Secret
uploadSecretPath string
uploadSecret *coreapi.Secret
cloneAuthConfig *steps.CloneAuthConfig
resultsOptions results.Options
censor *secrets.DynamicCensor
hiveKubeconfigPath string
hiveKubeconfig *rest.Config
multiStageParamOverrides stringSlice
dependencyOverrides stringSlice
targetAdditionalSuffix string
manifestToolDockerCfg string
localRegistryDNS string
}
func bindOptions(flag *flag.FlagSet) *options {
opt := &options{
idleCleanupDuration: 1 * time.Hour,
cleanupDuration: 24 * time.Hour,
}
// command specific options
flag.BoolVar(&opt.help, "h", false, "short for --help")
flag.BoolVar(&opt.help, "help", false, "See help for this command.")
flag.BoolVar(&opt.verbose, "v", false, "Show verbose output.")
// what we will run
flag.StringVar(&opt.nodeName, "node", "", "Restrict scheduling of pods to a single node in the cluster. Does not afffect indirectly created pods (e.g. builds).")
flag.DurationVar(&opt.podPendingTimeout, "pod-pending-timeout", 60*time.Minute, "Maximum amount of time created pods can spend before the running state. For test pods, this applies to each container. For builds, it applies to the build execution as a whole.")
flag.StringVar(&opt.leaseServer, "lease-server", leaseServerAddress, "Address of the server that manages leases. Required if any test is configured to acquire a lease.")
flag.StringVar(&opt.leaseServerCredentialsFile, "lease-server-credentials-file", "", "The path to credentials file used to access the lease server. The content is of the form <username>:<password>.")
flag.DurationVar(&opt.leaseAcquireTimeout, "lease-acquire-timeout", leaseAcquireTimeout, "Maximum amount of time to wait for lease acquisition")
flag.StringVar(&opt.registryPath, "registry", "", "Path to the step registry directory")
flag.StringVar(&opt.configSpecPath, "config", "", "The configuration file. If not specified the CONFIG_SPEC environment variable or the configresolver will be used.")
flag.StringVar(&opt.unresolvedConfigPath, "unresolved-config", "", "The configuration file, before resolution. If not specified the UNRESOLVED_CONFIG environment variable will be used, if set.")
flag.Var(&opt.targets, "target", "One or more targets in the configuration to build. Only steps that are required for this target will be run.")
flag.BoolVar(&opt.printGraph, "print-graph", opt.printGraph, "Print a directed graph of the build steps and exit. Intended for use with the golang digraph utility.")
// add to the graph of things we run or create
flag.Var(&opt.templatePaths, "template", "A set of paths to optional templates to add as stages to this job. Each template is expected to contain at least one restart=Never pod. Parameters are filled from environment or from the automatic parameters generated by the operator.")
flag.Var(&opt.secretDirectories, "secret-dir", "One or more directories that should converted into secrets in the test namespace. If the directory contains a single file with name .dockercfg or config.json it becomes a pull secret.")
flag.StringVar(&opt.sshKeyPath, "ssh-key-path", "", "A path of the private ssh key that is going to be used to clone a private repository.")
flag.StringVar(&opt.oauthTokenPath, "oauth-token-path", "", "A path of the OAuth token that is going to be used to clone a private repository.")
// the target namespace and cleanup behavior
flag.Var(&opt.extraInputHash, "input-hash", "Add arbitrary inputs to the build input hash to make the created namespace unique.")
flag.StringVar(&opt.namespace, "namespace", "", "Namespace to create builds into, defaults to build_id from JOB_SPEC. If the string '{id}' is in this value it will be replaced with the build input hash.")
flag.StringVar(&opt.baseNamespace, "base-namespace", "stable", "Namespace to read builds from, defaults to stable.")
flag.DurationVar(&opt.idleCleanupDuration, "delete-when-idle", opt.idleCleanupDuration, "If no pod is running for longer than this interval, delete the namespace. Set to zero to retain the contents. Requires the namespace TTL controller to be deployed.")
flag.DurationVar(&opt.cleanupDuration, "delete-after", opt.cleanupDuration, "If namespace exists for longer than this interval, delete the namespace. Set to zero to retain the contents. Requires the namespace TTL controller to be deployed.")
// actions to add to the graph
flag.BoolVar(&opt.promote, "promote", false, "When all other targets complete, publish the set of images built by this job into the release configuration.")
// output control
flag.StringVar(&opt.artifactDir, "artifact-dir", "", "DEPRECATED. Does nothing, set $ARTIFACTS instead.")
flag.StringVar(&opt.writeParams, "write-params", "", "If set write an env-compatible file with the output of the job.")
// experimental flags
flag.StringVar(&opt.gitRef, "git-ref", "", "Populate the job spec from this local Git reference. If JOB_SPEC is set, the refs field will be overwritten.")
flag.BoolVar(&opt.givePrAuthorAccessToNamespace, "give-pr-author-access-to-namespace", true, "Give view access to the temporarily created namespace to the PR author.")
flag.StringVar(&opt.impersonateUser, "as", "", "Username to impersonate")
// flags needed for the configresolver
flag.StringVar(&opt.resolverAddress, "resolver-address", configResolverAddress, "Address of configresolver")
flag.StringVar(&opt.org, "org", "", "Org of the project (used by configresolver)")
flag.StringVar(&opt.repo, "repo", "", "Repo of the project (used by configresolver)")
flag.StringVar(&opt.branch, "branch", "", "Branch of the project (used by configresolver)")
flag.StringVar(&opt.variant, "variant", "", "Variant of the project's ci-operator config (used by configresolver)")
flag.StringVar(&opt.injectTest, "with-test-from", "", "Inject a test from another ci-operator config, specified by ORG/REPO@BRANCH{__VARIANT}:TEST or JSON (used by configresolver)")
flag.StringVar(&opt.pullSecretPath, "image-import-pull-secret", "", "A set of dockercfg credentials used to import images for the tag_specification.")
flag.StringVar(&opt.pushSecretPath, "image-mirror-push-secret", "", "A set of dockercfg credentials used to mirror images for the promotion.")
flag.StringVar(&opt.uploadSecretPath, "gcs-upload-secret", "", "GCS credentials used to upload logs and artifacts.")
flag.StringVar(&opt.hiveKubeconfigPath, "hive-kubeconfig", "", "Path to the kubeconfig file to use for requests to Hive.")
flag.Var(&opt.multiStageParamOverrides, "multi-stage-param", "A repeatable option where one or more environment parameters can be passed down to the multi-stage steps. This parameter should be in the format NAME=VAL. e.g --multi-stage-param PARAM1=VAL1 --multi-stage-param PARAM2=VAL2.")
flag.Var(&opt.dependencyOverrides, "dependency-override-param", "A repeatable option used to override dependencies with external pull specs. This parameter should be in the format ENVVARNAME=PULLSPEC, e.g. --dependency-override-param=OO_INDEX=registry.mydomain.com:5000/pushed/myimage. This would override the value for the OO_INDEX environment variable for any tests/steps that currently have that dependency configured.")
flag.StringVar(&opt.targetAdditionalSuffix, "target-additional-suffix", "", "Inject an additional suffix onto the targeted test's 'as' name. Used for adding an aggregate index")
flag.StringVar(&opt.manifestToolDockerCfg, "manifest-tool-dockercfg", "/secrets/manifest-tool/.dockerconfigjson", "The dockercfg file path to be used to push the manifest listed image after build. This is being used by the manifest-tool binary.")
flag.StringVar(&opt.localRegistryDNS, "local-registry-dns", "image-registry.openshift-image-registry.svc:5000", "Defines the target image registry.")
opt.resultsOptions.Bind(flag)
return opt
}
func (o *options) Complete() error {
jobSpec, err := api.ResolveSpecFromEnv()
if err != nil {
if len(o.gitRef) == 0 {
return fmt.Errorf("failed to determine job spec: no --git-ref passed and failed to resolve job spec from env: %w", err)
}
// Failed to read $JOB_SPEC but --git-ref was passed, so try that instead
spec, refErr := jobSpecFromGitRef(o.gitRef)
if refErr != nil {
return fmt.Errorf("failed to determine job spec: failed to resolve --git-ref: %w", refErr)
}
jobSpec = spec
} else if len(o.gitRef) > 0 {
// Read from $JOB_SPEC but --git-ref was also passed, so merge them
spec, err := jobSpecFromGitRef(o.gitRef)
if err != nil {
return fmt.Errorf("failed to determine job spec: failed to resolve --git-ref: %w", err)
}
jobSpec.Refs = spec.Refs
}
jobSpec.BaseNamespace = o.baseNamespace
target := "all"
if len(o.targets.values) > 0 {
target = o.targets.values[0]
}
o.jobSpec = jobSpec
o.jobSpec.Target = target
info := o.getResolverInfo(jobSpec)
o.resolverClient = server.NewResolverClient(o.resolverAddress)
if o.unresolvedConfigPath != "" && o.configSpecPath != "" {
return errors.New("cannot set --config and --unresolved-config at the same time")
}
if o.unresolvedConfigPath != "" && o.resolverAddress == "" {
return errors.New("cannot request resolved config with --unresolved-config unless providing --resolver-address")
}
injectTest, err := o.getInjectTest()
if err != nil {
return err
}
var config *api.ReleaseBuildConfiguration
if injectTest != nil {
if o.resolverAddress == "" {
return errors.New("cannot request config with injected test without providing --resolver-address")
}
if o.unresolvedConfigPath != "" || o.configSpecPath != "" {
return errors.New("cannot request injecting test into locally provided config")
}
config, err = o.resolverClient.ConfigWithTest(info, injectTest)
} else {
config, err = o.loadConfig(info)
}
if err != nil {
return results.ForReason("loading_config").WithError(err).Errorf("failed to load configuration: %v", err)
}
if len(o.gitRef) != 0 && config.CanonicalGoRepository != nil {
o.jobSpec.Refs.PathAlias = *config.CanonicalGoRepository
}
o.configSpec = config
o.jobSpec.Metadata = config.Metadata
if err := validation.IsValidResolvedConfiguration(o.configSpec); err != nil {
return results.ForReason("validating_config").ForError(err)
}
o.graphConfig = defaults.FromConfigStatic(o.configSpec)
if err := validation.IsValidGraphConfiguration(o.graphConfig.Steps); err != nil {
return results.ForReason("validating_config").ForError(err)
}
if o.verbose {
config, _ := yaml.Marshal(o.configSpec)
logrus.WithField("config", string(config)).Trace("Resolved configuration.")
job, _ := json.Marshal(o.jobSpec)
logrus.WithField("jobspec", string(job)).Trace("Resolved job spec.")
}
var refs []prowapi.Refs
if o.jobSpec.Refs != nil {
refs = append(refs, *o.jobSpec.Refs)
}
refs = append(refs, o.jobSpec.ExtraRefs...)
if len(refs) == 0 {
logrus.Info("No source defined")
}
for _, ref := range refs {
if ref.BaseSHA == "" {
logrus.Debugf("Resolved SHA missing for %s in https://github.com/%s/%s: adding synthetic input to avoid false cache hit", ref.BaseRef, ref.Org, ref.Repo)
o.extraInputHash.values = append(o.extraInputHash.values, time.Now().String())
}
logrus.Info(summarizeRef(ref))
for _, pull := range ref.Pulls {
o.authors = append(o.authors, pull.Author)
}
}
if len(o.sshKeyPath) > 0 && len(o.oauthTokenPath) > 0 {
return errors.New("both --ssh-key-path and --oauth-token-path are specified")
}
var cloneAuthSecretPath string
if len(o.oauthTokenPath) > 0 {
cloneAuthSecretPath = o.oauthTokenPath
o.cloneAuthConfig = &steps.CloneAuthConfig{Type: steps.CloneAuthTypeOAuth}
} else if len(o.sshKeyPath) > 0 {
cloneAuthSecretPath = o.sshKeyPath
o.cloneAuthConfig = &steps.CloneAuthConfig{Type: steps.CloneAuthTypeSSH}
}
if len(cloneAuthSecretPath) > 0 {
o.cloneAuthConfig.Secret, err = getCloneSecretFromPath(o.cloneAuthConfig.Type, cloneAuthSecretPath)
if err != nil {
return fmt.Errorf("could not get secret from path %s: %w", cloneAuthSecretPath, err)
}
}
for _, path := range o.secretDirectories.values {
secret, err := util.SecretFromDir(path)
name := filepath.Base(path)
if err != nil {
return fmt.Errorf("failed to generate secret %s: %w", name, err)
}
secret.Name = name
if len(secret.Data) == 1 {
if _, ok := secret.Data[coreapi.DockerConfigJsonKey]; ok {
secret.Type = coreapi.SecretTypeDockerConfigJson
}
if _, ok := secret.Data[coreapi.DockerConfigKey]; ok {
secret.Type = coreapi.SecretTypeDockercfg
}
}
o.secrets = append(o.secrets, secret)
}
for _, path := range o.templatePaths.values {
contents, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("could not read dir %s for template: %w", path, err)
}
obj, gvk, err := templatescheme.Codecs.UniversalDeserializer().Decode(contents, nil, nil)
if err != nil {
return fmt.Errorf("unable to parse template %s: %w", path, err)
}
template, ok := obj.(*templateapi.Template)
if !ok {
return fmt.Errorf("%s is not a template: %v", path, gvk)
}
if len(template.Name) == 0 {
template.Name = filepath.Base(path)
template.Name = strings.TrimSuffix(template.Name, filepath.Ext(template.Name))
}
o.templates = append(o.templates, template)
}
clusterConfig, err := util.LoadClusterConfig()
if err != nil {
return fmt.Errorf("failed to load cluster config: %w", err)
}
if len(o.impersonateUser) > 0 {
clusterConfig.Impersonate = rest.ImpersonationConfig{UserName: o.impersonateUser}
}
if o.verbose {
clusterConfig.ContentType = "application/json"
clusterConfig.AcceptContentTypes = "application/json"
}
o.clusterConfig = clusterConfig
if o.pullSecretPath != "" {
if o.pullSecret, err = getDockerConfigSecret(api.RegistryPullCredentialsSecret, o.pullSecretPath); err != nil {
return fmt.Errorf("could not get pull secret %s from path %s: %w", api.RegistryPullCredentialsSecret, o.pullSecretPath, err)
}
}
if o.pushSecretPath != "" {
if o.pushSecret, err = getDockerConfigSecret(api.RegistryPushCredentialsCICentralSecret, o.pushSecretPath); err != nil {
return fmt.Errorf("could not get push secret %s from path %s: %w", api.RegistryPushCredentialsCICentralSecret, o.pushSecretPath, err)
}
}
if o.uploadSecretPath != "" {
gcsSecretName := resolveGCSCredentialsSecret(o.jobSpec)
if o.uploadSecret, err = getSecret(gcsSecretName, o.uploadSecretPath); err != nil {
return fmt.Errorf("could not get upload secret %s from path %s: %w", gcsSecretName, o.uploadSecretPath, err)
}
}
if o.hiveKubeconfigPath != "" {
kubeConfig, err := util.LoadKubeConfig(o.hiveKubeconfigPath)
if err != nil {
return fmt.Errorf("could not load Hive kube config from path %s: %w", o.hiveKubeconfigPath, err)
}
o.hiveKubeconfig = kubeConfig
}
if err := overrideMultiStageParams(o); err != nil {
return err
}
handleTargetAdditionalSuffix(o)
return overrideTestStepDependencyParams(o)
}
func parseKeyValParams(input []string, paramType string) (map[string]string, error) {
var validationErrors []error
params := make(map[string]string)
for _, param := range input {
paramNameAndVal := strings.Split(param, "=")
if len(paramNameAndVal) == 2 {
params[strings.TrimSpace(paramNameAndVal[0])] = strings.TrimSpace(paramNameAndVal[1])
} else {
validationErrors = append(validationErrors, fmt.Errorf("could not parse %s: %s is not in the format key=value", paramType, param))
}
}
if len(validationErrors) > 0 {
return nil, utilerrors.NewAggregate(validationErrors)
}
return params, nil
}
func handleTargetAdditionalSuffix(o *options) {
if o.targetAdditionalSuffix == "" {
return
}
o.jobSpec.TargetAdditionalSuffix = o.targetAdditionalSuffix
for i, test := range o.configSpec.Tests {
for j, target := range o.targets.values {
if test.As == target {
targetWithSuffix := fmt.Sprintf("%s-%s", test.As, o.targetAdditionalSuffix)
o.configSpec.Tests[i].As = targetWithSuffix
if j == 0 { //only set if it is the first target
o.jobSpec.Target = targetWithSuffix
}
o.targets.values[j] = targetWithSuffix
logrus.Debugf("added suffix to target, now: %s", test.As)
break
}
}
}
}
func overrideMultiStageParams(o *options) error {
// see if there are any passed-in multi-stage parameters.
if len(o.multiStageParamOverrides.values) == 0 {
return nil
}
multiStageParams, err := parseKeyValParams(o.multiStageParamOverrides.values, "multi-stage-param")
if err != nil {
return err
}
// for any multi-stage tests, go ahead and inject the passed-in parameters. Note that parameters explicitly passed
// in to ci-operator will take precedence.
for _, test := range o.configSpec.Tests {
if test.MultiStageTestConfigurationLiteral != nil {
if test.MultiStageTestConfigurationLiteral.Environment == nil {
test.MultiStageTestConfigurationLiteral.Environment = make(api.TestEnvironment)
}
for paramName, paramVal := range multiStageParams {
test.MultiStageTestConfigurationLiteral.Environment[paramName] = paramVal
}
}
}
return nil
}
func overrideTestStepDependencyParams(o *options) error {
dependencyOverrideParams, err := parseKeyValParams(o.dependencyOverrides.values, "dependency-override-param")
if err != nil {
return err
}
// first see if there are any dependency overrides at the test level. This should really only happen with rehearsals.
for _, test := range o.configSpec.Tests {
if test.MultiStageTestConfigurationLiteral != nil {
for dependencyName, pullspec := range test.MultiStageTestConfigurationLiteral.DependencyOverrides {
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Pre)
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Test)
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Post)
}
}
}
// dependency overrides specified as params to ci-operator always take precedence.
for dependencyName, pullspec := range dependencyOverrideParams {
for _, test := range o.configSpec.Tests {
if test.MultiStageTestConfigurationLiteral != nil {
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Pre)
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Test)
overrideTestStepDependency(dependencyName, pullspec, &test.MultiStageTestConfigurationLiteral.Post)
}
}
}
return nil
}
func overrideTestStepDependency(name string, value string, steps *[]api.LiteralTestStep) {
for stepI, step := range *steps {
for depI, dependency := range step.Dependencies {
if strings.EqualFold(dependency.Env, name) {
steps := *steps
steps[stepI].Dependencies[depI].PullSpec = value
}
}
}
}
func excludeContextCancelledErrors(errs []error) []error {
var ret []error
for _, err := range errs {
if !errors.Is(err, context.Canceled) {
ret = append(ret, err)
}
}
return ret
}
func (o *options) Report(errs ...error) {
if len(errs) > 0 {
o.writeFailingJUnit(errs)
}
reporter, loadErr := o.resultsOptions.Reporter(o.jobSpec, o.consoleHost)
if loadErr != nil {
logrus.WithError(loadErr).Warn("Could not load result reporting options.")
return
}
errorToReport := excludeContextCancelledErrors(errs)
for _, err := range errorToReport {
reporter.Report(err)
}
if len(errorToReport) == 0 {
reporter.Report(nil)
}
}
func (o *options) Run() []error {
start := time.Now()
defer func() {
logrus.Infof("Ran for %s", time.Since(start).Truncate(time.Second))
}()
ctx, cancel := context.WithCancel(context.Background())
handler := func(s os.Signal) {
logrus.Infof("error: Process interrupted with signal %s, cancelling execution...", s)
cancel()
}
var leaseClient *lease.Client
if o.leaseServer != "" && o.leaseServerCredentialsFile != "" {
leaseClient = &o.leaseClient
}
o.resolveConsoleHost()
streams, err := integratedStreams(o.configSpec, o.resolverClient, o.clusterConfig)
if err != nil {
return []error{results.ForReason("config_resolver").WithError(err).Errorf("failed to generate integrated streams: %v", err)}
}
client, err := coreclientset.NewForConfig(o.clusterConfig)
if err != nil {
return []error{fmt.Errorf("could not get core client for cluster config: %w", err)}
}
nodeArchitectures, err := resolveNodeArchitectures(ctx, client.Nodes())
if err != nil {
return []error{fmt.Errorf("could not resolve the node architectures: %w", err)}
}
mergedConfig := o.injectTest != "" || len(o.jobSpec.ExtraRefs) > 1
// load the graph from the configuration
buildSteps, postSteps, err := defaults.FromConfig(ctx, o.configSpec, &o.graphConfig, o.jobSpec, o.templates, o.writeParams, o.promote, o.clusterConfig,
o.podPendingTimeout, leaseClient, o.targets.values, o.cloneAuthConfig, o.pullSecret, o.pushSecret, o.censor, o.hiveKubeconfig,
o.consoleHost, o.nodeName, nodeArchitectures, o.targetAdditionalSuffix, o.manifestToolDockerCfg, o.localRegistryDNS, mergedConfig, streams)
if err != nil {
return []error{results.ForReason("defaulting_config").WithError(err).Errorf("failed to generate steps from config: %v", err)}
}
// Before we create the namespace, we need to ensure all inputs to the graph
// have been resolved. We must run this step before we resolve the partial
// graph or otherwise two jobs with different targets would create different
// artifact caches.
if err := o.resolveInputs(buildSteps); err != nil {
return []error{results.ForReason("resolving_inputs").WithError(err).Errorf("could not resolve inputs: %v", err)}
}
if err := o.writeMetadataJSON(); err != nil {
return []error{fmt.Errorf("unable to write metadata.json for build: %w", err)}
}
// convert the full graph into the subset we must run
nodes, err := api.BuildPartialGraph(buildSteps, o.targets.values)
if err != nil {
return []error{results.ForReason("building_graph").WithError(err).Errorf("could not build execution graph: %v", err)}
}
stepList, errs := nodes.TopologicalSort()
if errs != nil {
return append([]error{results.ForReason("building_graph").ForError(errors.New("could not sort nodes"))}, errs...)
}
logrus.Infof("Running %s", strings.Join(nodeNames(stepList), ", "))
if o.printGraph {
if err := printDigraph(os.Stdout, stepList); err != nil {
return []error{fmt.Errorf("could not print graph: %w", err)}
}
return nil
}
graph, errs := calculateGraph(stepList)
if errs != nil {
return errs
}
defer func() {
serializedGraph, err := json.Marshal(graph)
if err != nil {
logrus.WithError(err).Error("Failed to marshal graph")
return
}
_ = api.SaveArtifact(o.censor, api.CIOperatorStepGraphJSONFilename, serializedGraph)
}()
// initialize the namespace if necessary and create any resources that must
// exist prior to execution
if err := o.initializeNamespace(); err != nil {
return []error{results.ForReason("initializing_namespace").WithError(err).Errorf("could not initialize namespace: %v", err)}
}
return interrupt.New(handler, o.saveNamespaceArtifacts).Run(func() []error {
if leaseClient != nil {
if err := o.initializeLeaseClient(); err != nil {
return []error{fmt.Errorf("failed to create the lease client: %w", err)}
}
}
go monitorNamespace(ctx, cancel, o.namespace, client.Namespaces())
authClient, err := authclientset.NewForConfig(o.clusterConfig)
if err != nil {
return []error{fmt.Errorf("could not get auth client for cluster config: %w", err)}
}
eventRecorder, err := eventRecorder(client, authClient, o.namespace)
if err != nil {
return []error{fmt.Errorf("could not create event recorder: %w", err)}
}
runtimeObject := &coreapi.ObjectReference{Namespace: o.namespace}
eventRecorder.Event(runtimeObject, coreapi.EventTypeNormal, "CiJobStarted", eventJobDescription(o.jobSpec, o.namespace))
// execute the graph
suites, graphDetails, errs := steps.Run(ctx, nodes)
if err := o.writeJUnit(suites, "operator"); err != nil {
logrus.WithError(err).Warn("Unable to write JUnit result.")
}
graph.MergeFrom(graphDetails...)
// Rewrite the Metadata JSON to catch custom metadata if it has been generated by the job
if err := o.writeMetadataJSON(); err != nil {
logrus.WithError(err).Warn("Unable to update metadata.json for build")
}
if len(errs) > 0 {
eventRecorder.Event(runtimeObject, coreapi.EventTypeWarning, "CiJobFailed", eventJobDescription(o.jobSpec, o.namespace))
var wrapped []error
for _, err := range errs {
wrapped = append(wrapped, &errWroteJUnit{wrapped: results.ForReason("executing_graph").WithError(err).Errorf("could not run steps: %v", err)})
}
return wrapped
}
for _, step := range postSteps {
details, err := runStep(ctx, step)
graph.MergeFrom(details)
if err != nil {
eventRecorder.Event(runtimeObject, coreapi.EventTypeWarning, "PostStepFailed",
fmt.Sprintf("Post step %s failed while %s", step.Name(), eventJobDescription(o.jobSpec, o.namespace)))
return []error{results.ForReason("executing_post").WithError(err).Errorf("could not run post step %s: %v", step.Name(), err)}
}
}
eventRecorder.Event(runtimeObject, coreapi.EventTypeNormal, "CiJobSucceeded", eventJobDescription(o.jobSpec, o.namespace))
return nil
})
}
func integratedStreams(config *api.ReleaseBuildConfiguration, client server.ResolverClient, clusterConfig *rest.Config) (map[string]*configresolver.IntegratedStream, error) {
if config == nil {
return nil, errors.New("unable to get integrated stream for nil config")
}
if client == nil {
return nil, errors.New("unable to get integrated stream with nil client")