github.com/hugorut/terraform@v1.1.3/src/terraform/context.go (about) 1 package terraform 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "sort" 8 "sync" 9 10 "github.com/hugorut/terraform/src/addrs" 11 "github.com/hugorut/terraform/src/configs" 12 "github.com/hugorut/terraform/src/logging" 13 "github.com/hugorut/terraform/src/providers" 14 "github.com/hugorut/terraform/src/provisioners" 15 "github.com/hugorut/terraform/src/states" 16 "github.com/hugorut/terraform/src/tfdiags" 17 "github.com/zclconf/go-cty/cty" 18 19 _ "github.com/hugorut/terraform/src/logging" 20 ) 21 22 // InputMode defines what sort of input will be asked for when Input 23 // is called on Context. 24 type InputMode byte 25 26 const ( 27 // InputModeProvider asks for provider variables 28 InputModeProvider InputMode = 1 << iota 29 30 // InputModeStd is the standard operating mode and asks for both variables 31 // and providers. 32 InputModeStd = InputModeProvider 33 ) 34 35 // ContextOpts are the user-configurable options to create a context with 36 // NewContext. 37 type ContextOpts struct { 38 Meta *ContextMeta 39 Hooks []Hook 40 Parallelism int 41 Providers map[addrs.Provider]providers.Factory 42 Provisioners map[string]provisioners.Factory 43 44 UIInput UIInput 45 } 46 47 // ContextMeta is metadata about the running context. This is information 48 // that this package or structure cannot determine on its own but exposes 49 // into Terraform in various ways. This must be provided by the Context 50 // initializer. 51 type ContextMeta struct { 52 Env string // Env is the state environment 53 54 // OriginalWorkingDir is the working directory where the Terraform CLI 55 // was run from, which may no longer actually be the current working 56 // directory if the user included the -chdir=... option. 57 // 58 // If this string is empty then the original working directory is the same 59 // as the current working directory. 60 // 61 // In most cases we should respect the user's override by ignoring this 62 // path and just using the current working directory, but this is here 63 // for some exceptional cases where the original working directory is 64 // needed. 65 OriginalWorkingDir string 66 } 67 68 // Context represents all the context that Terraform needs in order to 69 // perform operations on infrastructure. This structure is built using 70 // NewContext. 71 type Context struct { 72 // meta captures some misc. information about the working directory where 73 // we're taking these actions, and thus which should remain steady between 74 // operations. 75 meta *ContextMeta 76 77 plugins *contextPlugins 78 79 hooks []Hook 80 sh *stopHook 81 uiInput UIInput 82 83 l sync.Mutex // Lock acquired during any task 84 parallelSem Semaphore 85 providerInputConfig map[string]map[string]cty.Value 86 runCond *sync.Cond 87 runContext context.Context 88 runContextCancel context.CancelFunc 89 } 90 91 // (additional methods on Context can be found in context_*.go files.) 92 93 // NewContext creates a new Context structure. 94 // 95 // Once a Context is created, the caller must not access or mutate any of 96 // the objects referenced (directly or indirectly) by the ContextOpts fields. 97 // 98 // If the returned diagnostics contains errors then the resulting context is 99 // invalid and must not be used. 100 func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { 101 var diags tfdiags.Diagnostics 102 103 log.Printf("[TRACE] terraform.NewContext: starting") 104 105 // Copy all the hooks and add our stop hook. We don't append directly 106 // to the Config so that we're not modifying that in-place. 107 sh := new(stopHook) 108 hooks := make([]Hook, len(opts.Hooks)+1) 109 copy(hooks, opts.Hooks) 110 hooks[len(opts.Hooks)] = sh 111 112 // Determine parallelism, default to 10. We do this both to limit 113 // CPU pressure but also to have an extra guard against rate throttling 114 // from providers. 115 // We throw an error in case of negative parallelism 116 par := opts.Parallelism 117 if par < 0 { 118 diags = diags.Append(tfdiags.Sourceless( 119 tfdiags.Error, 120 "Invalid parallelism value", 121 fmt.Sprintf("The parallelism must be a positive value. Not %d.", par), 122 )) 123 return nil, diags 124 } 125 126 if par == 0 { 127 par = 10 128 } 129 130 plugins := newContextPlugins(opts.Providers, opts.Provisioners) 131 132 log.Printf("[TRACE] terraform.NewContext: complete") 133 134 return &Context{ 135 hooks: hooks, 136 meta: opts.Meta, 137 uiInput: opts.UIInput, 138 139 plugins: plugins, 140 141 parallelSem: NewSemaphore(par), 142 providerInputConfig: make(map[string]map[string]cty.Value), 143 sh: sh, 144 }, diags 145 } 146 147 func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) { 148 // TODO: This method gets called multiple times on the same context with 149 // the same inputs by different parts of Terraform that all need the 150 // schemas, and it's typically quite expensive because it has to spin up 151 // plugins to gather their schemas, so it'd be good to have some caching 152 // here to remember plugin schemas we already loaded since the plugin 153 // selections can't change during the life of a *Context object. 154 155 var diags tfdiags.Diagnostics 156 157 ret, err := loadSchemas(config, state, c.plugins) 158 if err != nil { 159 diags = diags.Append(tfdiags.Sourceless( 160 tfdiags.Error, 161 "Failed to load plugin schemas", 162 fmt.Sprintf("Error while loading schemas for plugin components: %s.", err), 163 )) 164 return nil, diags 165 } 166 return ret, diags 167 } 168 169 type ContextGraphOpts struct { 170 // If true, validates the graph structure (checks for cycles). 171 Validate bool 172 173 // Legacy graphs only: won't prune the graph 174 Verbose bool 175 } 176 177 // Stop stops the running task. 178 // 179 // Stop will block until the task completes. 180 func (c *Context) Stop() { 181 log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence") 182 183 c.l.Lock() 184 defer c.l.Unlock() 185 186 // If we're running, then stop 187 if c.runContextCancel != nil { 188 log.Printf("[WARN] terraform: run context exists, stopping") 189 190 // Tell the hook we want to stop 191 c.sh.Stop() 192 193 // Stop the context 194 c.runContextCancel() 195 c.runContextCancel = nil 196 } 197 198 // Grab the condition var before we exit 199 if cond := c.runCond; cond != nil { 200 log.Printf("[INFO] terraform: waiting for graceful stop to complete") 201 cond.Wait() 202 } 203 204 log.Printf("[WARN] terraform: stop complete") 205 } 206 207 func (c *Context) acquireRun(phase string) func() { 208 // With the run lock held, grab the context lock to make changes 209 // to the run context. 210 c.l.Lock() 211 defer c.l.Unlock() 212 213 // Wait until we're no longer running 214 for c.runCond != nil { 215 c.runCond.Wait() 216 } 217 218 // Build our lock 219 c.runCond = sync.NewCond(&c.l) 220 221 // Create a new run context 222 c.runContext, c.runContextCancel = context.WithCancel(context.Background()) 223 224 // Reset the stop hook so we're not stopped 225 c.sh.Reset() 226 227 return c.releaseRun 228 } 229 230 func (c *Context) releaseRun() { 231 // Grab the context lock so that we can make modifications to fields 232 c.l.Lock() 233 defer c.l.Unlock() 234 235 // End our run. We check if runContext is non-nil because it can be 236 // set to nil if it was cancelled via Stop() 237 if c.runContextCancel != nil { 238 c.runContextCancel() 239 } 240 241 // Unlock all waiting our condition 242 cond := c.runCond 243 c.runCond = nil 244 cond.Broadcast() 245 246 // Unset the context 247 c.runContext = nil 248 } 249 250 // watchStop immediately returns a `stop` and a `wait` chan after dispatching 251 // the watchStop goroutine. This will watch the runContext for cancellation and 252 // stop the providers accordingly. When the watch is no longer needed, the 253 // `stop` chan should be closed before waiting on the `wait` chan. 254 // The `wait` chan is important, because without synchronizing with the end of 255 // the watchStop goroutine, the runContext may also be closed during the select 256 // incorrectly causing providers to be stopped. Even if the graph walk is done 257 // at that point, stopping a provider permanently cancels its StopContext which 258 // can cause later actions to fail. 259 func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) { 260 stop := make(chan struct{}) 261 wait := make(chan struct{}) 262 263 // get the runContext cancellation channel now, because releaseRun will 264 // write to the runContext field. 265 done := c.runContext.Done() 266 267 go func() { 268 defer logging.PanicHandler() 269 270 defer close(wait) 271 // Wait for a stop or completion 272 select { 273 case <-done: 274 // done means the context was canceled, so we need to try and stop 275 // providers. 276 case <-stop: 277 // our own stop channel was closed. 278 return 279 } 280 281 // If we're here, we're stopped, trigger the call. 282 log.Printf("[TRACE] Context: requesting providers and provisioners to gracefully stop") 283 284 { 285 // Copy the providers so that a misbehaved blocking Stop doesn't 286 // completely hang Terraform. 287 walker.providerLock.Lock() 288 ps := make([]providers.Interface, 0, len(walker.providerCache)) 289 for _, p := range walker.providerCache { 290 ps = append(ps, p) 291 } 292 defer walker.providerLock.Unlock() 293 294 for _, p := range ps { 295 // We ignore the error for now since there isn't any reasonable 296 // action to take if there is an error here, since the stop is still 297 // advisory: Terraform will exit once the graph node completes. 298 p.Stop() 299 } 300 } 301 302 { 303 // Call stop on all the provisioners 304 walker.provisionerLock.Lock() 305 ps := make([]provisioners.Interface, 0, len(walker.provisionerCache)) 306 for _, p := range walker.provisionerCache { 307 ps = append(ps, p) 308 } 309 defer walker.provisionerLock.Unlock() 310 311 for _, p := range ps { 312 // We ignore the error for now since there isn't any reasonable 313 // action to take if there is an error here, since the stop is still 314 // advisory: Terraform will exit once the graph node completes. 315 p.Stop() 316 } 317 } 318 }() 319 320 return stop, wait 321 } 322 323 // checkConfigDependencies checks whether the recieving context is able to 324 // support the given configuration, returning error diagnostics if not. 325 // 326 // Currently this function checks whether the current Terraform CLI version 327 // matches the version requirements of all of the modules, and whether our 328 // plugin library contains all of the plugin names/addresses needed. 329 // 330 // This function does *not* check that external modules are installed (that's 331 // the responsibility of the configuration loader) and doesn't check that the 332 // plugins are of suitable versions to match any version constraints (which is 333 // the responsibility of the code which installed the plugins and then 334 // constructed the Providers/Provisioners maps passed in to NewContext). 335 // 336 // In most cases we should typically catch the problems this function detects 337 // before we reach this point, but this function can come into play in some 338 // unusual cases outside of the main workflow, and can avoid some 339 // potentially-more-confusing errors from later operations. 340 func (c *Context) checkConfigDependencies(config *configs.Config) tfdiags.Diagnostics { 341 var diags tfdiags.Diagnostics 342 343 // This checks the Terraform CLI version constraints specified in all of 344 // the modules. 345 diags = diags.Append(CheckCoreVersionRequirements(config)) 346 347 // We only check that we have a factory for each required provider, and 348 // assume the caller already assured that any separately-installed 349 // plugins are of a suitable version, match expected checksums, etc. 350 providerReqs, hclDiags := config.ProviderRequirements() 351 diags = diags.Append(hclDiags) 352 if hclDiags.HasErrors() { 353 return diags 354 } 355 for providerAddr := range providerReqs { 356 if !c.plugins.HasProvider(providerAddr) { 357 if !providerAddr.IsBuiltIn() { 358 diags = diags.Append(tfdiags.Sourceless( 359 tfdiags.Error, 360 "Missing required provider", 361 fmt.Sprintf( 362 "This configuration requires provider %s, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", 363 providerAddr, 364 ), 365 )) 366 } else { 367 // Built-in providers can never be installed by "terraform init", 368 // so no point in confusing the user by suggesting that. 369 diags = diags.Append(tfdiags.Sourceless( 370 tfdiags.Error, 371 "Missing required provider", 372 fmt.Sprintf( 373 "This configuration requires built-in provider %s, but that provider isn't available in this Terraform version.", 374 providerAddr, 375 ), 376 )) 377 } 378 } 379 } 380 381 // Our handling of provisioners is much less sophisticated than providers 382 // because they are in many ways a legacy system. We need to go hunting 383 // for them more directly in the configuration. 384 config.DeepEach(func(modCfg *configs.Config) { 385 if modCfg == nil || modCfg.Module == nil { 386 return // should not happen, but we'll be robust 387 } 388 for _, rc := range modCfg.Module.ManagedResources { 389 if rc.Managed == nil { 390 continue // should not happen, but we'll be robust 391 } 392 for _, pc := range rc.Managed.Provisioners { 393 if !c.plugins.HasProvisioner(pc.Type) { 394 // This is not a very high-quality error, because really 395 // the caller of terraform.NewContext should've already 396 // done equivalent checks when doing plugin discovery. 397 // This is just to make sure we return a predictable 398 // error in a central place, rather than failing somewhere 399 // later in the non-deterministically-ordered graph walk. 400 diags = diags.Append(tfdiags.Sourceless( 401 tfdiags.Error, 402 "Missing required provisioner plugin", 403 fmt.Sprintf( 404 "This configuration requires provisioner plugin %q, which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running Terraform.", 405 pc.Type, 406 ), 407 )) 408 } 409 } 410 } 411 }) 412 413 // Because we were doing a lot of map iteration above, and we're only 414 // generating sourceless diagnostics anyway, our diagnostics will not be 415 // in a deterministic order. To ensure stable output when there are 416 // multiple errors to report, we'll sort these particular diagnostics 417 // so they are at least always consistent alone. This ordering is 418 // arbitrary and not a compatibility constraint. 419 sort.Slice(diags, func(i, j int) bool { 420 // Because these are sourcelss diagnostics and we know they are all 421 // errors, we know they'll only differ in their description fields. 422 descI := diags[i].Description() 423 descJ := diags[j].Description() 424 switch { 425 case descI.Summary != descJ.Summary: 426 return descI.Summary < descJ.Summary 427 default: 428 return descI.Detail < descJ.Detail 429 } 430 }) 431 432 return diags 433 }