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