kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/terraform/context.go (about) 1 package terraform 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "strings" 8 "sync" 9 10 "github.com/apparentlymart/go-versions/versions" 11 "kubeform.dev/terraform-backend-sdk/addrs" 12 "kubeform.dev/terraform-backend-sdk/configs" 13 "kubeform.dev/terraform-backend-sdk/providers" 14 "kubeform.dev/terraform-backend-sdk/provisioners" 15 "kubeform.dev/terraform-backend-sdk/states" 16 "kubeform.dev/terraform-backend-sdk/tfdiags" 17 "github.com/zclconf/go-cty/cty" 18 19 "kubeform.dev/terraform-backend-sdk/depsfile" 20 "kubeform.dev/terraform-backend-sdk/getproviders" 21 _ "kubeform.dev/terraform-backend-sdk/logging" 22 ) 23 24 // InputMode defines what sort of input will be asked for when Input 25 // is called on Context. 26 type InputMode byte 27 28 const ( 29 // InputModeProvider asks for provider variables 30 InputModeProvider InputMode = 1 << iota 31 32 // InputModeStd is the standard operating mode and asks for both variables 33 // and providers. 34 InputModeStd = InputModeProvider 35 ) 36 37 // ContextOpts are the user-configurable options to create a context with 38 // NewContext. 39 type ContextOpts struct { 40 Meta *ContextMeta 41 Hooks []Hook 42 Parallelism int 43 Providers map[addrs.Provider]providers.Factory 44 Provisioners map[string]provisioners.Factory 45 46 // If non-nil, will apply as additional constraints on the provider 47 // plugins that will be requested from the provider resolver. 48 ProviderSHA256s map[string][]byte 49 50 // If non-nil, will be verified to ensure that provider requirements from 51 // configuration can be satisfied by the set of locked dependencies. 52 LockedDependencies *depsfile.Locks 53 54 // Set of providers to exclude from the requirements check process, as they 55 // are marked as in local development. 56 ProvidersInDevelopment map[addrs.Provider]struct{} 57 58 UIInput UIInput 59 } 60 61 // ContextMeta is metadata about the running context. This is information 62 // that this package or structure cannot determine on its own but exposes 63 // into Terraform in various ways. This must be provided by the Context 64 // initializer. 65 type ContextMeta struct { 66 Env string // Env is the state environment 67 68 // OriginalWorkingDir is the working directory where the Terraform CLI 69 // was run from, which may no longer actually be the current working 70 // directory if the user included the -chdir=... option. 71 // 72 // If this string is empty then the original working directory is the same 73 // as the current working directory. 74 // 75 // In most cases we should respect the user's override by ignoring this 76 // path and just using the current working directory, but this is here 77 // for some exceptional cases where the original working directory is 78 // needed. 79 OriginalWorkingDir string 80 } 81 82 // Context represents all the context that Terraform needs in order to 83 // perform operations on infrastructure. This structure is built using 84 // NewContext. 85 type Context struct { 86 // meta captures some misc. information about the working directory where 87 // we're taking these actions, and thus which should remain steady between 88 // operations. 89 meta *ContextMeta 90 91 plugins *contextPlugins 92 dependencyLocks *depsfile.Locks 93 providersInDevelopment map[addrs.Provider]struct{} 94 95 hooks []Hook 96 sh *stopHook 97 uiInput UIInput 98 99 l sync.Mutex // Lock acquired during any task 100 parallelSem Semaphore 101 providerInputConfig map[string]map[string]cty.Value 102 providerSHA256s map[string][]byte 103 runCond *sync.Cond 104 runContext context.Context 105 runContextCancel context.CancelFunc 106 } 107 108 // (additional methods on Context can be found in context_*.go files.) 109 110 // NewContext creates a new Context structure. 111 // 112 // Once a Context is created, the caller must not access or mutate any of 113 // the objects referenced (directly or indirectly) by the ContextOpts fields. 114 // 115 // If the returned diagnostics contains errors then the resulting context is 116 // invalid and must not be used. 117 func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { 118 var diags tfdiags.Diagnostics 119 120 log.Printf("[TRACE] terraform.NewContext: starting") 121 122 // Copy all the hooks and add our stop hook. We don't append directly 123 // to the Config so that we're not modifying that in-place. 124 sh := new(stopHook) 125 hooks := make([]Hook, len(opts.Hooks)+1) 126 copy(hooks, opts.Hooks) 127 hooks[len(opts.Hooks)] = sh 128 129 // Determine parallelism, default to 10. We do this both to limit 130 // CPU pressure but also to have an extra guard against rate throttling 131 // from providers. 132 // We throw an error in case of negative parallelism 133 par := opts.Parallelism 134 if par < 0 { 135 diags = diags.Append(tfdiags.Sourceless( 136 tfdiags.Error, 137 "Invalid parallelism value", 138 fmt.Sprintf("The parallelism must be a positive value. Not %d.", par), 139 )) 140 return nil, diags 141 } 142 143 if par == 0 { 144 par = 10 145 } 146 147 plugins := newContextPlugins(opts.Providers, opts.Provisioners) 148 149 log.Printf("[TRACE] terraform.NewContext: complete") 150 151 return &Context{ 152 hooks: hooks, 153 meta: opts.Meta, 154 uiInput: opts.UIInput, 155 156 plugins: plugins, 157 dependencyLocks: opts.LockedDependencies, 158 providersInDevelopment: opts.ProvidersInDevelopment, 159 160 parallelSem: NewSemaphore(par), 161 providerInputConfig: make(map[string]map[string]cty.Value), 162 providerSHA256s: opts.ProviderSHA256s, 163 sh: sh, 164 }, diags 165 } 166 167 func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) { 168 // TODO: This method gets called multiple times on the same context with 169 // the same inputs by different parts of Terraform that all need the 170 // schemas, and it's typically quite expensive because it has to spin up 171 // plugins to gather their schemas, so it'd be good to have some caching 172 // here to remember plugin schemas we already loaded since the plugin 173 // selections can't change during the life of a *Context object. 174 175 var diags tfdiags.Diagnostics 176 177 // If we have a configuration and a set of locked dependencies, verify that 178 // the provider requirements from the configuration can be satisfied by the 179 // locked dependencies. 180 if c.dependencyLocks != nil && config != nil { 181 reqs, providerDiags := config.ProviderRequirements() 182 diags = diags.Append(providerDiags) 183 184 locked := c.dependencyLocks.AllProviders() 185 unmetReqs := make(getproviders.Requirements) 186 for provider, versionConstraints := range reqs { 187 // Builtin providers are not listed in the locks file 188 if provider.IsBuiltIn() { 189 continue 190 } 191 // Development providers must be excluded from this check 192 if _, ok := c.providersInDevelopment[provider]; ok { 193 continue 194 } 195 // If the required provider doesn't exist in the lock, or the 196 // locked version doesn't meet the constraints, mark the 197 // requirement unmet 198 acceptable := versions.MeetingConstraints(versionConstraints) 199 if lock, ok := locked[provider]; !ok || !acceptable.Has(lock.Version()) { 200 unmetReqs[provider] = versionConstraints 201 } 202 } 203 204 if len(unmetReqs) > 0 { 205 var buf strings.Builder 206 for provider, versionConstraints := range unmetReqs { 207 fmt.Fprintf(&buf, "\n- %s", provider) 208 if len(versionConstraints) > 0 { 209 fmt.Fprintf(&buf, " (%s)", getproviders.VersionConstraintsString(versionConstraints)) 210 } 211 } 212 diags = diags.Append(tfdiags.Sourceless( 213 tfdiags.Error, 214 "Provider requirements cannot be satisfied by locked dependencies", 215 fmt.Sprintf("The following required providers are not installed:\n%s\n\nPlease run \"terraform init\".", buf.String()), 216 )) 217 return nil, diags 218 } 219 } 220 221 ret, err := loadSchemas(config, state, c.plugins) 222 if err != nil { 223 diags = diags.Append(tfdiags.Sourceless( 224 tfdiags.Error, 225 "Failed to load plugin schemas", 226 fmt.Sprintf("Error while loading schemas for plugin components: %s.", err), 227 )) 228 return nil, diags 229 } 230 return ret, diags 231 } 232 233 type ContextGraphOpts struct { 234 // If true, validates the graph structure (checks for cycles). 235 Validate bool 236 237 // Legacy graphs only: won't prune the graph 238 Verbose bool 239 } 240 241 // Stop stops the running task. 242 // 243 // Stop will block until the task completes. 244 func (c *Context) Stop() { 245 log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence") 246 247 c.l.Lock() 248 defer c.l.Unlock() 249 250 // If we're running, then stop 251 if c.runContextCancel != nil { 252 log.Printf("[WARN] terraform: run context exists, stopping") 253 254 // Tell the hook we want to stop 255 c.sh.Stop() 256 257 // Stop the context 258 c.runContextCancel() 259 c.runContextCancel = nil 260 } 261 262 // Grab the condition var before we exit 263 if cond := c.runCond; cond != nil { 264 log.Printf("[INFO] terraform: waiting for graceful stop to complete") 265 cond.Wait() 266 } 267 268 log.Printf("[WARN] terraform: stop complete") 269 } 270 271 func (c *Context) acquireRun(phase string) func() { 272 // With the run lock held, grab the context lock to make changes 273 // to the run context. 274 c.l.Lock() 275 defer c.l.Unlock() 276 277 // Wait until we're no longer running 278 for c.runCond != nil { 279 c.runCond.Wait() 280 } 281 282 // Build our lock 283 c.runCond = sync.NewCond(&c.l) 284 285 // Create a new run context 286 c.runContext, c.runContextCancel = context.WithCancel(context.Background()) 287 288 // Reset the stop hook so we're not stopped 289 c.sh.Reset() 290 291 return c.releaseRun 292 } 293 294 func (c *Context) releaseRun() { 295 // Grab the context lock so that we can make modifications to fields 296 c.l.Lock() 297 defer c.l.Unlock() 298 299 // End our run. We check if runContext is non-nil because it can be 300 // set to nil if it was cancelled via Stop() 301 if c.runContextCancel != nil { 302 c.runContextCancel() 303 } 304 305 // Unlock all waiting our condition 306 cond := c.runCond 307 c.runCond = nil 308 cond.Broadcast() 309 310 // Unset the context 311 c.runContext = nil 312 } 313 314 // watchStop immediately returns a `stop` and a `wait` chan after dispatching 315 // the watchStop goroutine. This will watch the runContext for cancellation and 316 // stop the providers accordingly. When the watch is no longer needed, the 317 // `stop` chan should be closed before waiting on the `wait` chan. 318 // The `wait` chan is important, because without synchronizing with the end of 319 // the watchStop goroutine, the runContext may also be closed during the select 320 // incorrectly causing providers to be stopped. Even if the graph walk is done 321 // at that point, stopping a provider permanently cancels its StopContext which 322 // can cause later actions to fail. 323 func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) { 324 stop := make(chan struct{}) 325 wait := make(chan struct{}) 326 327 // get the runContext cancellation channel now, because releaseRun will 328 // write to the runContext field. 329 done := c.runContext.Done() 330 331 go func() { 332 defer close(wait) 333 // Wait for a stop or completion 334 select { 335 case <-done: 336 // done means the context was canceled, so we need to try and stop 337 // providers. 338 case <-stop: 339 // our own stop channel was closed. 340 return 341 } 342 343 // If we're here, we're stopped, trigger the call. 344 log.Printf("[TRACE] Context: requesting providers and provisioners to gracefully stop") 345 346 { 347 // Copy the providers so that a misbehaved blocking Stop doesn't 348 // completely hang Terraform. 349 walker.providerLock.Lock() 350 ps := make([]providers.Interface, 0, len(walker.providerCache)) 351 for _, p := range walker.providerCache { 352 ps = append(ps, p) 353 } 354 defer walker.providerLock.Unlock() 355 356 for _, p := range ps { 357 // We ignore the error for now since there isn't any reasonable 358 // action to take if there is an error here, since the stop is still 359 // advisory: Terraform will exit once the graph node completes. 360 p.Stop() 361 } 362 } 363 364 { 365 // Call stop on all the provisioners 366 walker.provisionerLock.Lock() 367 ps := make([]provisioners.Interface, 0, len(walker.provisionerCache)) 368 for _, p := range walker.provisionerCache { 369 ps = append(ps, p) 370 } 371 defer walker.provisionerLock.Unlock() 372 373 for _, p := range ps { 374 // We ignore the error for now since there isn't any reasonable 375 // action to take if there is an error here, since the stop is still 376 // advisory: Terraform will exit once the graph node completes. 377 p.Stop() 378 } 379 } 380 }() 381 382 return stop, wait 383 }