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 }