github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/maintenance/migratestatus/migrator/migrator.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 migrator
    18  
    19  import (
    20  	"fmt"
    21  
    22  	"k8s.io/test-infra/ghclient"
    23  
    24  	"github.com/golang/glog"
    25  	"github.com/google/go-github/github"
    26  )
    27  
    28  var (
    29  	stateAny = "ANY_STATE"
    30  	stateDNE = "DOES_NOT_EXIST"
    31  )
    32  
    33  // contextCondition is a struct that describes a condition about the state or existence of a context.
    34  type contextCondition struct {
    35  	// context is the status context that this condition applies to.
    36  	context string
    37  	// state is the status state that the condition accepts, or one of the special values "ANY_STATE"
    38  	// and "DOES_NOT_EXIST".
    39  	state string
    40  }
    41  
    42  // Mode is a struct that describes the behavior of a status migration. The behavior is described as
    43  // a list of conditions and a function that determines the actions to be taken when the conditions
    44  // are met.
    45  type Mode struct {
    46  	conditions []*contextCondition
    47  	// actions returns the status updates to make based on the current statuses and the sha.
    48  	// When actions is called, the Mode may assume that it's conditions are met.
    49  	actions func(statuses []github.RepoStatus, sha string) []*github.RepoStatus
    50  }
    51  
    52  // MoveMode creates a mode that both copies and retires.
    53  // The mode creates a new context on every PR with the old context but not the new one, setting the
    54  // state of the new context to that of the old context before retiring the old context.
    55  func MoveMode(origContext, newContext string) *Mode {
    56  	dup := copyAction(origContext, newContext)
    57  	dep := retireAction(origContext, newContext)
    58  
    59  	return &Mode{
    60  		conditions: []*contextCondition{
    61  			{context: origContext, state: stateAny},
    62  			{context: newContext, state: stateDNE},
    63  		},
    64  		actions: func(statuses []github.RepoStatus, sha string) []*github.RepoStatus {
    65  			return append(dup(statuses, sha), dep(statuses, sha)...)
    66  		},
    67  	}
    68  }
    69  
    70  // CopyMode makes a mode that creates a new context in every PR that has the old context, but not the new one.
    71  // The state, description and target URL of the new context are made the same as those of the old context.
    72  func CopyMode(origContext, newContext string) *Mode {
    73  	return &Mode{
    74  		conditions: []*contextCondition{
    75  			{context: origContext, state: stateAny},
    76  			{context: newContext, state: stateDNE},
    77  		},
    78  		actions: copyAction(origContext, newContext),
    79  	}
    80  }
    81  
    82  // RetireMode creates a mode that retires an old context on all PRs.
    83  // If newContext is the empty string, origContext is retired without replacement. Its state is set to
    84  // 'success' and its description is set to indicate that the context is retired.
    85  // If newContext is not the empty string it is considered the replacement of origContext. This means
    86  // that only PRs that have the newContext in addition to the origContext will be considered and the
    87  // description of the retired context will indicate that it was replaced by newContext.
    88  func RetireMode(origContext, newContext string) *Mode {
    89  	conditions := []*contextCondition{{context: origContext, state: stateAny}}
    90  	if newContext != "" {
    91  		conditions = append(conditions, &contextCondition{context: newContext, state: stateAny})
    92  	}
    93  	return &Mode{
    94  		conditions: conditions,
    95  		actions:    retireAction(origContext, newContext),
    96  	}
    97  }
    98  
    99  // copyAction creates a function that returns a copy action.
   100  // Specifically the returned function returns a RepoStatus that will create a status for newContext
   101  // with state set to the state of origContext.
   102  func copyAction(origContext, newContext string) func(statuses []github.RepoStatus, sha string) []*github.RepoStatus {
   103  	return func(statuses []github.RepoStatus, sha string) []*github.RepoStatus {
   104  		var oldStatus *github.RepoStatus
   105  		for _, status := range statuses {
   106  			if status.Context != nil && *status.Context == origContext {
   107  				oldStatus = &status
   108  				break
   109  			}
   110  		}
   111  		if oldStatus == nil {
   112  			// This means the conditions were not met! Should never have called this function, but it is a recoverable error.
   113  			glog.Errorf("failed to find original context in status list thus conditions for this duplicate action were not met. This should never happen!")
   114  			return nil
   115  		}
   116  		return []*github.RepoStatus{
   117  			{
   118  				Context:     &newContext,
   119  				State:       oldStatus.State,
   120  				TargetURL:   oldStatus.TargetURL,
   121  				Description: oldStatus.Description,
   122  			},
   123  		}
   124  	}
   125  }
   126  
   127  // retireAction creates a function that returns a retire action.
   128  // Specifically the returned function returns a RepoStatus that will update the origContext status
   129  // to 'success' and set it's description to mark it as retired and replaced by newContext.
   130  func retireAction(origContext, newContext string) func(statuses []github.RepoStatus, sha string) []*github.RepoStatus {
   131  	stateSuccess := "success"
   132  	var desc string
   133  	if newContext == "" {
   134  		desc = fmt.Sprint("Context retired without replacement.")
   135  	} else {
   136  		desc = fmt.Sprintf("Context retired. Status moved to \"%s\".", newContext)
   137  	}
   138  	return func(statuses []github.RepoStatus, sha string) []*github.RepoStatus {
   139  		return []*github.RepoStatus{
   140  			{
   141  				Context:     &origContext,
   142  				State:       &stateSuccess,
   143  				TargetURL:   nil,
   144  				Description: &desc,
   145  			},
   146  		}
   147  	}
   148  }
   149  
   150  // ProcessStatuses checks the mode against the combined status of a PR and emits the actions to take.
   151  func (m Mode) ProcessStatuses(combStatus *github.CombinedStatus) []*github.RepoStatus {
   152  	var sha string
   153  	if combStatus.SHA != nil {
   154  		sha = *combStatus.SHA
   155  	}
   156  
   157  	for _, cond := range m.conditions {
   158  		var match *github.RepoStatus
   159  		match = nil
   160  		for _, status := range combStatus.Statuses {
   161  			if status.Context == nil {
   162  				glog.Errorf("a status context for SHA ref '%s' had a nil Context field.", sha)
   163  				continue
   164  			}
   165  			if *status.Context == cond.context {
   166  				match = &status
   167  				break
   168  			}
   169  		}
   170  
   171  		switch cond.state {
   172  		case stateDNE:
   173  			if match != nil {
   174  				return nil
   175  			}
   176  		case stateAny:
   177  			if match == nil {
   178  				return nil
   179  			}
   180  		default:
   181  			// Looking for a specific state in this case.
   182  			if match == nil {
   183  				// Did not find the context.
   184  				return nil
   185  			}
   186  			if match.State == nil {
   187  				glog.Errorf("context '%s' of SHA ref '%s' has a nil state.", cond.context, sha)
   188  				return nil
   189  			}
   190  			if *match.State != cond.state {
   191  				// Context had a different state than what the condition requires.
   192  				return nil
   193  			}
   194  		}
   195  	}
   196  	return m.actions(combStatus.Statuses, sha)
   197  }
   198  
   199  type Migrator struct {
   200  	org             string
   201  	repo            string
   202  	continueOnError bool
   203  
   204  	client *ghclient.Client
   205  	Mode
   206  }
   207  
   208  func New(mode Mode, token, org, repo string, dryRun, continueOnError bool) *Migrator {
   209  	return &Migrator{
   210  		org:             org,
   211  		repo:            repo,
   212  		continueOnError: continueOnError,
   213  		client:          ghclient.NewClient(token, dryRun),
   214  		Mode:            mode,
   215  	}
   216  }
   217  
   218  func (m *Migrator) ProcessPR(pr *github.PullRequest) error {
   219  	if pr == nil {
   220  		return fmt.Errorf("migrator cannot process a nil PullRequest.")
   221  	}
   222  	if pr.Head == nil {
   223  		return fmt.Errorf("migrator cannot process a PullRequest with a nil 'Head' field.")
   224  	}
   225  	if pr.Head.SHA == nil {
   226  		return fmt.Errorf("migrator cannot process a PullRequest with a nil 'Head.SHA' field.")
   227  	}
   228  
   229  	combined, err := m.client.GetCombinedStatus(m.org, m.repo, *pr.Head.SHA)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	actions := m.ProcessStatuses(combined)
   234  
   235  	for _, action := range actions {
   236  		if _, err = m.client.CreateStatus(m.org, m.repo, *pr.Head.SHA, action); err != nil {
   237  			return err
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  func (m *Migrator) Migrate(prOptions *github.PullRequestListOptions) error {
   244  	return m.client.ForEachPR(m.org, m.repo, prOptions, m.continueOnError, m.ProcessPR)
   245  }