github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/updateconfig/updateconfig.go (about) 1 /* 2 Copyright 2017 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 updateconfig 18 19 import ( 20 "fmt" 21 "path" 22 23 "github.com/mattn/go-zglob" 24 "github.com/sirupsen/logrus" 25 26 "k8s.io/test-infra/prow/github" 27 "k8s.io/test-infra/prow/kube" 28 "k8s.io/test-infra/prow/pluginhelp" 29 "k8s.io/test-infra/prow/plugins" 30 ) 31 32 const ( 33 pluginName = "config-updater" 34 ) 35 36 func init() { 37 plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider) 38 } 39 40 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 41 var configInfo map[string]string 42 if len(enabledRepos) == 1 { 43 msg := fmt.Sprintf( 44 "The main configuration is kept in sync with '%s/%s'.\nThe plugin configuration is kept in sync with '%s/%s'.", 45 enabledRepos[0], 46 config.ConfigUpdater.ConfigFile, 47 enabledRepos[0], 48 config.ConfigUpdater.PluginFile, 49 ) 50 configInfo = map[string]string{"": msg} 51 } 52 return &pluginhelp.PluginHelp{ 53 Description: "The config-updater plugin automatically redeploys configuration and plugin configuration files when they change. The plugin watches for pull request merges that modify either of the config files and updates the cluster's configmap resources in response.", 54 Config: configInfo, 55 }, 56 nil 57 } 58 59 type githubClient interface { 60 CreateComment(owner, repo string, number int, comment string) error 61 GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) 62 GetFile(org, repo, filepath, commit string) ([]byte, error) 63 } 64 65 // KubeClient knows how to interact with ConfigMaps on a cluster 66 type KubeClient interface { 67 GetConfigMap(name, namespace string) (kube.ConfigMap, error) 68 ReplaceConfigMap(name string, config kube.ConfigMap) (kube.ConfigMap, error) 69 CreateConfigMap(content kube.ConfigMap) (kube.ConfigMap, error) 70 } 71 72 func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error { 73 return handle(pc.GitHubClient, pc.KubeClient, pc.Logger, pre, maps(pc)) 74 } 75 76 func maps(pc plugins.Agent) map[string]plugins.ConfigMapSpec { 77 return pc.PluginConfig.ConfigUpdater.Maps 78 } 79 80 // FileGetter knows how to get the contents of a file by name 81 type FileGetter interface { 82 GetFile(filename string) ([]byte, error) 83 } 84 85 type gitHubFileGetter struct { 86 org, repo, commit string 87 client githubClient 88 } 89 90 func (g *gitHubFileGetter) GetFile(filename string) ([]byte, error) { 91 return g.client.GetFile(g.org, g.repo, filename, g.commit) 92 } 93 94 // Update updates the configmap with the data from the identified files 95 func Update(fg FileGetter, kc KubeClient, name, namespace string, updates map[string]string) error { 96 currentContent, getErr := kc.GetConfigMap(name, namespace) 97 _, isNotFound := getErr.(kube.NotFoundError) 98 if getErr != nil && !isNotFound { 99 return fmt.Errorf("failed to fetch current state of configmap: %v", getErr) 100 } 101 102 data := map[string]string{} 103 if currentContent.Data != nil { 104 data = currentContent.Data 105 } 106 107 for key, filename := range updates { 108 if filename == "" { 109 delete(data, key) 110 continue 111 } 112 113 content, err := fg.GetFile(filename) 114 if err != nil { 115 return fmt.Errorf("get file err: %v", err) 116 } 117 data[key] = string(content) 118 } 119 120 cm := kube.ConfigMap{ 121 ObjectMeta: kube.ObjectMeta{ 122 Name: name, 123 Namespace: namespace, 124 }, 125 Data: data, 126 } 127 128 var updateErr error 129 if getErr != nil && isNotFound { 130 _, updateErr = kc.CreateConfigMap(cm) 131 } else { 132 _, updateErr = kc.ReplaceConfigMap(name, cm) 133 } 134 if updateErr != nil { 135 return fmt.Errorf("replace config map err: %v", updateErr) 136 } 137 return nil 138 } 139 140 // ConfigMapID is a name/namespace combination that identifies a config map 141 type ConfigMapID struct { 142 Name, Namespace string 143 } 144 145 // FilterChanges determines which of the changes are relevant for config updating, returning mapping of 146 // config map to key to filename to update that key from. 147 func FilterChanges(configMaps map[string]plugins.ConfigMapSpec, changes []github.PullRequestChange, log *logrus.Entry) map[ConfigMapID]map[string]string { 148 toUpdate := map[ConfigMapID]map[string]string{} 149 for _, change := range changes { 150 var cm plugins.ConfigMapSpec 151 found := false 152 153 for key, configMap := range configMaps { 154 var matchErr error 155 found, matchErr = zglob.Match(key, change.Filename) 156 if matchErr != nil { 157 // Should not happen, log matchErr and continue 158 log.WithError(matchErr).Info("key matching error") 159 continue 160 } 161 162 if found { 163 cm = configMap 164 break 165 } 166 } 167 168 if !found { 169 continue // This file does not define a configmap 170 } 171 172 // Yes, update the configmap with the contents of this file 173 id := ConfigMapID{Name: cm.Name, Namespace: cm.Namespace} 174 if _, ok := toUpdate[id]; !ok { 175 toUpdate[id] = map[string]string{} 176 } 177 key := cm.Key 178 if key == "" { 179 key = path.Base(change.Filename) 180 // if the key changed, we need to remove the old key 181 if change.Status == github.PullRequestFileRenamed { 182 oldKey := path.Base(change.PreviousFilename) 183 toUpdate[id][oldKey] = "" 184 } 185 } 186 if change.Status == github.PullRequestFileRemoved { 187 toUpdate[id][key] = "" 188 } else { 189 toUpdate[id][key] = change.Filename 190 } 191 } 192 return toUpdate 193 } 194 195 func handle(gc githubClient, kc KubeClient, log *logrus.Entry, pre github.PullRequestEvent, configMaps map[string]plugins.ConfigMapSpec) error { 196 // Only consider newly merged PRs 197 if pre.Action != github.PullRequestActionClosed { 198 return nil 199 } 200 201 if len(configMaps) == 0 { // Nothing to update 202 return nil 203 } 204 205 pr := pre.PullRequest 206 207 if !pr.Merged || pr.MergeSHA == nil || pr.Base.Repo.DefaultBranch != pr.Base.Ref { 208 return nil 209 } 210 211 org := pr.Base.Repo.Owner.Login 212 repo := pr.Base.Repo.Name 213 214 // Which files changed in this PR? 215 changes, err := gc.GetPullRequestChanges(org, repo, pr.Number) 216 if err != nil { 217 return err 218 } 219 220 message := func(name, namespace string, data map[string]string, indent string) string { 221 identifier := fmt.Sprintf("`%s` configmap", name) 222 if namespace != "" { 223 identifier = fmt.Sprintf("%s in namespace `%s`", identifier, namespace) 224 } 225 msg := fmt.Sprintf("%s using the following files:", identifier) 226 for key, file := range data { 227 msg = fmt.Sprintf("%s\n%s- key `%s` using file `%s`", msg, indent, key, file) 228 } 229 return msg 230 } 231 232 // Are any of the changes files ones that define a configmap we want to update? 233 toUpdate := FilterChanges(configMaps, changes, log) 234 235 var updated []string 236 indent := " " // one space 237 if len(toUpdate) > 1 { 238 indent = " " // three spaces for sub bullets 239 } 240 for cm, data := range toUpdate { 241 if err := Update(&gitHubFileGetter{org: org, repo: repo, commit: *pr.MergeSHA, client: gc}, kc, cm.Name, cm.Namespace, data); err != nil { 242 return err 243 } 244 updated = append(updated, message(cm.Name, cm.Namespace, data, indent)) 245 } 246 247 var msg string 248 switch n := len(updated); n { 249 case 0: 250 return nil 251 case 1: 252 msg = fmt.Sprintf("Updated the %s", updated[0]) 253 default: 254 msg = fmt.Sprintf("Updated the following %d configmaps:\n", n) 255 for _, updateMsg := range updated { 256 msg += fmt.Sprintf(" * %s\n", updateMsg) // one space indent 257 } 258 } 259 260 if err := gc.CreateComment(org, repo, pr.Number, plugins.FormatResponseRaw(pr.Body, pr.HTMLURL, pr.User.Login, msg)); err != nil { 261 return fmt.Errorf("comment err: %v", err) 262 } 263 return nil 264 }