sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/statusreconciler/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 "github.com/golang/glog" 23 utilerrors "k8s.io/apimachinery/pkg/util/errors" 24 25 "sigs.k8s.io/prow/pkg/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.Status, sha string) []github.Status 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. A target URL 55 // to describe why the old context was migrated can optionally be provided, as well. 56 func MoveMode(origContext, newContext, targetURL string) *Mode { 57 dup := copyAction(origContext, newContext) 58 dep := retireAction(origContext, newContext, targetURL) 59 60 return &Mode{ 61 conditions: []*contextCondition{ 62 {context: origContext, state: stateAny}, 63 {context: newContext, state: stateDNE}, 64 }, 65 actions: func(statuses []github.Status, sha string) []github.Status { 66 return append(dup(statuses, sha), dep(statuses, sha)...) 67 }, 68 } 69 } 70 71 // CopyMode makes a mode that creates a new context in every PR that has the old context, but not the new one. 72 // The state, description and target URL of the new context are made the same as those of the old context. 73 func CopyMode(origContext, newContext string) *Mode { 74 return &Mode{ 75 conditions: []*contextCondition{ 76 {context: origContext, state: stateAny}, 77 {context: newContext, state: stateDNE}, 78 }, 79 actions: copyAction(origContext, newContext), 80 } 81 } 82 83 // RetireMode creates a mode that retires an old context on all PRs. 84 // If newContext is the empty string, origContext is retired without replacement. Its state is set to 85 // 'success' and its description is set to indicate that the context is retired. 86 // If newContext is not the empty string it is considered the replacement of origContext. This means 87 // that only PRs that have the newContext in addition to the origContext will be considered and the 88 // description of the retired context will indicate that it was replaced by newContext. A target URL 89 // to describe why the old context was migrated can optionally be provided, as well. 90 func RetireMode(origContext, newContext, targetURL string) *Mode { 91 conditions := []*contextCondition{{context: origContext, state: stateAny}} 92 if newContext != "" { 93 conditions = append(conditions, &contextCondition{context: newContext, state: stateAny}) 94 } 95 return &Mode{ 96 conditions: conditions, 97 actions: retireAction(origContext, newContext, targetURL), 98 } 99 } 100 101 // copyAction creates a function that returns a copy action. 102 // Specifically the returned function returns a RepoStatus that will create a status for newContext 103 // with state set to the state of origContext. 104 func copyAction(origContext, newContext string) func(statuses []github.Status, sha string) []github.Status { 105 return func(statuses []github.Status, sha string) []github.Status { 106 var oldStatus github.Status 107 var found bool 108 for _, status := range statuses { 109 if status.Context == origContext { 110 oldStatus = status 111 found = true 112 break 113 } 114 } 115 if !found { 116 // This means the conditions were not met! Should never have called this function, but it is a recoverable error. 117 glog.Error("failed to find original context in status list thus conditions for this duplicate action were not met. This should never happen!") 118 return nil 119 } 120 return []github.Status{ 121 { 122 Context: newContext, 123 State: oldStatus.State, 124 TargetURL: oldStatus.TargetURL, 125 Description: oldStatus.Description, 126 }, 127 } 128 } 129 } 130 131 // retireAction creates a function that returns a retire action. 132 // Specifically the returned function returns a RepoStatus that will update the origContext status 133 // to 'success' and set it's description to mark it as retired and replaced by newContext. 134 // If a non-empty URL is provided to describe why the context was retired, it will be 135 // set as the target URL for the context. 136 func retireAction(origContext, newContext, targetURL string) func(statuses []github.Status, sha string) []github.Status { 137 stateSuccess := "success" 138 var desc string 139 if newContext == "" { 140 desc = "Context retired without replacement." 141 } else { 142 desc = fmt.Sprintf("Context retired. Status moved to \"%s\".", newContext) 143 } 144 return func(statuses []github.Status, sha string) []github.Status { 145 return []github.Status{ 146 { 147 Context: origContext, 148 State: stateSuccess, 149 TargetURL: targetURL, 150 Description: desc, 151 }, 152 } 153 } 154 } 155 156 // processStatuses checks the mode against the combined status of a PR and emits the actions to take. 157 func (m Mode) processStatuses(combStatus *github.CombinedStatus) []github.Status { 158 for _, cond := range m.conditions { 159 var match github.Status 160 var found bool 161 for _, status := range combStatus.Statuses { 162 if status.Context == "" { 163 glog.Errorf("a status context for SHA ref '%s' had an empty Context field.", combStatus.SHA) 164 continue 165 } 166 if status.Context == cond.context { 167 match = status 168 found = true 169 break 170 } 171 } 172 173 switch cond.state { 174 case stateDNE: 175 if found { 176 return nil 177 } 178 case stateAny: 179 if !found { 180 return nil 181 } 182 default: 183 // Looking for a specific state in this case. 184 if !found { 185 // Did not find the context. 186 return nil 187 } 188 if match.State == "" { 189 glog.Errorf("context '%s' of SHA ref '%s' has an empty state.", cond.context, combStatus.SHA) 190 return nil 191 } 192 if match.State != cond.state { 193 // Context had a different state than what the condition requires. 194 return nil 195 } 196 } 197 } 198 return m.actions(combStatus.Statuses, combStatus.SHA) 199 } 200 201 type githubClient interface { 202 GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) 203 CreateStatus(org, repo, SHA string, s github.Status) error 204 GetPullRequests(org, repo string) ([]github.PullRequest, error) 205 } 206 207 // Migrator will search github for PRs with a given context and migrate/retire/move them. 208 type Migrator struct { 209 org string 210 repo string 211 212 targetBranchFilter func(string) bool 213 214 continueOnError bool 215 216 client githubClient 217 Mode 218 } 219 220 // New creates a new migrator with specified options and client. 221 func New(mode Mode, client github.Client, org, repo string, targetBranchFilter func(string) bool, continueOnError bool) *Migrator { 222 return &Migrator{ 223 org: org, 224 repo: repo, 225 targetBranchFilter: targetBranchFilter, 226 continueOnError: continueOnError, 227 client: client, 228 Mode: mode, 229 } 230 } 231 232 func (m *Migrator) processPR(pr github.PullRequest) error { 233 if !m.targetBranchFilter(pr.Base.Ref) { 234 return nil 235 } 236 237 combined, err := m.client.GetCombinedStatus(m.org, m.repo, pr.Head.SHA) 238 if err != nil { 239 return err 240 } 241 actions := m.processStatuses(combined) 242 243 for _, action := range actions { 244 if err := m.client.CreateStatus(m.org, m.repo, pr.Head.SHA, action); err != nil { 245 return err 246 } 247 } 248 return nil 249 } 250 251 // Migrate will retire/migrate/copy statuses for all matching PRs. 252 func (m *Migrator) Migrate() error { 253 prs, err := m.client.GetPullRequests(m.org, m.repo) 254 if err != nil { 255 return err 256 } 257 258 var errors []error 259 for _, pr := range prs { 260 if err := m.processPR(pr); err != nil { 261 if m.continueOnError { 262 errors = append(errors, err) 263 continue 264 } 265 return err 266 } 267 } 268 return utilerrors.NewAggregate(errors) 269 }