github.com/abayer/test-infra@v0.0.5/prow/cmd/mkbuild-cluster/main.go (about) 1 /* 2 Copyright 2018 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/base64" 21 "errors" 22 "flag" 23 "fmt" 24 "io/ioutil" 25 "os" 26 "os/exec" 27 "strings" 28 29 "github.com/ghodss/yaml" 30 "github.com/sirupsen/logrus" 31 32 "k8s.io/test-infra/prow/kube" 33 ) 34 35 const ( 36 useClientCertEnv = "CLOUDSDK_CONTAINER_USE_CLIENT_CERTIFICATE" 37 ) 38 39 var ( 40 coder = base64.StdEncoding 41 ) 42 43 type options struct { 44 account string 45 alias string 46 changeContext bool 47 cluster string 48 getClientCert bool 49 overwrite bool 50 printEntry bool 51 printData bool 52 project string 53 skipCheck bool 54 zone string 55 } 56 57 type describe struct { 58 Auth describeAuth `json:"masterAuth"` 59 Endpoint string `json:"endpoint"` 60 } 61 62 type describeAuth struct { 63 ClientCertificate string `json:"clientCertificate"` 64 ClientKey string `json:"clientKey"` 65 ClusterCACertificate string `json:"clusterCaCertificate"` 66 } 67 68 func parseOptions() options { 69 var o options 70 if err := o.parseArgs(flag.CommandLine, os.Args[1:]); err != nil { 71 logrus.Fatalf("Invalid flags: %v", err) 72 } 73 return o 74 } 75 76 func (o *options) parseArgs(flags *flag.FlagSet, args []string) error { 77 flags.StringVar(&o.account, "account", "", "use this account to describe --cluster") 78 flags.StringVar(&o.alias, "alias", "", "the --build-cluster alias to add") 79 flags.StringVar(&o.cluster, "cluster", "", "the GKE cluster to describe") 80 flags.StringVar(&o.project, "project", "", "the GKE project to describe") 81 flags.StringVar(&o.zone, "zone", "", "the GKE zone to describe") 82 flags.BoolVar(&o.printData, "print-file", false, "print the file outside of the configmap secret") 83 flags.BoolVar(&o.printEntry, "print-entry", false, "print the new entry without appending to existing ones at stdin") 84 flags.BoolVar(&o.getClientCert, "get-client-cert", false, fmt.Sprintf("first get-credentials for the cluster using %s=True", useClientCertEnv)) 85 flags.BoolVar(&o.changeContext, "change-context", false, "allow --get-client-cert to change kubectl config current-context") 86 flags.BoolVar(&o.skipCheck, "skip-check", false, "skip validating the creds work in a client") 87 switch err := flags.Parse(args); { 88 case err != nil: 89 return err 90 case o.cluster == "": 91 return errors.New("--cluster required") 92 case o.project == "": 93 return errors.New("--project required") 94 case o.zone == "": 95 return errors.New("--zone required") 96 case o.alias == "": 97 return fmt.Errorf("--alias required (use %q for default)", kube.DefaultClusterAlias) 98 } 99 return nil 100 } 101 102 func main() { 103 // Gather options from flags 104 o := parseOptions() 105 if err := do(o); err != nil { 106 logrus.Fatalf("Failed: %v", err) 107 } 108 } 109 110 // useContext calls kubectl config use-context ctx 111 func useContext(o options, ctx string) error { 112 _, cmd := command("kubectl", "config", "use-context", ctx) 113 return cmd.Run() 114 } 115 116 // currentContext returns kubectl config current-context 117 func currentContext(o options) (string, error) { 118 _, cmd := command("kubectl", "config", "current-context") 119 b, err := cmd.Output() 120 return strings.TrimSpace(string(b)), err 121 } 122 123 // getCredentials calls gcloud container clusters get-credentials, usually preserving currentContext() 124 func getCredentials(o options) error { 125 if !o.changeContext { 126 cur, err := currentContext(o) 127 if err != nil { 128 return fmt.Errorf("read current-context: %v", err) 129 } 130 defer useContext(o, cur) 131 } 132 133 // TODO(fejta): we ought to update kube.Client to support modern auth methods. 134 // More info: https://github.com/kubernetes/kubernetes/issues/30617 135 old, set := os.LookupEnv(useClientCertEnv) 136 if set { 137 defer os.Setenv(useClientCertEnv, old) 138 } 139 if err := os.Setenv("CLOUDSDK_CONTAINER_USE_CLIENT_CERTIFICATE", "True"); err != nil { 140 return fmt.Errorf("failed to set %s: %v", useClientCertEnv, err) 141 } 142 args, cmd := command( 143 "gcloud", "container", "clusters", "get-credentials", o.cluster, 144 "--project", o.project, 145 "--zone", o.zone, 146 ) 147 if err := cmd.Run(); err != nil { 148 return fmt.Errorf("%s: %v", strings.Join(args, " "), err) 149 } 150 return nil 151 } 152 153 // command creates an exec.Cmd with Stderr piped to os.Stderr and returns the args 154 func command(bin string, args ...string) ([]string, *exec.Cmd) { 155 cmd := exec.Command(bin, args...) 156 cmd.Stderr = os.Stderr 157 return append([]string{bin}, args...), cmd 158 } 159 160 // getAccount returns gcloud config get-value core/account 161 func getAccount() (string, error) { 162 args, cmd := command("gcloud", "config", "get-value", "core/account") 163 b, err := cmd.Output() 164 if err != nil { 165 return "", fmt.Errorf("%s: %v", strings.Join(args, " "), err) 166 } 167 return strings.TrimSpace(string(b)), nil 168 } 169 170 // setAccount calls gcloud config set core/account 171 func setAccount(account string) error { 172 _, cmd := command("gcloud", "config", "set", "core/account", account) 173 return cmd.Run() 174 } 175 176 // describeCluster returns details from gcloud container clusters describe. 177 func describeCluster(o options) (*describe, error) { 178 if o.account != "" { 179 act, err := getAccount() 180 if err != nil { 181 return nil, fmt.Errorf("get current account: %v", err) 182 } 183 defer setAccount(act) 184 if err = setAccount(o.account); err != nil { 185 return nil, fmt.Errorf("set account %s: %v", o.account, err) 186 } 187 } 188 args, cmd := command( 189 "gcloud", "container", "clusters", "describe", o.cluster, 190 "--project", o.project, 191 "--zone", o.zone, 192 "--format=yaml", 193 ) 194 data, err := cmd.Output() 195 if err != nil { 196 return nil, fmt.Errorf("%s: %v", strings.Join(args, " "), err) 197 } 198 var d describe 199 if yaml.Unmarshal(data, &d); err != nil { 200 return nil, fmt.Errorf("unmarshal gcloud: %v", err) 201 } 202 203 if d.Endpoint == "" { 204 return nil, errors.New("empty endpoint") 205 } 206 if d.Auth.ClusterCACertificate == "" { 207 return nil, errors.New("empty clusterCaCertificate") 208 } 209 if d.Auth.ClusterCACertificate, err = decode(d.Auth.ClusterCACertificate); err != nil { 210 return nil, fmt.Errorf("decode clusterCaCertificate") 211 } 212 213 if d.Auth.ClientKey == "" { 214 return nil, errors.New("empty clientKey, consider running with --get-client-cert") 215 } 216 if d.Auth.ClientKey, err = decode(d.Auth.ClientKey); err != nil { 217 return nil, fmt.Errorf("decode clientKey: %v", err) 218 } 219 if d.Auth.ClientCertificate == "" { 220 return nil, errors.New("empty clientCertificate, consider running with --get-client-cert") 221 } 222 if d.Auth.ClientCertificate, err = decode(d.Auth.ClientCertificate); err != nil { 223 return nil, fmt.Errorf("decode clientCertificate: %v", err) 224 } 225 226 return &d, nil 227 } 228 229 // decode returns the string encoded as the base64 in string. 230 func decode(in string) (string, error) { 231 out, err := coder.DecodeString(in) 232 return string(out), err 233 } 234 235 // decode returns in string encoded as a base64 string. 236 func encode(in string) string { 237 return coder.EncodeToString([]byte(in)) 238 } 239 240 // do will get creds for the specified cluster and add them to the stdin secret 241 func do(o options) error { 242 // Refresh credentials if requested 243 if o.getClientCert { 244 if err := getCredentials(o); err != nil { 245 return fmt.Errorf("get client cert: %v", err) 246 } 247 } 248 // Create the new cluster entry 249 d, err := describeCluster(o) 250 if err != nil { 251 return fmt.Errorf("describe auth: %v", err) 252 } 253 newCluster := kube.Cluster{ 254 Endpoint: "https://" + d.Endpoint, 255 ClusterCACertificate: encode(d.Auth.ClusterCACertificate), 256 ClientKey: encode(d.Auth.ClientKey), 257 ClientCertificate: encode(d.Auth.ClientCertificate), 258 } 259 260 // Try to use this entry 261 if !o.skipCheck { 262 c, err := kube.NewClient(&newCluster, "kube-system") 263 if err != nil { 264 return err 265 } 266 if _, err = c.ListPods("k8s-app=kube-dns"); err != nil { 267 return fmt.Errorf("authenticated client could not list pods: %v", err) 268 } 269 } 270 271 // Just print this entry if requested 272 if o.printEntry { 273 data, err := kube.MarshalClusterMap(map[string]kube.Cluster{o.alias: newCluster}) 274 if err != nil { 275 return fmt.Errorf("marshal %s: %v", o.alias, err) 276 } 277 fmt.Println(string(data)) 278 return nil 279 } 280 281 // Append the new entry to the current secret 282 283 // First read in the secret from stdin 284 b, err := ioutil.ReadAll(os.Stdin) 285 if err != nil { 286 return fmt.Errorf("read stdin: %v", err) 287 } 288 var s kube.Secret 289 if err := yaml.Unmarshal(b, &s); err != nil { 290 return fmt.Errorf("unmarshal stdin: %v", err) 291 } 292 293 // Now decode the {alias: cluster} map and print out current keys 294 clusters, err := kube.UnmarshalClusterMap(s.Data["cluster"]) 295 if err != nil { 296 return fmt.Errorf("unmarshal secret: %v", err) 297 } 298 var existing []string 299 for a := range clusters { 300 existing = append(existing, a) 301 } 302 logrus.Infof("Existing clusters: %s", strings.Join(existing, ", ")) 303 304 // Add new key 305 _, ok := clusters[o.alias] 306 if ok && !o.overwrite { 307 return fmt.Errorf("cluster %s already exists", o.alias) 308 } 309 clusters[o.alias] = newCluster 310 logrus.Infof("New cluster: %s", o.alias) 311 312 // Marshal the {alias: cluster} map back into secret data 313 data, err := kube.MarshalClusterMap(clusters) 314 if err != nil { 315 return fmt.Errorf("marshal clusters: %v", err) 316 } 317 318 if o.printData { // Just print the data outside of the secret 319 fmt.Println(string(data)) 320 return nil 321 } 322 323 // Output the new secret 324 s.Data["cluster"] = data 325 buf, err := yaml.Marshal(s) 326 if err != nil { 327 return fmt.Errorf("marshal secret: %v", err) 328 } 329 fmt.Println(string(buf)) 330 return nil 331 }