github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/merger/merger.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package merger 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sync" 24 "time" 25 26 "cloud.google.com/go/storage" 27 "github.com/golang/protobuf/proto" 28 "github.com/sirupsen/logrus" 29 "gopkg.in/yaml.v2" 30 31 "github.com/GoogleCloudPlatform/testgrid/config" 32 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 33 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 34 "github.com/GoogleCloudPlatform/testgrid/util/metrics" 35 ) 36 37 const componentName = "config-merger" 38 39 // MergeList is a list of config sources to merge together 40 // ParseAndCheck will construct this from a YAML document 41 type MergeList struct { 42 Target string `json:"Target"` 43 Path *gcs.Path `json:"-"` 44 Sources []Source `json:"Sources"` 45 } 46 47 // Source represents a configuration source in cloud storage 48 type Source struct { 49 Name string `json:"Name"` 50 Location string `json:"Location"` 51 Path *gcs.Path `json:"-"` 52 Contact string `json:"Contact,omitempty"` 53 } 54 55 // ParseAndCheck parses and checks the configuration file for common errors 56 func ParseAndCheck(data []byte) (list MergeList, err error) { 57 err = yaml.UnmarshalStrict(data, &list) 58 if err != nil { 59 return 60 } 61 62 list.Path, err = gcs.NewPath(list.Target) 63 if err != nil { 64 return 65 } 66 67 if len(list.Sources) == 0 { 68 return list, errors.New("no shards to converge") 69 } 70 71 names := map[string]bool{} 72 for i, source := range list.Sources { 73 if _, exists := names[source.Name]; exists { 74 return list, fmt.Errorf("duplicated name %s", source.Name) 75 } 76 path, err := gcs.NewPath(source.Location) 77 if err != nil { 78 return list, err 79 } 80 list.Sources[i].Path = path 81 source.Path = path 82 names[source.Name] = true 83 } 84 85 return 86 } 87 88 // Metrics holds metrics relevant to the config merger. 89 type Metrics struct { 90 Update metrics.Cyclic 91 Fields metrics.Int64 92 LastModified metrics.Int64 93 } 94 95 // CreateMetrics creates metrics for the Config Merger 96 func CreateMetrics(factory metrics.Factory) *Metrics { 97 return &Metrics{ 98 Update: factory.NewCyclic(componentName), 99 Fields: factory.NewInt64("config_fields", "Config field usage by name", "component", "field"), 100 LastModified: factory.NewInt64("last_modified", "Seconds since shard last modified ", "shard"), 101 } 102 } 103 104 type mergeClient interface { 105 gcs.Opener 106 gcs.Uploader 107 } 108 109 // MergeAndUpdate gathers configurations from each path and merges them. 110 // Puts the result at targetPath if confirm is true 111 // Will skip an input config if it is invalid and skipValidate is false 112 // Other problems are considered fatal and will return an error 113 func MergeAndUpdate(ctx context.Context, client mergeClient, mets *Metrics, list MergeList, skipValidate, confirm bool) (*configpb.Configuration, error) { 114 ctx, cancel := context.WithCancel(ctx) 115 defer cancel() 116 117 var finish *metrics.CycleReporter 118 if mets != nil { 119 finish = mets.Update.Start() 120 } 121 122 // TODO: Cache the version for each source. Only read if they've changed. 123 shards := map[string]*configpb.Configuration{} 124 var shardsLock sync.Mutex 125 var fatal error 126 127 var wg sync.WaitGroup 128 129 for _, source := range list.Sources { 130 if source.Path == nil { 131 finish.Skip() 132 return nil, fmt.Errorf("path at %q is nil", source.Name) 133 } 134 135 wg.Add(1) 136 source := source 137 go func() { 138 defer wg.Done() 139 cfg, attrs, err := config.ReadGCS(ctx, client, *source.Path) 140 recordLastModified(attrs, mets, source.Name) 141 if err != nil { 142 // Log each fatal error, but it's okay to return any fatal error 143 logrus.WithError(err).WithFields(logrus.Fields{ 144 "component": "config-merger", 145 "config-path": source.Location, 146 "contact": source.Contact, 147 }).Errorf("can't read config %q", source.Name) 148 fatal = fmt.Errorf("can't read config %q at %s: %w", source.Name, source.Path, err) 149 return 150 } 151 if !skipValidate { 152 if err := config.Validate(cfg); err != nil { 153 logrus.WithError(err).WithFields(logrus.Fields{ 154 "component": "config-merger", 155 "config-path": source.Location, 156 "contact": source.Contact, 157 }).Errorf("config %q is invalid; skipping config", source.Name) 158 return 159 } 160 } 161 162 shardsLock.Lock() 163 defer shardsLock.Unlock() 164 shards[source.Name] = cfg 165 }() 166 } 167 168 wg.Wait() 169 170 if fatal != nil { 171 finish.Fail() 172 return nil, fatal 173 } 174 if len(shards) == 0 { 175 finish.Skip() 176 return nil, errors.New("no configs to merge") 177 } 178 179 // Merge and output the result 180 result, err := config.Converge(shards) 181 if err != nil { 182 finish.Fail() 183 return result, fmt.Errorf("can't merge configurations: %w", err) 184 } 185 186 if !confirm { 187 fmt.Println(result) 188 finish.Success() 189 return result, nil 190 } 191 192 // Log each field as a metric 193 if mets != nil { 194 f := config.Fields(result) 195 for name, qty := range f { 196 mets.Fields.Set(qty, componentName, name) 197 } 198 } 199 200 buf, err := proto.Marshal(result) 201 if err != nil { 202 finish.Fail() 203 return result, fmt.Errorf("can't marshal merged proto: %w", err) 204 } 205 206 if _, err := client.Upload(ctx, *list.Path, buf, gcs.DefaultACL, gcs.NoCache); err != nil { 207 finish.Fail() 208 return result, fmt.Errorf("can't upload merged proto to %s: %w", list.Path, err) 209 } 210 211 finish.Success() 212 return result, nil 213 } 214 215 func recordLastModified(attrs *storage.ReaderObjectAttrs, mets *Metrics, source string) { 216 if attrs != nil { 217 lastModified := attrs.LastModified 218 diff := time.Since(lastModified) 219 if mets != nil { 220 mets.LastModified.Set(int64(diff.Seconds()), source) 221 } 222 logrus.WithFields(logrus.Fields{ 223 "diff": diff, 224 "shard": source, 225 }).Info("Time since last updated.") 226 } 227 }