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  }