github.com/abayer/test-infra@v0.0.5/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 type kubeClient interface { 66 GetConfigMap(name, namespace string) (kube.ConfigMap, error) 67 ReplaceConfigMap(name string, config kube.ConfigMap) (kube.ConfigMap, error) 68 } 69 70 func handlePullRequest(pc plugins.PluginClient, pre github.PullRequestEvent) error { 71 return handle(pc.GitHubClient, pc.KubeClient, pc.Logger, pre, maps(pc)) 72 } 73 74 func maps(pc plugins.PluginClient) map[string]plugins.ConfigMapSpec { 75 return pc.PluginConfig.ConfigUpdater.Maps 76 } 77 78 func update(gc githubClient, kc kubeClient, org, repo, commit, name, namespace string, updates map[string]string) error { 79 currentContent, err := kc.GetConfigMap(name, namespace) 80 if _, isNotFound := err.(kube.NotFoundError); err != nil && !isNotFound { 81 return fmt.Errorf("failed to fetch current state of configmap: %v", err) 82 } 83 84 data := map[string]string{} 85 if currentContent.Data != nil { 86 data = currentContent.Data 87 } 88 89 for key, filename := range updates { 90 if filename == "" { 91 delete(data, key) 92 continue 93 } 94 95 content, err := gc.GetFile(org, repo, filename, commit) 96 if err != nil { 97 return fmt.Errorf("get file err: %v", err) 98 } 99 data[key] = string(content) 100 } 101 102 cm := kube.ConfigMap{ 103 ObjectMeta: kube.ObjectMeta{ 104 Name: name, 105 Namespace: namespace, 106 }, 107 Data: data, 108 } 109 110 _, err = kc.ReplaceConfigMap(name, cm) 111 if err != nil { 112 return fmt.Errorf("replace config map err: %v", err) 113 } 114 return nil 115 } 116 117 func handle(gc githubClient, kc kubeClient, log *logrus.Entry, pre github.PullRequestEvent, configMaps map[string]plugins.ConfigMapSpec) error { 118 // Only consider newly merged PRs 119 if pre.Action != github.PullRequestActionClosed { 120 return nil 121 } 122 123 if len(configMaps) == 0 { // Nothing to update 124 return nil 125 } 126 127 pr := pre.PullRequest 128 129 if !pr.Merged || pr.MergeSHA == nil || pr.Base.Repo.DefaultBranch != pr.Base.Ref { 130 return nil 131 } 132 133 org := pr.Base.Repo.Owner.Login 134 repo := pr.Base.Repo.Name 135 136 // Which files changed in this PR? 137 changes, err := gc.GetPullRequestChanges(org, repo, pr.Number) 138 if err != nil { 139 return err 140 } 141 142 message := func(name, namespace string, data map[string]string, indent string) string { 143 identifier := fmt.Sprintf("`%s` configmap", name) 144 if namespace != "" { 145 identifier = fmt.Sprintf("%s in namespace `%s`", identifier, namespace) 146 } 147 msg := fmt.Sprintf("%s using the following files:", identifier) 148 for key, file := range data { 149 msg = fmt.Sprintf("%s\n%s- key `%s` using file `%s`", msg, indent, key, file) 150 } 151 return msg 152 } 153 154 // Are any of the changes files ones that define a configmap we want to update? 155 var updated []string 156 type configMapID struct { 157 name, namespace string 158 } 159 toUpdate := map[configMapID]map[string]string{} 160 for _, change := range changes { 161 var cm plugins.ConfigMapSpec 162 found := false 163 164 for key, configMap := range configMaps { 165 found, err = zglob.Match(key, change.Filename) 166 if err != nil { 167 // Should not happen, log err and continue 168 log.WithError(err).Info("key matching error") 169 continue 170 } 171 172 if found { 173 cm = configMap 174 break 175 } 176 } 177 178 if !found { 179 continue // This file does not define a configmap 180 } 181 182 // Yes, update the configmap with the contents of this file 183 key := cm.Key 184 if key == "" { 185 key = path.Base(change.Filename) 186 } 187 id := configMapID{name: cm.Name, namespace: cm.Namespace} 188 if _, ok := toUpdate[id]; !ok { 189 toUpdate[id] = map[string]string{} 190 } 191 if change.Status == "removed" { 192 toUpdate[id][key] = "" 193 } else { 194 toUpdate[id][key] = change.Filename 195 } 196 } 197 198 indent := " " // one space 199 if len(toUpdate) > 1 { 200 indent = " " // three spaces for sub bullets 201 } 202 for cm, data := range toUpdate { 203 if err := update(gc, kc, org, repo, *pr.MergeSHA, cm.name, cm.namespace, data); err != nil { 204 return err 205 } 206 updated = append(updated, message(cm.name, cm.namespace, data, indent)) 207 } 208 209 var msg string 210 switch n := len(updated); n { 211 case 0: 212 return nil 213 case 1: 214 msg = fmt.Sprintf("Updated the %s", updated[0]) 215 default: 216 msg = fmt.Sprintf("Updated the following %d configmaps:\n", n) 217 for _, updateMsg := range updated { 218 msg += fmt.Sprintf(" * %s\n", updateMsg) // one space indent 219 } 220 } 221 222 if err := gc.CreateComment(org, repo, pr.Number, plugins.FormatResponseRaw(pr.Body, pr.HTMLURL, pr.User.Login, msg)); err != nil { 223 return fmt.Errorf("comment err: %v", err) 224 } 225 return nil 226 }