Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions docs/resources/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
subcategory: "manifest"
page_title: "Kubernetes: kubernetes_manifest"
description: |-
The resource provides a way to create and manage custom resources
The resource provides a way to create and manage custom resources
---

# kubernetes_manifest
Expand All @@ -25,6 +25,7 @@ Once applied, the `object` attribute contains the state of the resource as retur
- `computed_fields` (List of String) List of manifest fields whose values can be altered by the API server during 'apply'. Defaults to: ["metadata.annotations", "metadata.labels"]
- `field_manager` (Block List, Max: 1) Configure field manager options. (see [below for nested schema](#nestedblock--field_manager))
- `object` (Dynamic) The resulting resource state, as returned by the API server after applying the desired state from `manifest`.
- `skip_validation` (Boolean) When set to `true`, skips validation of the manifest against the Kubernetes API server during the plan phase. This can be useful when the cluster is not available or when you want to defer validation until apply time. Defaults to `false`.
- `timeouts` (Block List, Max: 1) (see [below for nested schema](#nestedblock--timeouts))
- `wait` (Block List, Max: 1) Configure waiter options. (see [below for nested schema](#nestedblock--wait))
- `wait_for` (Object, Deprecated) A map of attribute paths and desired patterns to be matched. After each apply the provider will wait for all attributes listed here to reach a value that matches the desired pattern. (see [below for nested schema](#nestedatt--wait_for))
Expand Down Expand Up @@ -79,7 +80,7 @@ Optional:

### Before you use this resource

- This resource requires API access during planning time. This means the cluster has to be accessible at plan time and thus cannot be created in the same apply operation. We recommend only using this resource for custom resources or resources not yet fully supported by the provider.
- This resource requires API access during planning time by default. This means the cluster has to be accessible at plan time and thus cannot be created in the same apply operation. However, you can use the `skip_validation` attribute to defer validation until apply time, which allows creating the cluster and its resources in the same Terraform run. We recommend only using this resource for custom resources or resources not yet fully supported by the provider.

- This resource uses [Server-side Apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) to carry out apply operations. A minimum Kubernetes version of 1.16.x is required, but versions 1.17+ are strongly recommended as the SSA implementation in Kubernetes 1.16.x is incomplete and unstable.

Expand Down Expand Up @@ -299,3 +300,63 @@ A field path is a string that describes the fully qualified address of a field w
> type(kubernetes_manifest.my-secret.object.data)
map(string)
```

## Skipping validation during plan phase

By default, the `kubernetes_manifest` resource validates the manifest against the Kubernetes API server during the plan phase. This includes checking if the resource type exists, verifying CRD schemas, and validating namespace requirements. However, there are scenarios where you may want to skip this validation:

- When the cluster is not available during plan time (e.g., creating the cluster and its resources in the same Terraform run)
- When deploying CRDs and their custom resources in the same apply operation
- When you want to defer validation until apply time

To skip validation during plan phase, set the `skip_validation` attribute to `true`:

```terraform
resource "kubernetes_manifest" "test" {
manifest = {
apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
name = "test-deployment"
namespace = "default"
}
spec = {
replicas = 3
selector = {
matchLabels = {
app = "test"
}
}
template = {
metadata = {
labels = {
app = "test"
}
}
spec = {
containers = [
{
name = "nginx"
image = "nginx:1.21"
}
]
}
}
}
}

skip_validation = true
}
```

**Important notes:**

- When `skip_validation` is enabled, the provider will not connect to the Kubernetes cluster during plan phase, which means:
- CRD validation will be skipped
- Namespace validation will be skipped
- OpenAPI schema validation will be skipped
- The resource will be treated as requiring replacement on updates

- Validation will still occur during the apply phase when the resource is actually created or updated in the cluster.

- It is recommended to use this feature only when necessary, as it reduces the ability to catch configuration errors early during planning.
239 changes: 172 additions & 67 deletions manifest/provider/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
)
Expand Down Expand Up @@ -197,10 +198,27 @@ func (s *RawProviderServer) PlanResourceChange(ctx context.Context, req *tfproto
return resp, nil
}

// Check if skip_validation is enabled
skipValidation := false
if skipVal, ok := proposedVal["skip_validation"]; ok && !skipVal.IsNull() && skipVal.IsKnown() {
err := skipVal.As(&skipValidation)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to parse skip_validation attribute",
Detail: err.Error(),
})
return resp, nil
}
}

// test if credentials are valid - we're going to need them further down
resp.Diagnostics = append(resp.Diagnostics, s.checkValidCredentials(ctx)...)
if len(resp.Diagnostics) > 0 {
return resp, nil
// Skip validation if skip_validation is true
if !skipValidation {
resp.Diagnostics = append(resp.Diagnostics, s.checkValidCredentials(ctx)...)
if len(resp.Diagnostics) > 0 {
return resp, nil
}
}

computedFields := make(map[string]*tftypes.AttributePath)
Expand Down Expand Up @@ -285,91 +303,178 @@ func (s *RawProviderServer) PlanResourceChange(ctx context.Context, req *tfproto
return resp, nil
}

rm, err := s.getRestMapper()
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to create K8s RESTMapper client",
Detail: err.Error(),
})
return resp, nil
}
gvk, err := GVKFromTftypesObject(&ppMan, rm)
if err != nil {
rd := &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "API did not recognize GroupVersionKind from manifest (CRD may not be installed)",
Detail: err.Error(),
var gvk schema.GroupVersionKind
var ns bool

if !skipValidation {
rm, err := s.getRestMapper()
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to create K8s RESTMapper client",
Detail: err.Error(),
})
return resp, nil
}
resp.Diagnostics = append(resp.Diagnostics, rd)
if canDeferr && meta.IsNoMatchError(err) {
// request deferral when client configuration not fully known
resp.Deferred = &tfprotov5.Deferred{
Reason: tfprotov5.DeferredReasonResourceConfigUnknown,
gvk, err = GVKFromTftypesObject(&ppMan, rm)
if err != nil {
rd := &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "API did not recognize GroupVersionKind from manifest (CRD may not be installed)",
Detail: err.Error(),
}
resp.Diagnostics = append(resp.Diagnostics, rd)
if canDeferr && meta.IsNoMatchError(err) {
// request deferral when client configuration not fully known
resp.Deferred = &tfprotov5.Deferred{
Reason: tfprotov5.DeferredReasonResourceConfigUnknown,
}
rd.Severity = tfprotov5.DiagnosticSeverityWarning
}
rd.Severity = tfprotov5.DiagnosticSeverityWarning
return resp, nil
}
return resp, nil
}

vdiags := s.validateResourceOnline(&ppMan)
if len(vdiags) > 0 {
resp.Diagnostics = append(resp.Diagnostics, vdiags...)
return resp, nil
}

ns, err := IsResourceNamespaced(gvk, rm)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to discover scope of resource",
Detail: err.Error(),
})
return resp, nil
}
if ns && !isImported {
resp.RequiresReplace = append(resp.RequiresReplace,
tftypes.NewAttributePath().WithAttributeName("manifest").WithAttributeName("metadata").WithAttributeName("namespace"),
)
}
vdiags := s.validateResourceOnline(&ppMan)
if len(vdiags) > 0 {
resp.Diagnostics = append(resp.Diagnostics, vdiags...)
return resp, nil
}

// Request a complete type for the resource from the OpenAPI spec
objectType, hints, err := s.TFTypeFromOpenAPI(ctx, gvk, false)
if err != nil {
return resp, fmt.Errorf("failed to determine resource type ID: %s", err)
}
ns, err = IsResourceNamespaced(gvk, rm)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to discover scope of resource",
Detail: err.Error(),
})
return resp, nil
}
} else {
// When skip_validation is true, we need to extract basic info from manifest
// without connecting to the cluster
manifestMap := make(map[string]tftypes.Value)
err := ppMan.As(&manifestMap)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to parse manifest for skip_validation mode",
Detail: err.Error(),
})
return resp, nil
}

if !objectType.Is(tftypes.Object{}) {
// non-structural resources have no schema so we just use the
// type information we can get from the config
objectType = ppMan.Type()
// Extract basic GVK info from manifest
var apiVersion, kind string
if apiVal, ok := manifestMap["apiVersion"]; ok && !apiVal.IsNull() {
apiVal.As(&apiVersion)
}
if kindVal, ok := manifestMap["kind"]; ok && !kindVal.IsNull() {
kindVal.As(&kind)
}

resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: "This custom resource does not have an associated OpenAPI schema.",
Detail: "We could not find an OpenAPI schema for this custom resource. Updates to this resource will cause a forced replacement.",
})
if apiVersion == "" || kind == "" {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Missing required fields in manifest for skip_validation mode",
Detail: "Both apiVersion and kind are required when skip_validation is enabled",
})
return resp, nil
}

fieldManagerName, forceConflicts, err := s.getFieldManagerConfig(proposedVal)
// Parse GroupVersion from apiVersion
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Could not extract field_manager config",
Summary: "Invalid apiVersion format",
Detail: err.Error(),
})
return resp, nil
}

err = s.dryRun(ctx, ppMan, fieldManagerName, forceConflicts, ns)
gvk = gv.WithKind(kind)

// For skip_validation mode, we assume the resource is namespaced if namespace is present
// This is a best-effort guess since we can't query the API
metadata, ok := manifestMap["metadata"]
if ok && !metadata.IsNull() {
var metadataMap map[string]tftypes.Value
if err := metadata.As(&metadataMap); err == nil {
if namespaceVal, exists := metadataMap["namespace"]; exists && !namespaceVal.IsNull() {
var namespace string
if err := namespaceVal.As(&namespace); err == nil && namespace != "" {
ns = true
}
}
}
}
}
if ns && !isImported {
resp.RequiresReplace = append(resp.RequiresReplace,
tftypes.NewAttributePath().WithAttributeName("manifest").WithAttributeName("metadata").WithAttributeName("namespace"),
)
}

// Request a complete type for the resource from the OpenAPI spec
var objectType tftypes.Type
var hints map[string]string

if !skipValidation {
objectType, hints, err = s.TFTypeFromOpenAPI(ctx, gvk, false)
if err != nil {
return resp, fmt.Errorf("failed to determine resource type ID: %s", err)
}

if !objectType.Is(tftypes.Object{}) {
// non-structural resources have no schema so we just use the
// type information we can get from the config
objectType = ppMan.Type()

resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Dry-run failed for non-structured resource",
Detail: fmt.Sprintf("A dry-run apply was performed for this resource but was unsuccessful: %v", err),
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: "This custom resource does not have an associated OpenAPI schema.",
Detail: "We could not find an OpenAPI schema for this custom resource. Updates to this resource will cause a forced replacement.",
})
return resp, nil

fieldManagerName, forceConflicts, err := s.getFieldManagerConfig(proposedVal)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Could not extract field_manager config",
Detail: err.Error(),
})
return resp, nil
}

err = s.dryRun(ctx, ppMan, fieldManagerName, forceConflicts, ns)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Dry-run failed for non-structured resource",
Detail: fmt.Sprintf("A dry-run apply was performed for this resource but was unsuccessful: %v", err),
})
return resp, nil
}

resp.RequiresReplace = []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("manifest"),
tftypes.NewAttributePath().WithAttributeName("object"),
}
}
} else {
// When skip_validation is true, use the manifest type directly
// This means we won't have OpenAPI schema validation
objectType = ppMan.Type()
hints = make(map[string]string)

resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: "Skipping OpenAPI schema validation",
Detail: "When skip_validation is enabled, the resource will not be validated against the Kubernetes API schema during plan. Validation will occur during apply.",
})

// For skip_validation mode, we need to force replacement since we can't
// determine the proper schema for comparison
resp.RequiresReplace = []*tftypes.AttributePath{
tftypes.NewAttributePath().WithAttributeName("manifest"),
tftypes.NewAttributePath().WithAttributeName("object"),
Expand Down
6 changes: 6 additions & 0 deletions manifest/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ func GetProviderResourceSchema() map[string]*tfprotov5.Schema {
Description: "List of manifest fields whose values can be altered by the API server during 'apply'. Defaults to: [\"metadata.annotations\", \"metadata.labels\"]",
Optional: true,
},
{
Name: "skip_validation",
Type: tftypes.Bool,
Description: "When set to true, skips validation of the manifest against the Kubernetes API server during the plan phase. This can be useful when the cluster is not available or when you want to defer validation until apply time.",
Optional: true,
},
},
},
},
Expand Down
Loading