Prefer fmt.Errorf("...: %w", err) (stdlib) for all new code. The github.com/pkg/errors package is deprecated and no longer maintained -- do not introduce new usages. Existing errors.Wrap/errors.Wrapf calls should be migrated to fmt.Errorf when touching those files.
- Always wrap with context describing what failed, not why:
"failed to create EC2 client: %w", not"EC2 client error because credentials were bad". - Use
errors.Isanderrors.As(stdlib) for error inspection. Avoiderrors.Cause()frompkg/errors.
All install-config and platform validation uses k8s.io/apimachinery/pkg/util/validation/field.
Structure: Validation functions typically return field.ErrorList. Some helper functions (e.g., ValidateIPinMachineCIDR) return error for use in interactive prompts or simple checks.
func ValidatePlatform(p *aws.Platform, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if p.Region == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("region"), "region must be specified"))
}
// Compose child validators
allErrs = append(allErrs, validateSubnets(p.VPC.Subnets, fldPath.Child("vpc", "subnets"))...)
return allErrs
}Rules:
- Use
field.Required,field.Invalid,field.Forbidden,field.NotSupported,field.Duplicate-- choose the one that matches the problem. - Build
fldPathwith.Child()and.Index()so errors print asplatform.aws.vpc.subnets[0].id. - Pure type validation lives in
pkg/types/<platform>/validation/. Platform-API validation (calling cloud APIs) lives inpkg/asset/installconfig/<platform>/validation.go. - Convert to
errorat the boundary using.ToAggregate():
if err := validation.ValidateInstallConfig(cfg, false).ToAggregate(); err != nil {
return fmt.Errorf("invalid %q file: %w", filename, err)
}pkg/diagnostics/error.go defines Err with Source, Reason (CamelCase), and Message fields. Use it when the error should be categorized for end-user reporting.
return &diagnostics.Err{Reason: "MissingQuota", Message: msg}Currently only used for quota check failures. Reason must be a single CamelCase word. Message is free-form text written for non-expert users.
The asset store (pkg/asset/store/store.go) propagates errors with dependency context automatically:
failed to generate asset "Cluster" -> failed to fetch dependency of "Cluster" -> ...
Generate()returning a non-nil error stops the entire graph.- The store wraps each level:
errors.Wrapf(err, "failed to fetch dependency of %q", a.Name())anderrors.Wrapf(err, "failed to generate asset %q", a.Name()). - Do NOT duplicate the asset name in your own error messages; the store adds it.
Sentinel error constants in pkg/asset/asset.go:
InstallConfigError="failed to create install config"ClusterCreationError="failed to create cluster"ControlPlaneCreationError="failed to provision control-plane machines"
These are used as wrapper messages at top-level boundaries (e.g., errors.Wrap(err, asset.InstallConfigError)).
- Extract error codes via
errors.As(err, &apiErr)andapiErr.ErrorCode(). Helper:pkg/destroy/aws/errors.go:HandleErrorCode(). - Permission checks:
pkg/asset/installconfig/aws/awserrors.goprovidesIsUnauthorized(err)andIsHTTPForbidden(err). - In destroy paths, when a resource is already gone (e.g.,
InvalidDhcpOptionsID.NotFound), treat it as success and return nil.
- Use
errors.As(err, &dErr)withautorest.DetailedErrorto check HTTP status codes. - Dedicated helpers:
isNotFoundError(),isAuthError(),isResourceGroupBlockedError()inpkg/destroy/azure/azure.go. NotFoundduring destroy is not an error -- return nil.
- Use
errors.As(err, &ae)withgoogleapi.Errorand checkae.Code(e.g., 404, 409, 429). - Permission checks:
pkg/quota/gcp/gcp.go:IsUnauthorized().
ibmErrorstruct withStatusandMessage. UseisNoOp()to detect 404s during destroy.- Aggregate errors from multi-resource operations with
utilerrors.NewAggregate(errs).
- Custom error type
networkextensions.Error(astringtype implementingerror) designed forerrors.Asmatching.
Destroy flows run in a poll loop (wait.PollImmediateUntil / wait.PollImmediateInfinite) and follow these conventions:
- Continue on failure: Individual resource deletion errors are logged but do NOT abort the loop. The loop only exits when all resources are gone or the context is canceled.
pendingItemTracker: GCP and Power VS track resources remaining to delete. The loop returnsdone=trueonly when the tracker is empty.ErrorTracker(AWS,pkg/destroy/aws/errortracker.go): Rate-limits repeated warnings. First occurrence logs at DEBUG; after a suppression duration, escalates to WARN. Usetracker.suppressWarning(id, err, logger).- Staged deletion: GCP uses ordered stages (stop instances -> delete resources -> delete networking). Errors in a stage skip subsequent stages for that iteration but the poll retries.
- Graceful degradation for permissions (exception, not the norm): Quota and capability pre-flight checks are best-effort -- when they fail due to missing permissions, log a warning and skip the check rather than failing the install. This applies only to optional pre-flight checks, not to operations required for a successful install. Pattern:
if IsUnauthorized(err) { logrus.Warn(...); return nil }. utilerrors.NewAggregate: Use to combine multiple independent errors (destroy loops, SSH key loading, multi-resource operations). Returns nil if the slice is empty.- Error messages: Start with lowercase, do not end with punctuation. Use
"failed to <verb>"not"error <verbing>". errors.As/errors.Is: Prefer over type assertions for cloud SDK errors. The codebase uses these consistently across all platforms.