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  }