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  }