github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/caas/add.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package caas 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "reflect" 13 "strings" 14 "time" 15 16 "github.com/juju/cmd" 17 "github.com/juju/errors" 18 "github.com/juju/gnuflag" 19 "github.com/juju/loggo" 20 "github.com/juju/utils/set" 21 "golang.org/x/crypto/ssh/terminal" 22 "gopkg.in/juju/names.v2" 23 24 "github.com/juju/juju/api" 25 cloudapi "github.com/juju/juju/api/cloud" 26 "github.com/juju/juju/api/modelconfig" 27 "github.com/juju/juju/apiserver/params" 28 "github.com/juju/juju/caas" 29 "github.com/juju/juju/caas/kubernetes/clientconfig" 30 jujucloud "github.com/juju/juju/cloud" 31 jujucmd "github.com/juju/juju/cmd" 32 jujucmdcloud "github.com/juju/juju/cmd/juju/cloud" 33 "github.com/juju/juju/cmd/juju/common" 34 "github.com/juju/juju/cmd/modelcmd" 35 "github.com/juju/juju/environs" 36 "github.com/juju/juju/environs/config" 37 "github.com/juju/juju/jujuclient" 38 ) 39 40 var logger = loggo.GetLogger("juju.cmd.juju.k8s") 41 42 type CloudMetadataStore interface { 43 ParseCloudMetadataFile(path string) (map[string]jujucloud.Cloud, error) 44 ParseOneCloud(data []byte) (jujucloud.Cloud, error) 45 PublicCloudMetadata(searchPaths ...string) (result map[string]jujucloud.Cloud, fallbackUsed bool, _ error) 46 PersonalCloudMetadata() (map[string]jujucloud.Cloud, error) 47 WritePersonalCloudMetadata(cloudsMap map[string]jujucloud.Cloud) error 48 } 49 50 // AddCloudAPI - Implemented by cloudapi.Client. 51 type AddCloudAPI interface { 52 AddCloud(jujucloud.Cloud) error 53 AddCredential(tag string, credential jujucloud.Credential) error 54 Close() error 55 } 56 57 // BrokerGetter returns caas broker instance. 58 type BrokerGetter func(cloud jujucloud.Cloud, credential jujucloud.Credential) (k8sBrokerRegionLister, error) 59 60 type k8sBrokerRegionLister interface { 61 ListHostCloudRegions() (set.Strings, error) 62 } 63 64 var usageAddCAASSummary = ` 65 Adds a k8s endpoint and credential to Juju.`[1:] 66 67 var usageAddCAASDetails = ` 68 Creates a user-defined cloud and populate the selected controller with the k8s 69 cloud details. Speficify non default kubeconfig file location using $KUBECONFIG 70 environment variable or pipe in file content from stdin. 71 72 The config file can contain definitions for different k8s clusters, 73 use --cluster-name to pick which one to use. 74 It's also possible to select a context by name using --context-name. 75 76 When running add-k8s on JAAS and the cloud/region cannot be detected automatically, 77 use --region <cloudType/region> to specify the host cloud type and region. 78 79 When adding a GKE cluster, you can use the --gke option to interactively be stepped 80 through the registration process, or you can supply the necessary parameters directly. 81 82 Examples: 83 juju add-k8s myk8scloud 84 juju add-k8s --context-name mycontext myk8scloud 85 juju add-k8s myk8scloud --region <cloudType/region> 86 87 KUBECONFIG=path-to-kubuconfig-file juju add-k8s myk8scloud --cluster-name=my_cluster_name 88 kubectl config view --raw | juju add-k8s myk8scloud --cluster-name=my_cluster_name 89 90 juju add-k8s --gke myk8scloud 91 juju add-k8s --gke --project=myproject myk8scloud 92 juju add-k8s --gke --credential=myaccount --project=myproject myk8scloud 93 juju add-k8s --gke --credential=myaccount --project=myproject --region=someregion myk8scloud 94 95 See also: 96 remove-k8s 97 ` 98 99 // AddCAASCommand is the command that allows you to add a caas and credential 100 type AddCAASCommand struct { 101 modelcmd.ControllerCommandBase 102 103 // caasName is the name of the caas to add. 104 caasName string 105 106 // caasType is the type of CAAS being added. 107 caasType string 108 109 // clusterName is the name of the cluster (k8s) or credential to import. 110 clusterName string 111 112 // contextName is the name of the contex to import. 113 contextName string 114 115 // project is the project id for the cluster. 116 project string 117 118 // credential is the credential to use when accessing the cluster. 119 credential string 120 121 // hostCloudRegion is the cloud region that the nodes of cluster (k8s) are running in. 122 // The format is <cloudType/region>. 123 hostCloudRegion string 124 125 // brokerGetter returns caas broker instance. 126 brokerGetter BrokerGetter 127 128 gke bool 129 k8sCluster k8sCluster 130 131 cloudMetadataStore CloudMetadataStore 132 fileCredentialStore jujuclient.CredentialStore 133 addCloudAPIFunc func() (AddCloudAPI, error) 134 newClientConfigReader func(string) (clientconfig.ClientConfigFunc, error) 135 136 getAllCloudDetails func() (map[string]*jujucmdcloud.CloudDetails, error) 137 } 138 139 // NewAddCAASCommand returns a command to add caas information. 140 func NewAddCAASCommand(cloudMetadataStore CloudMetadataStore) cmd.Command { 141 cmd := &AddCAASCommand{ 142 cloudMetadataStore: cloudMetadataStore, 143 fileCredentialStore: jujuclient.NewFileCredentialStore(), 144 newClientConfigReader: func(caasType string) (clientconfig.ClientConfigFunc, error) { 145 return clientconfig.NewClientConfigReader(caasType) 146 }, 147 } 148 cmd.addCloudAPIFunc = func() (AddCloudAPI, error) { 149 root, err := cmd.NewAPIRoot() 150 if err != nil { 151 return nil, errors.Trace(err) 152 } 153 return cloudapi.NewClient(root), nil 154 } 155 156 cmd.brokerGetter = newK8sBrokerGetter(cmd.NewAPIRoot) 157 cmd.getAllCloudDetails = jujucmdcloud.GetAllCloudDetails 158 return modelcmd.WrapController(cmd) 159 } 160 161 // Info returns help information about the command. 162 func (c *AddCAASCommand) Info() *cmd.Info { 163 return jujucmd.Info(&cmd.Info{ 164 Name: "add-k8s", 165 Args: "<k8s name>", 166 Purpose: usageAddCAASSummary, 167 Doc: usageAddCAASDetails, 168 }) 169 } 170 171 // SetFlags initializes the flags supported by the command. 172 func (c *AddCAASCommand) SetFlags(f *gnuflag.FlagSet) { 173 c.CommandBase.SetFlags(f) 174 f.StringVar(&c.clusterName, "cluster-name", "", "Specify the k8s cluster to import") 175 f.StringVar(&c.contextName, "context-name", "", "Specify the k8s context to import") 176 f.StringVar(&c.hostCloudRegion, "region", "", "kubernetes cluster cloud and/or region") 177 f.StringVar(&c.project, "project", "", "project to which the cluster belongs") 178 f.StringVar(&c.credential, "credential", "", "the credential to use when accessing the cluster") 179 f.BoolVar(&c.gke, "gke", false, "used when adding a GKE cluster") 180 } 181 182 // Init populates the command with the args from the command line. 183 func (c *AddCAASCommand) Init(args []string) (err error) { 184 if len(args) == 0 { 185 return errors.Errorf("missing k8s name.") 186 } 187 c.caasType = "kubernetes" 188 c.caasName = args[0] 189 190 if c.contextName != "" && c.clusterName != "" { 191 return errors.New("only specify one of cluster-name or context-name, not both") 192 } 193 if c.gke { 194 if c.contextName != "" { 195 return errors.New("do not specify context name when adding a GKE cluster") 196 } 197 } else { 198 if c.project != "" { 199 return errors.New("do not specify project unless adding a GKE cluster") 200 } 201 if c.credential != "" { 202 return errors.New("do not specify credential unless adding a GKE cluster") 203 } 204 } 205 206 return cmd.CheckEmpty(args[1:]) 207 } 208 209 // getStdinPipe returns nil if the context's stdin is not a pipe. 210 func getStdinPipe(ctx *cmd.Context) (io.Reader, error) { 211 if stdIn, ok := ctx.Stdin.(*os.File); ok && !terminal.IsTerminal(int(stdIn.Fd())) { 212 // stdIn from pipe but not terminal 213 stat, err := stdIn.Stat() 214 if err != nil { 215 return nil, err 216 } 217 content, err := ioutil.ReadAll(stdIn) 218 if err != nil { 219 return nil, err 220 } 221 if (stat.Mode()&os.ModeCharDevice) == 0 && len(content) > 0 { 222 // workaround to get piped stdIn size because stat.Size() always == 0 223 return bytes.NewReader(content), nil 224 } 225 } 226 return nil, nil 227 } 228 229 func (c *AddCAASCommand) newCloudCredentialFromKubeConfig(reader io.Reader, contextName, clusterName string) (jujucloud.Cloud, jujucloud.Credential, clientconfig.Context, error) { 230 var credential jujucloud.Credential 231 var context clientconfig.Context 232 newCloud := jujucloud.Cloud{ 233 Name: c.caasName, 234 Type: c.caasType, 235 HostCloudRegion: c.hostCloudRegion, 236 } 237 clientConfigFunc, err := c.newClientConfigReader(c.caasType) 238 if err != nil { 239 return newCloud, credential, context, errors.Trace(err) 240 } 241 caasConfig, err := clientConfigFunc(reader, contextName, clusterName, clientconfig.EnsureK8sCredential) 242 if err != nil { 243 return newCloud, credential, context, errors.Trace(err) 244 } 245 logger.Debugf("caasConfig: %+v", caasConfig) 246 247 if len(caasConfig.Contexts) == 0 { 248 return newCloud, credential, context, errors.Errorf("No k8s cluster definitions found in config") 249 } 250 251 context = caasConfig.Contexts[reflect.ValueOf(caasConfig.Contexts).MapKeys()[0].Interface().(string)] 252 253 credential = caasConfig.Credentials[context.CredentialName] 254 newCloud.AuthTypes = []jujucloud.AuthType{credential.AuthType()} 255 currentCloud := caasConfig.Clouds[context.CloudName] 256 newCloud.Endpoint = currentCloud.Endpoint 257 258 cloudCAData, ok := currentCloud.Attributes["CAData"].(string) 259 if !ok { 260 return newCloud, credential, context, errors.Errorf("CAData attribute should be a string") 261 } 262 newCloud.CACertificates = []string{cloudCAData} 263 return newCloud, credential, context, nil 264 } 265 266 func (c *AddCAASCommand) getConfigReader(ctx *cmd.Context) (io.Reader, string, error) { 267 if !c.gke { 268 rdr, err := getStdinPipe(ctx) 269 return rdr, c.clusterName, err 270 } 271 p := &clusterParams{ 272 name: c.clusterName, 273 region: c.hostCloudRegion, 274 project: c.project, 275 credential: c.credential, 276 } 277 // TODO - add support for AKS etc 278 cluster := c.k8sCluster 279 if cluster == nil { 280 cluster = newGKECluster() 281 } 282 283 // If any items are missing, prompt for them. 284 if p.name == "" || p.project == "" || p.region == "" { 285 var err error 286 p, err = cluster.interactiveParams(ctx, p) 287 if err != nil { 288 return nil, "", errors.Trace(err) 289 } 290 } 291 c.clusterName = p.name 292 c.hostCloudRegion = cluster.cloud() + "/" + p.region 293 return cluster.getKubeConfig(p) 294 } 295 296 // Run is defined on the Command interface. 297 func (c *AddCAASCommand) Run(ctx *cmd.Context) error { 298 if err := c.verifyName(c.caasName); err != nil { 299 return errors.Trace(err) 300 } 301 rdr, clusterName, err := c.getConfigReader(ctx) 302 if err != nil { 303 return errors.Trace(err) 304 } 305 if closer, ok := rdr.(io.Closer); ok { 306 defer closer.Close() 307 } 308 newCloud, credential, context, err := c.newCloudCredentialFromKubeConfig(rdr, c.contextName, clusterName) 309 if err != nil { 310 return errors.Trace(err) 311 } 312 313 cloudClient, err := c.addCloudAPIFunc() 314 if err != nil { 315 return errors.Trace(err) 316 } 317 defer cloudClient.Close() 318 319 if err := c.addCloudToControllerWithRegion(cloudClient, newCloud); err != nil { 320 if !params.IsCodeCloudRegionRequired(err) { 321 return errors.Trace(err) 322 } 323 // try to fetch cloud and region then retry. 324 cloudRegion, err := c.getClusterRegion(ctx, newCloud, credential) 325 errMsg := ` 326 JAAS requires cloud and region information. But it's 327 not possible to fetch cluster region in this case, 328 please use --region to specify the cloud/region manually. 329 `[1:] 330 if err != nil { 331 return errors.Annotate(err, errMsg) 332 } 333 if cloudRegion == "" { 334 return errors.NewNotValid(nil, errMsg) 335 } 336 newCloud.HostCloudRegion = cloudRegion 337 if err := c.addCloudToControllerWithRegion(cloudClient, newCloud); err != nil { 338 return errors.Trace(err) 339 } 340 } 341 342 if err := addCloudToLocal(c.cloudMetadataStore, newCloud); err != nil { 343 return errors.Trace(err) 344 } 345 346 if err := c.addCredentialToLocal(c.caasName, credential, context.CredentialName); err != nil { 347 return errors.Trace(err) 348 } 349 350 if err := c.addCredentialToController(cloudClient, credential, context.CredentialName); err != nil { 351 return errors.Trace(err) 352 } 353 fmt.Fprintf(ctx.Stdout, "k8s substrate %q added as cloud %q\n", clusterName, c.caasName) 354 355 return nil 356 } 357 358 func (c *AddCAASCommand) addCloudToControllerWithRegion(apiClient AddCloudAPI, newCloud jujucloud.Cloud) (err error) { 359 if newCloud.HostCloudRegion != "" { 360 hostCloudRegion, err := c.validateCloudRegion(newCloud.HostCloudRegion) 361 if err != nil { 362 return errors.Trace(err) 363 } 364 newCloud.HostCloudRegion = hostCloudRegion 365 } 366 if err := addCloudToController(apiClient, newCloud); err != nil { 367 return errors.Trace(err) 368 } 369 return nil 370 } 371 372 func newK8sBrokerGetter(rootAPIGetter func() (api.Connection, error)) BrokerGetter { 373 return func(cloud jujucloud.Cloud, credential jujucloud.Credential) (k8sBrokerRegionLister, error) { 374 conn, err := rootAPIGetter() 375 if err != nil { 376 return nil, errors.Trace(err) 377 } 378 modelAPI := modelconfig.NewClient(conn) 379 defer modelAPI.Close() 380 381 // Use the controller model config for constructing the Juju k8s client. 382 attrs, err := modelAPI.ModelGet() 383 if err != nil { 384 return nil, errors.Trace(err) 385 } 386 cfg, err := config.New(config.NoDefaults, attrs) 387 if err != nil { 388 return nil, errors.Trace(err) 389 } 390 391 cloudSpec, err := environs.MakeCloudSpec(cloud, "", &credential) 392 if err != nil { 393 return nil, errors.Trace(err) 394 } 395 return caas.New(environs.OpenParams{Cloud: cloudSpec, Config: cfg}) 396 } 397 } 398 399 func parseCloudRegion(cloudRegion string) (string, string, error) { 400 fields := strings.SplitN(cloudRegion, "/", 2) 401 if len(fields) != 2 || fields[0] == "" || fields[1] == "" { 402 return "", "", errors.NotValidf("cloud region %s", cloudRegion) 403 } 404 return fields[0], fields[1], nil 405 } 406 407 func (c *AddCAASCommand) validateCloudRegion(cloudRegion string) (_ string, err error) { 408 defer errors.DeferredAnnotatef(&err, "validating cloud region %q", cloudRegion) 409 410 cloudNameOrType, region, err := parseCloudRegion(cloudRegion) 411 if err != nil { 412 return "", errors.Annotate(err, "parsing cloud region") 413 } 414 415 clouds, err := c.getAllCloudDetails() 416 if err != nil { 417 return "", errors.Annotate(err, "listing cloud regions") 418 } 419 for name, details := range clouds { 420 // User may have specified cloud name or type so match on both. 421 if name == cloudNameOrType || details.CloudType == cloudNameOrType { 422 for k := range details.RegionsMap { 423 if k == region { 424 logger.Debugf("cloud region %q is valid", cloudRegion) 425 return details.CloudType + "/" + region, nil 426 } 427 } 428 } 429 } 430 return "", errors.NotValidf("cloud region %s", cloudRegion) 431 } 432 433 func (c *AddCAASCommand) getClusterRegion( 434 ctx *cmd.Context, 435 cloud jujucloud.Cloud, 436 credential jujucloud.Credential, 437 ) (string, error) { 438 broker, err := c.brokerGetter(cloud, credential) 439 if err != nil { 440 return "", errors.Trace(err) 441 } 442 443 interrupted := make(chan os.Signal, 1) 444 defer close(interrupted) 445 ctx.InterruptNotify(interrupted) 446 defer ctx.StopInterruptNotify(interrupted) 447 448 result := make(chan string, 1) 449 errChan := make(chan error, 1) 450 go func() { 451 cloudRegions, err := broker.ListHostCloudRegions() 452 if err != nil { 453 errChan <- err 454 } 455 if cloudRegions == nil || cloudRegions.Size() == 0 { 456 result <- "" 457 } else { 458 // we currently assume it's always a single region cluster. 459 result <- cloudRegions.SortedValues()[0] 460 } 461 }() 462 463 timeout := 30 * time.Second 464 defer fmt.Fprintln(ctx.Stdout, "") 465 for { 466 select { 467 case <-time.After(1 * time.Second): 468 fmt.Fprintf(ctx.Stdout, ".") 469 case <-interrupted: 470 ctx.Infof("ctrl+c detected, aborting...") 471 return "", nil 472 case <-time.After(timeout): 473 return "", errors.Timeoutf("timeout after %v", timeout) 474 case err := <-errChan: 475 return "", err 476 case cloudRegion := <-result: 477 return cloudRegion, nil 478 } 479 } 480 } 481 482 func (c *AddCAASCommand) verifyName(name string) error { 483 public, _, err := c.cloudMetadataStore.PublicCloudMetadata() 484 if err != nil { 485 return err 486 } 487 msg, err := nameExists(name, public) 488 if err != nil { 489 return errors.Trace(err) 490 } 491 if msg != "" { 492 return errors.Errorf(msg) 493 } 494 return nil 495 } 496 497 // nameExists returns either an empty string if the name does not exist, or a 498 // non-empty string with an error message if it does exist. 499 func nameExists(name string, public map[string]jujucloud.Cloud) (string, error) { 500 if _, ok := public[name]; ok { 501 return fmt.Sprintf("%q is the name of a public cloud", name), nil 502 } 503 builtin, err := common.BuiltInClouds() 504 if err != nil { 505 return "", errors.Trace(err) 506 } 507 if _, ok := builtin[name]; ok { 508 return fmt.Sprintf("%q is the name of a built-in cloud", name), nil 509 } 510 return "", nil 511 } 512 513 func addCloudToLocal(cloudMetadataStore CloudMetadataStore, newCloud jujucloud.Cloud) error { 514 personalClouds, err := cloudMetadataStore.PersonalCloudMetadata() 515 if err != nil { 516 return err 517 } 518 if personalClouds == nil { 519 personalClouds = make(map[string]jujucloud.Cloud) 520 } 521 personalClouds[newCloud.Name] = newCloud 522 return cloudMetadataStore.WritePersonalCloudMetadata(personalClouds) 523 } 524 525 func addCloudToController(apiClient AddCloudAPI, newCloud jujucloud.Cloud) error { 526 err := apiClient.AddCloud(newCloud) 527 if err != nil { 528 return errors.Trace(err) 529 } 530 return nil 531 } 532 533 func (c *AddCAASCommand) addCredentialToLocal(cloudName string, newCredential jujucloud.Credential, credentialName string) error { 534 newCredentials := &jujucloud.CloudCredential{ 535 AuthCredentials: make(map[string]jujucloud.Credential), 536 } 537 newCredentials.AuthCredentials[credentialName] = newCredential 538 err := c.fileCredentialStore.UpdateCredential(cloudName, *newCredentials) 539 if err != nil { 540 return errors.Trace(err) 541 } 542 return nil 543 } 544 545 func (c *AddCAASCommand) addCredentialToController(apiClient AddCloudAPI, newCredential jujucloud.Credential, credentialName string) error { 546 currentAccountDetails, err := c.CurrentAccountDetails() 547 if err != nil { 548 return errors.Trace(err) 549 } 550 551 cloudCredTag := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", 552 c.caasName, currentAccountDetails.User, credentialName)) 553 554 if err := apiClient.AddCredential(cloudCredTag.String(), newCredential); err != nil { 555 return errors.Trace(err) 556 } 557 return nil 558 }