k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/experiment/slack-oncall-updater/main.go (about) 1 /* 2 Copyright 2019 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 main 18 19 import ( 20 "encoding/json" 21 "flag" 22 "fmt" 23 "log" 24 "net/http" 25 "os" 26 "strings" 27 ) 28 29 // githubToSlack maps (lowercase!) GitHub usernames to Slack user IDs. 30 // Add yourself when you join the oncall rotation. 31 // You can mostly easily find your user ID by visiting your Slack profile, clicking "...", 32 // and clicking "Copy user ID". 33 var githubToSlack = map[string]string{ 34 "airbornepony": "U057DPPH17U", 35 "chases2": "UJ9R0FWD6", 36 "cjwagner": "U4QFZFMCM", 37 "michelle192837": "U3TRY5WV7", 38 "mpherman2": "U01AJA1N8NL", 39 "timwangmusic": "U05UKSMCY8H", 40 } 41 42 // rotationToSlack group maps the rotation in go.k8s.io/oncall to the slack 43 // group ID for the oncall alias group for this rotation 44 var rotationToSlackGroup = map[string]string{ 45 "testinfra": "SGLF0GUQH", // @test-infra-oncall 46 "google-build-admin": "S017N31TLNN", // @google-build-admin 47 // TODO: create this slack group? 48 // NOTE: If anyone adds this group, you must also send a PR like: 49 // https://github.com/kubernetes-sigs/slack-infra/pull/36 50 // "scalability": "", 51 } 52 53 func getJSON(url string, v interface{}) error { 54 resp, err := http.Get(url) 55 if err != nil { 56 return fmt.Errorf("failed to fetch %q: %w", url, err) 57 } 58 defer resp.Body.Close() 59 decoder := json.NewDecoder(resp.Body) 60 err = decoder.Decode(v) 61 if err != nil { 62 return fmt.Errorf("failed to decode json from %q: %w", url, err) 63 } 64 return nil 65 } 66 67 func getCurrentOncallers() (map[string]string, error) { 68 oncallBlob := struct { 69 Oncall map[string]string `json:"Oncall"` 70 }{} 71 72 err := getJSON("https://storage.googleapis.com/kubernetes-jenkins/oncall.json", &oncallBlob) 73 if err != nil { 74 return nil, err 75 } 76 77 return oncallBlob.Oncall, nil 78 } 79 80 func updateGroupMembership(token, groupID, userID string) error { 81 result := struct { 82 Ok bool `json:"ok"` 83 Error string `json:"error,omitempty"` 84 }{} 85 86 err := getJSON("https://slack.com/api/usergroups.users.update?token="+token+"&users="+userID+"&usergroup="+groupID, &result) 87 if err != nil { 88 return fmt.Errorf("couldn't make membership request: %w", err) 89 } 90 91 if !result.Ok { 92 return fmt.Errorf("couldn't update membership: %s", result.Error) 93 } 94 95 return nil 96 } 97 98 func getTokenFromPath(path string) (string, error) { 99 content, err := os.ReadFile(path) 100 if err != nil { 101 return "", fmt.Errorf("couldn't open file: %w", err) 102 } 103 return strings.TrimSpace(string(content)), nil 104 } 105 106 type options struct { 107 tokenPath string 108 } 109 110 func parseFlags() options { 111 o := options{} 112 flag.StringVar(&o.tokenPath, "token-path", "/etc/slack-token", "Path to a file containing the slack token") 113 flag.Parse() 114 return o 115 } 116 117 func main() { 118 o := parseFlags() 119 120 token, err := getTokenFromPath(o.tokenPath) 121 if err != nil { 122 log.Fatalf("Failed to get Slack token: %v", err) 123 } 124 125 oncallers, err := getCurrentOncallers() 126 if err != nil { 127 log.Fatalf("Failed to find current oncallers: %v", err) 128 } 129 log.Printf("Current oncallers: %s\n", oncallers) 130 131 failed := false 132 for rotation, user := range oncallers { 133 // skip rotations that have not yet added their slack ID 134 // this tool is not required 135 groupID, exists := rotationToSlackGroup[rotation] 136 if !exists { 137 log.Printf("Rotation %q does not yet have a Group ID, skipping ...", rotation) 138 continue 139 } 140 141 // get the slack ID for the current oncall in this rotation 142 userID, exists := githubToSlack[strings.ToLower(user)] 143 if !exists { 144 log.Printf("Failed to get Slack ID for GitHub user %q", user) 145 // continue with other rotations, but exit error afterwards 146 failed = true 147 continue 148 } 149 log.Printf("%s's slack ID: %s\n", user, userID) 150 151 // update this rotation's slack instance 152 log.Printf("Adding slack user %s to slack usergroup %s", userID, groupID) 153 if err := updateGroupMembership(token, groupID, userID); err != nil { 154 log.Fatalf("Failed to update usergroup membership: %v", err) 155 } 156 } 157 // if one of the group updates failed, fail the run now 158 if failed { 159 log.Fatal("Failed to update some rotation(s)") 160 } 161 162 log.Printf("Done!") 163 }