github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/moduletest/run.go (about) 1 package moduletest 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 8 "github.com/terramate-io/tf/addrs" 9 "github.com/terramate-io/tf/configs" 10 "github.com/terramate-io/tf/configs/configschema" 11 "github.com/terramate-io/tf/plans" 12 "github.com/terramate-io/tf/providers" 13 "github.com/terramate-io/tf/states" 14 "github.com/terramate-io/tf/tfdiags" 15 ) 16 17 type Run struct { 18 Config *configs.TestRun 19 20 Verbose *Verbose 21 22 Name string 23 Index int 24 Status Status 25 26 Diagnostics tfdiags.Diagnostics 27 } 28 29 // Verbose is a utility struct that holds all the information required for a run 30 // to render the results verbosely. 31 // 32 // At the moment, this basically means printing out the plan. To do that we need 33 // all the information within this struct. 34 type Verbose struct { 35 Plan *plans.Plan 36 State *states.State 37 Config *configs.Config 38 Providers map[addrs.Provider]providers.ProviderSchema 39 Provisioners map[string]*configschema.Block 40 } 41 42 func (run *Run) GetTargets() ([]addrs.Targetable, tfdiags.Diagnostics) { 43 var diagnostics tfdiags.Diagnostics 44 var targets []addrs.Targetable 45 46 for _, target := range run.Config.Options.Target { 47 addr, diags := addrs.ParseTarget(target) 48 diagnostics = diagnostics.Append(diags) 49 if addr != nil { 50 targets = append(targets, addr.Subject) 51 } 52 } 53 54 return targets, diagnostics 55 } 56 57 func (run *Run) GetReplaces() ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { 58 var diagnostics tfdiags.Diagnostics 59 var replaces []addrs.AbsResourceInstance 60 61 for _, replace := range run.Config.Options.Replace { 62 addr, diags := addrs.ParseAbsResourceInstance(replace) 63 diagnostics = diagnostics.Append(diags) 64 if diags.HasErrors() { 65 continue 66 } 67 68 if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { 69 diagnostics = diagnostics.Append(&hcl.Diagnostic{ 70 Severity: hcl.DiagError, 71 Summary: "Can only target managed resources for forced replacements.", 72 Detail: addr.String(), 73 Subject: replace.SourceRange().Ptr(), 74 }) 75 continue 76 } 77 78 replaces = append(replaces, addr) 79 } 80 81 return replaces, diagnostics 82 } 83 84 func (run *Run) GetReferences() ([]*addrs.Reference, tfdiags.Diagnostics) { 85 var diagnostics tfdiags.Diagnostics 86 var references []*addrs.Reference 87 88 for _, rule := range run.Config.CheckRules { 89 for _, variable := range rule.Condition.Variables() { 90 reference, diags := addrs.ParseRef(variable) 91 diagnostics = diagnostics.Append(diags) 92 if reference != nil { 93 references = append(references, reference) 94 } 95 } 96 for _, variable := range rule.ErrorMessage.Variables() { 97 reference, diags := addrs.ParseRef(variable) 98 diagnostics = diagnostics.Append(diags) 99 if reference != nil { 100 references = append(references, reference) 101 } 102 } 103 } 104 105 return references, diagnostics 106 } 107 108 // ValidateExpectedFailures steps through the provided diagnostics (which should 109 // be the result of a plan or an apply operation), and does 3 things: 110 // 1. Removes diagnostics that match the expected failures from the config. 111 // 2. Upgrades warnings from check blocks into errors where appropriate so the 112 // test will fail later. 113 // 3. Adds diagnostics for any expected failures that were not satisfied. 114 // 115 // Point 2 is a bit complicated so worth expanding on. In normal Terraform 116 // execution, any error that originates within a check block (either from an 117 // assertion or a scoped data source) is wrapped up as a Warning to be 118 // identified to the user but not to fail the actual Terraform operation. During 119 // test execution, we want to upgrade (or rollback) these warnings into errors 120 // again so the test will fail. We do that as part of this function as we are 121 // already processing the diagnostics from check blocks in here anyway. 122 // 123 // The way the function works out which diagnostics are relevant to expected 124 // failures is by using the tfdiags Extra functionality to detect which 125 // diagnostics were generated by custom conditions. Terraform adds the 126 // addrs.CheckRule that generated each diagnostic to the diagnostic itself so we 127 // can tell which diagnostics can be expected. 128 func (run *Run) ValidateExpectedFailures(originals tfdiags.Diagnostics) tfdiags.Diagnostics { 129 130 // We're going to capture all the checkable objects that are referenced 131 // from the expected failures. 132 expectedFailures := addrs.MakeMap[addrs.Referenceable, bool]() 133 sourceRanges := addrs.MakeMap[addrs.Referenceable, tfdiags.SourceRange]() 134 135 for _, traversal := range run.Config.ExpectFailures { 136 // Ignore the diagnostics returned from the reference parsing, these 137 // references will have been checked earlier in the process by the 138 // validate stage so we don't need to do that again here. 139 reference, _ := addrs.ParseRefFromTestingScope(traversal) 140 expectedFailures.Put(reference.Subject, false) 141 sourceRanges.Put(reference.Subject, reference.SourceRange) 142 } 143 144 var diags tfdiags.Diagnostics 145 for _, diag := range originals { 146 147 if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok { 148 switch rule.Container.CheckableKind() { 149 case addrs.CheckableOutputValue: 150 addr := rule.Container.(addrs.AbsOutputValue) 151 if !addr.Module.IsRoot() { 152 // failures can only be expected against checkable objects 153 // in the root module. This diagnostic will be added into 154 // returned set below. 155 break 156 } 157 158 if diag.Severity() == tfdiags.Warning { 159 // Warnings don't count as errors. This diagnostic will be 160 // added into the returned set below. 161 break 162 } 163 164 if expectedFailures.Has(addr.OutputValue) { 165 // Then this failure is expected! Mark the original map as 166 // having found a failure and swallow this error by 167 // continuing and not adding it into the returned set of 168 // diagnostics. 169 expectedFailures.Put(addr.OutputValue, true) 170 continue 171 } 172 173 // Otherwise, this isn't an expected failure so just fall out 174 // and add it into the returned set of diagnostics below. 175 176 case addrs.CheckableInputVariable: 177 addr := rule.Container.(addrs.AbsInputVariableInstance) 178 if !addr.Module.IsRoot() { 179 // failures can only be expected against checkable objects 180 // in the root module. This diagnostic will be added into 181 // returned set below. 182 break 183 } 184 185 if diag.Severity() == tfdiags.Warning { 186 // Warnings don't count as errors. This diagnostic will be 187 // added into the returned set below. 188 break 189 } 190 if expectedFailures.Has(addr.Variable) { 191 // Then this failure is expected! Mark the original map as 192 // having found a failure and swallow this error by 193 // continuing and not adding it into the returned set of 194 // diagnostics. 195 expectedFailures.Put(addr.Variable, true) 196 continue 197 } 198 199 // Otherwise, this isn't an expected failure so just fall out 200 // and add it into the returned set of diagnostics below. 201 202 case addrs.CheckableResource: 203 addr := rule.Container.(addrs.AbsResourceInstance) 204 if !addr.Module.IsRoot() { 205 // failures can only be expected against checkable objects 206 // in the root module. This diagnostic will be added into 207 // returned set below. 208 break 209 } 210 211 if diag.Severity() == tfdiags.Warning { 212 // Warnings don't count as errors. This diagnostic will be 213 // added into the returned set below. 214 break 215 } 216 217 if expectedFailures.Has(addr.Resource) { 218 // Then this failure is expected! Mark the original map as 219 // having found a failure and swallow this error by 220 // continuing and not adding it into the returned set of 221 // diagnostics. 222 expectedFailures.Put(addr.Resource, true) 223 continue 224 } 225 226 if expectedFailures.Has(addr.Resource.Resource) { 227 // We can also blanket expect failures in all instances for 228 // a resource so we check for that here as well. 229 expectedFailures.Put(addr.Resource.Resource, true) 230 continue 231 } 232 233 // Otherwise, this isn't an expected failure so just fall out 234 // and add it into the returned set of diagnostics below. 235 236 case addrs.CheckableCheck: 237 addr := rule.Container.(addrs.AbsCheck) 238 239 // Check blocks are a bit more difficult than the others. Check 240 // block diagnostics could be from a nested data block, or 241 // from a failed assertion, and have all been marked as just 242 // warning severity. 243 // 244 // For diagnostics from failed assertions, we want to check if 245 // it was expected and skip it if it was. But if it wasn't 246 // expected we want to upgrade the diagnostic from a warning 247 // into an error so the test case will fail overall. 248 // 249 // For diagnostics from nested data blocks, we have two 250 // categories of diagnostics. First, diagnostics that were 251 // originally errors and we mapped into warnings. Second, 252 // diagnostics that were originally warnings and stayed that 253 // way. For the first case, we want to turn these back to errors 254 // and use them as part of the expected failures functionality. 255 // The second case should remain as warnings and be ignored by 256 // the expected failures functionality. 257 // 258 // Note, as well that we still want to upgrade failed checks 259 // from child modules into errors, so in the other branches we 260 // just do a simple blanket skip off all diagnostics not 261 // from the root module. We're more selective here, only 262 // diagnostics from the root module are considered for the 263 // expect failures functionality but we do also upgrade 264 // diagnostics from child modules back into errors. 265 266 if rule.Type == addrs.CheckAssertion { 267 // Then this diagnostic is from a check block assertion, it 268 // is something we want to treat as an error even though it 269 // is actually claiming to be a warning. 270 271 if addr.Module.IsRoot() && expectedFailures.Has(addr.Check) { 272 // Then this failure is expected! Mark the original map as 273 // having found a failure and continue. 274 expectedFailures.Put(addr.Check, true) 275 continue 276 } 277 278 // Otherwise, let's package this up as an error and move on. 279 diags = diags.Append(tfdiags.Override(diag, tfdiags.Error, nil)) 280 continue 281 } else if rule.Type == addrs.CheckDataResource { 282 // Then the diagnostic we have was actually overridden so 283 // let's get back to the original. 284 original := tfdiags.UndoOverride(diag) 285 286 // This diagnostic originated from a scoped data source. 287 if addr.Module.IsRoot() && original.Severity() == tfdiags.Error { 288 // Okay, we have a genuine error from the root module, 289 // so we can now check if we want to ignore it or not. 290 if expectedFailures.Has(addr.Check) { 291 // Then this failure is expected! Mark the original map as 292 // having found a failure and continue. 293 expectedFailures.Put(addr.Check, true) 294 continue 295 } 296 } 297 298 // In all other cases, we want to add the original error 299 // into the set we return to the testing framework and move 300 // onto the next one. 301 diags = diags.Append(original) 302 continue 303 } else { 304 panic("invalid CheckType: " + rule.Type.String()) 305 } 306 default: 307 panic("unrecognized CheckableKind: " + rule.Container.CheckableKind().String()) 308 } 309 } 310 311 // If we get here, then we're not modifying the original diagnostic at 312 // all. We just want the testing framework to treat it as normal. 313 diags = diags.Append(diag) 314 } 315 316 // Okay, we've checked all our diagnostics to see if any were expected. 317 // Now, let's make sure that all the checkable objects we expected to fail 318 // actually did! 319 320 for _, elem := range expectedFailures.Elems { 321 addr := elem.Key 322 failed := elem.Value 323 324 if !failed { 325 // Then we expected a failure, and it did not occur. Add it to the 326 // diagnostics. 327 diags = diags.Append(&hcl.Diagnostic{ 328 Severity: hcl.DiagError, 329 Summary: "Missing expected failure", 330 Detail: fmt.Sprintf("The checkable object, %s, was expected to report an error but did not.", addr.String()), 331 Subject: sourceRanges.Get(addr).ToHCL().Ptr(), 332 }) 333 } 334 } 335 336 return diags 337 }