github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/builtin/provisioners/habitat/resource_provisioner.go (about) 1 package habitat 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "errors" 7 "fmt" 8 "io" 9 "net/url" 10 "strings" 11 12 version "github.com/hashicorp/go-version" 13 "github.com/hashicorp/terraform/communicator" 14 "github.com/hashicorp/terraform/communicator/remote" 15 "github.com/hashicorp/terraform/configs/hcl2shim" 16 "github.com/hashicorp/terraform/helper/schema" 17 "github.com/hashicorp/terraform/helper/validation" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/mitchellh/go-linereader" 20 ) 21 22 type provisioner struct { 23 Version string 24 AutoUpdate bool 25 HttpDisable bool 26 Services []Service 27 PermanentPeer bool 28 ListenCtl string 29 ListenGossip string 30 ListenHTTP string 31 Peer string 32 Peers []string 33 RingKey string 34 RingKeyContent string 35 CtlSecret string 36 SkipInstall bool 37 UseSudo bool 38 ServiceType string 39 ServiceName string 40 URL string 41 Channel string 42 Events string 43 Organization string 44 GatewayAuthToken string 45 BuilderAuthToken string 46 SupOptions string 47 AcceptLicense bool 48 49 installHabitat provisionFn 50 startHabitat provisionFn 51 uploadRingKey provisionFn 52 uploadCtlSecret provisionFn 53 startHabitatService provisionServiceFn 54 55 osType string 56 } 57 58 type provisionFn func(terraform.UIOutput, communicator.Communicator) error 59 type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error 60 61 func Provisioner() terraform.ResourceProvisioner { 62 return &schema.Provisioner{ 63 Schema: map[string]*schema.Schema{ 64 "version": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 }, 68 "auto_update": &schema.Schema{ 69 Type: schema.TypeBool, 70 Optional: true, 71 Default: false, 72 }, 73 "http_disable": &schema.Schema{ 74 Type: schema.TypeBool, 75 Optional: true, 76 Default: false, 77 }, 78 "peer": &schema.Schema{ 79 Type: schema.TypeString, 80 Optional: true, 81 }, 82 "peers": &schema.Schema{ 83 Type: schema.TypeList, 84 Elem: &schema.Schema{Type: schema.TypeString}, 85 Optional: true, 86 }, 87 "service_type": &schema.Schema{ 88 Type: schema.TypeString, 89 Optional: true, 90 Default: "systemd", 91 ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false), 92 }, 93 "service_name": &schema.Schema{ 94 Type: schema.TypeString, 95 Optional: true, 96 Default: "hab-supervisor", 97 }, 98 "use_sudo": &schema.Schema{ 99 Type: schema.TypeBool, 100 Optional: true, 101 Default: true, 102 }, 103 "accept_license": &schema.Schema{ 104 Type: schema.TypeBool, 105 Required: true, 106 }, 107 "permanent_peer": &schema.Schema{ 108 Type: schema.TypeBool, 109 Optional: true, 110 Default: false, 111 }, 112 "listen_ctl": &schema.Schema{ 113 Type: schema.TypeString, 114 Optional: true, 115 }, 116 "listen_gossip": &schema.Schema{ 117 Type: schema.TypeString, 118 Optional: true, 119 }, 120 "listen_http": &schema.Schema{ 121 Type: schema.TypeString, 122 Optional: true, 123 }, 124 "ring_key": &schema.Schema{ 125 Type: schema.TypeString, 126 Optional: true, 127 }, 128 "ring_key_content": &schema.Schema{ 129 Type: schema.TypeString, 130 Optional: true, 131 }, 132 "ctl_secret": &schema.Schema{ 133 Type: schema.TypeString, 134 Optional: true, 135 }, 136 "url": &schema.Schema{ 137 Type: schema.TypeString, 138 Optional: true, 139 ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { 140 u, err := url.Parse(val.(string)) 141 if err != nil { 142 errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err)) 143 } 144 145 if u.Scheme == "" { 146 errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key)) 147 } 148 149 return warns, errs 150 }, 151 }, 152 "channel": &schema.Schema{ 153 Type: schema.TypeString, 154 Optional: true, 155 }, 156 "events": &schema.Schema{ 157 Type: schema.TypeString, 158 Optional: true, 159 }, 160 "organization": &schema.Schema{ 161 Type: schema.TypeString, 162 Optional: true, 163 }, 164 "gateway_auth_token": &schema.Schema{ 165 Type: schema.TypeString, 166 Optional: true, 167 }, 168 "builder_auth_token": &schema.Schema{ 169 Type: schema.TypeString, 170 Optional: true, 171 }, 172 "service": &schema.Schema{ 173 Type: schema.TypeSet, 174 Elem: &schema.Resource{ 175 Schema: map[string]*schema.Schema{ 176 "name": &schema.Schema{ 177 Type: schema.TypeString, 178 Required: true, 179 }, 180 "binds": &schema.Schema{ 181 Type: schema.TypeList, 182 Elem: &schema.Schema{Type: schema.TypeString}, 183 Optional: true, 184 }, 185 "bind": &schema.Schema{ 186 Type: schema.TypeSet, 187 Elem: &schema.Resource{ 188 Schema: map[string]*schema.Schema{ 189 "alias": &schema.Schema{ 190 Type: schema.TypeString, 191 Required: true, 192 }, 193 "service": &schema.Schema{ 194 Type: schema.TypeString, 195 Required: true, 196 }, 197 "group": &schema.Schema{ 198 Type: schema.TypeString, 199 Required: true, 200 }, 201 }, 202 }, 203 Optional: true, 204 }, 205 "topology": &schema.Schema{ 206 Type: schema.TypeString, 207 Optional: true, 208 ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false), 209 }, 210 "user_toml": &schema.Schema{ 211 Type: schema.TypeString, 212 Optional: true, 213 }, 214 "strategy": &schema.Schema{ 215 Type: schema.TypeString, 216 Optional: true, 217 ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false), 218 }, 219 "channel": &schema.Schema{ 220 Type: schema.TypeString, 221 Optional: true, 222 }, 223 "group": &schema.Schema{ 224 Type: schema.TypeString, 225 Optional: true, 226 }, 227 "url": &schema.Schema{ 228 Type: schema.TypeString, 229 Optional: true, 230 ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { 231 u, err := url.Parse(val.(string)) 232 if err != nil { 233 errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err)) 234 } 235 236 if u.Scheme == "" { 237 errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key)) 238 } 239 240 return warns, errs 241 }, 242 }, 243 "application": &schema.Schema{ 244 Type: schema.TypeString, 245 Optional: true, 246 }, 247 "environment": &schema.Schema{ 248 Type: schema.TypeString, 249 Optional: true, 250 }, 251 "service_key": &schema.Schema{ 252 Type: schema.TypeString, 253 Optional: true, 254 }, 255 }, 256 }, 257 Optional: true, 258 }, 259 }, 260 ApplyFunc: applyFn, 261 ValidateFunc: validateFn, 262 } 263 } 264 265 func applyFn(ctx context.Context) error { 266 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 267 s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 268 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 269 270 p, err := decodeConfig(d) 271 if err != nil { 272 return err 273 } 274 275 // Automatically determine the OS type 276 switch t := s.Ephemeral.ConnInfo["type"]; t { 277 case "ssh", "": 278 p.osType = "linux" 279 case "winrm": 280 p.osType = "windows" 281 default: 282 return fmt.Errorf("unsupported connection type: %s", t) 283 } 284 285 switch p.osType { 286 case "linux": 287 p.installHabitat = p.linuxInstallHabitat 288 p.uploadRingKey = p.linuxUploadRingKey 289 p.uploadCtlSecret = p.linuxUploadCtlSecret 290 p.startHabitat = p.linuxStartHabitat 291 p.startHabitatService = p.linuxStartHabitatService 292 case "windows": 293 return fmt.Errorf("windows is not supported yet for the habitat provisioner") 294 default: 295 return fmt.Errorf("unsupported os type: %s", p.osType) 296 } 297 298 // Get a new communicator 299 comm, err := communicator.New(s) 300 if err != nil { 301 return err 302 } 303 304 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 305 defer cancel() 306 307 // Wait and retry until we establish the connection 308 err = communicator.Retry(retryCtx, func() error { 309 return comm.Connect(o) 310 }) 311 312 if err != nil { 313 return err 314 } 315 defer comm.Disconnect() 316 317 if !p.SkipInstall { 318 o.Output("Installing habitat...") 319 if err := p.installHabitat(o, comm); err != nil { 320 return err 321 } 322 } 323 324 if p.RingKeyContent != "" { 325 o.Output("Uploading supervisor ring key...") 326 if err := p.uploadRingKey(o, comm); err != nil { 327 return err 328 } 329 } 330 331 if p.CtlSecret != "" { 332 o.Output("Uploading ctl secret...") 333 if err := p.uploadCtlSecret(o, comm); err != nil { 334 return err 335 } 336 } 337 338 o.Output("Starting the habitat supervisor...") 339 if err := p.startHabitat(o, comm); err != nil { 340 return err 341 } 342 343 if p.Services != nil { 344 for _, service := range p.Services { 345 o.Output("Starting service: " + service.Name) 346 if err := p.startHabitatService(o, comm, service); err != nil { 347 return err 348 } 349 } 350 } 351 352 return nil 353 } 354 355 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 356 ringKeyContent, ok := c.Get("ring_key_content") 357 if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue { 358 ringKey, ringOk := c.Get("ring_key") 359 if ringOk && ringKey == "" { 360 es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well")) 361 } 362 } 363 364 v, ok := c.Get("version") 365 if ok && v != nil && strings.TrimSpace(v.(string)) != "" { 366 if _, err := version.NewVersion(v.(string)); err != nil { 367 es = append(es, errors.New(v.(string)+" is not a valid version.")) 368 } 369 } 370 371 acceptLicense, ok := c.Get("accept_license") 372 if ok && !acceptLicense.(bool) { 373 if v != nil && strings.TrimSpace(v.(string)) != "" { 374 versionOld, _ := version.NewVersion("0.79.0") 375 versionRequired, _ := version.NewVersion(v.(string)) 376 if versionRequired.GreaterThan(versionOld) { 377 es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept")) 378 } 379 } else { // blank means latest version 380 es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept")) 381 } 382 } 383 384 // Validate service level configs 385 services, ok := c.Get("service") 386 if ok { 387 data, dataOk := services.(string) 388 if dataOk { 389 es = append(es, fmt.Errorf("service '%v': must be a block", data)) 390 } 391 } 392 393 return ws, es 394 } 395 396 type Service struct { 397 Name string 398 Strategy string 399 Topology string 400 Channel string 401 Group string 402 URL string 403 Binds []Bind 404 BindStrings []string 405 UserTOML string 406 AppName string 407 Environment string 408 ServiceGroupKey string 409 } 410 411 func (s *Service) getPackageName(fullName string) string { 412 return strings.Split(fullName, "/")[1] 413 } 414 415 func (s *Service) getServiceNameChecksum() string { 416 return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name))) 417 } 418 419 type Bind struct { 420 Alias string 421 Service string 422 Group string 423 } 424 425 func (b *Bind) toBindString() string { 426 return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group) 427 } 428 429 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 430 p := &provisioner{ 431 Version: d.Get("version").(string), 432 AutoUpdate: d.Get("auto_update").(bool), 433 HttpDisable: d.Get("http_disable").(bool), 434 Peer: d.Get("peer").(string), 435 Peers: getPeers(d.Get("peers").([]interface{})), 436 Services: getServices(d.Get("service").(*schema.Set).List()), 437 UseSudo: d.Get("use_sudo").(bool), 438 AcceptLicense: d.Get("accept_license").(bool), 439 ServiceType: d.Get("service_type").(string), 440 ServiceName: d.Get("service_name").(string), 441 RingKey: d.Get("ring_key").(string), 442 RingKeyContent: d.Get("ring_key_content").(string), 443 CtlSecret: d.Get("ctl_secret").(string), 444 PermanentPeer: d.Get("permanent_peer").(bool), 445 ListenCtl: d.Get("listen_ctl").(string), 446 ListenGossip: d.Get("listen_gossip").(string), 447 ListenHTTP: d.Get("listen_http").(string), 448 URL: d.Get("url").(string), 449 Channel: d.Get("channel").(string), 450 Events: d.Get("events").(string), 451 Organization: d.Get("organization").(string), 452 BuilderAuthToken: d.Get("builder_auth_token").(string), 453 GatewayAuthToken: d.Get("gateway_auth_token").(string), 454 } 455 456 return p, nil 457 } 458 459 func getPeers(v []interface{}) []string { 460 peers := make([]string, 0, len(v)) 461 for _, rawPeerData := range v { 462 peers = append(peers, rawPeerData.(string)) 463 } 464 return peers 465 } 466 467 func getServices(v []interface{}) []Service { 468 services := make([]Service, 0, len(v)) 469 for _, rawServiceData := range v { 470 serviceData := rawServiceData.(map[string]interface{}) 471 name := (serviceData["name"].(string)) 472 strategy := (serviceData["strategy"].(string)) 473 topology := (serviceData["topology"].(string)) 474 channel := (serviceData["channel"].(string)) 475 group := (serviceData["group"].(string)) 476 url := (serviceData["url"].(string)) 477 app := (serviceData["application"].(string)) 478 env := (serviceData["environment"].(string)) 479 userToml := (serviceData["user_toml"].(string)) 480 serviceGroupKey := (serviceData["service_key"].(string)) 481 var bindStrings []string 482 binds := getBinds(serviceData["bind"].(*schema.Set).List()) 483 for _, b := range serviceData["binds"].([]interface{}) { 484 bind, err := getBindFromString(b.(string)) 485 if err != nil { 486 return nil 487 } 488 binds = append(binds, bind) 489 } 490 491 service := Service{ 492 Name: name, 493 Strategy: strategy, 494 Topology: topology, 495 Channel: channel, 496 Group: group, 497 URL: url, 498 UserTOML: userToml, 499 BindStrings: bindStrings, 500 Binds: binds, 501 AppName: app, 502 Environment: env, 503 ServiceGroupKey: serviceGroupKey, 504 } 505 services = append(services, service) 506 } 507 return services 508 } 509 510 func getBinds(v []interface{}) []Bind { 511 binds := make([]Bind, 0, len(v)) 512 for _, rawBindData := range v { 513 bindData := rawBindData.(map[string]interface{}) 514 alias := bindData["alias"].(string) 515 service := bindData["service"].(string) 516 group := bindData["group"].(string) 517 bind := Bind{ 518 Alias: alias, 519 Service: service, 520 Group: group, 521 } 522 binds = append(binds, bind) 523 } 524 return binds 525 } 526 527 func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) { 528 lr := linereader.New(r) 529 for line := range lr.Ch { 530 o.Output(line) 531 } 532 } 533 534 func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error { 535 outR, outW := io.Pipe() 536 errR, errW := io.Pipe() 537 538 go p.copyOutput(o, outR) 539 go p.copyOutput(o, errR) 540 defer outW.Close() 541 defer errW.Close() 542 543 cmd := &remote.Cmd{ 544 Command: command, 545 Stdout: outW, 546 Stderr: errW, 547 } 548 549 if err := comm.Start(cmd); err != nil { 550 return fmt.Errorf("error executing command %q: %v", cmd.Command, err) 551 } 552 553 if err := cmd.Wait(); err != nil { 554 return err 555 } 556 557 return nil 558 } 559 560 func getBindFromString(bind string) (Bind, error) { 561 t := strings.FieldsFunc(bind, func(d rune) bool { 562 switch d { 563 case ':', '.': 564 return true 565 } 566 return false 567 }) 568 if len(t) != 3 { 569 return Bind{}, errors.New("invalid bind specification: " + bind) 570 } 571 return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil 572 }