Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changes/v1.16/NOTES-20260715-114855.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: NOTES
body: 'policy (experimental): loaded policies are now validated against the run''s provider schemas before plan/apply, so a policy referencing an attribute a provider does not have fails early instead of partway through a run'
time: 2026-07-15T11:48:55.000000-04:00
custom:
Issue: "38877"
76 changes: 76 additions & 0 deletions internal/policy/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
Expand Down Expand Up @@ -272,6 +273,81 @@ func (c *client) Setup(ctx context.Context, req SetupRequest) SetupResponse {
}
}

// ValidateProviderSchemas validates the loaded policies against the run's
// provider schemas. It serialises each provider's config/resource/data-source
// object types as cty JSON type encodings, calls the plugin, and returns the
// diagnostics — errors when a policy references something a provider lacks.
func (c *client) ValidateProviderSchemas(ctx context.Context, req ValidateProviderSchemasRequest) ValidateProviderSchemasResponse {
ctx, span := tracer().Start(ctx, "policy.client.validate_provider_schemas",
trace.WithAttributes(attribute.Int("policy.provider_schemas.count", len(req.ProviderSchemas))),
)
defer span.End()

protoReq := &proto.ValidateProviderSchemasRequest{}
for _, ps := range req.ProviderSchemas {
protoPS, err := providerSchemaToProto(ps)
if err != nil {
return ValidateProviderSchemasResponse{Diagnostics: Diagnostics{
NewErrorDiagnostic("Failed to encode provider schema",
fmt.Sprintf("Failed to encode the schema for provider %q: %v.", ps.Type, err),
SetupErrorResult,
),
}}
}
protoReq.ProviderSchemas = append(protoReq.ProviderSchemas, protoPS)
}

response, err := c.client.ValidateProviderSchemas(ctx, protoReq)
if err != nil {
return ValidateProviderSchemasResponse{Diagnostics: Diagnostics{
NewErrorDiagnostic("Failed to validate policies against provider schemas",
fmt.Sprintf("Failed to validate policies against provider schemas: %v.", err),
SetupErrorResult,
),
}}
}
return ValidateProviderSchemasResponse{Diagnostics: DiagsFromProto(response.Diagnostics, nil)}
}

// providerSchemaToProto encodes a provider schema's cty object types as cty JSON
// type encodings for the wire.
func providerSchemaToProto(ps ProviderSchema) (*proto.ProviderSchema, error) {
config, err := ctyjson.MarshalType(ps.Config)
if err != nil {
return nil, fmt.Errorf("config: %w", err)
}
resources, err := marshalTypeMap(ps.Resources)
if err != nil {
return nil, fmt.Errorf("resources: %w", err)
}
dataSources, err := marshalTypeMap(ps.DataSources)
if err != nil {
return nil, fmt.Errorf("data sources: %w", err)
}
return &proto.ProviderSchema{
Type: ps.Type,
LocalNames: ps.LocalNames,
Config: config,
Resources: resources,
DataSources: dataSources,
}, nil
}

func marshalTypeMap(in map[string]cty.Type) (map[string][]byte, error) {
if len(in) == 0 {
return nil, nil
}
out := make(map[string][]byte, len(in))
for name, ty := range in {
raw, err := ctyjson.MarshalType(ty)
if err != nil {
return nil, fmt.Errorf("%q: %w", name, err)
}
out[name] = raw
}
return out, nil
}

func (c *client) EvaluateResource(ctx context.Context, req EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) EvaluationResponse {
ctx, span := tracer().Start(ctx, "policy.client.evaluate_resource",
trace.WithAttributes(attribute.String("policy.resource.type", req.Target)),
Expand Down
22 changes: 22 additions & 0 deletions internal/policy/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ type MockClient struct {
EvaluateModuleRequest EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]
EvaluateModuleFn func(context.Context, EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) EvaluationResponse

// ValidateProviderSchemas method tracking
ValidateProviderSchemasCalled bool
ValidateProviderSchemasResponse *ValidateProviderSchemasResponse
ValidateProviderSchemasRequest ValidateProviderSchemasRequest
ValidateProviderSchemasFn func(context.Context, ValidateProviderSchemasRequest) ValidateProviderSchemasResponse

// Stop method tracking
StopCalled bool
StopFn func()
Expand Down Expand Up @@ -115,6 +121,22 @@ func (p *MockClient) EvaluateModule(ctx context.Context, r EvaluationRequest[*pr
return resp
}

func (p *MockClient) ValidateProviderSchemas(ctx context.Context, req ValidateProviderSchemasRequest) (resp ValidateProviderSchemasResponse) {
defer p.beginWrite()()

p.ValidateProviderSchemasCalled = true
p.ValidateProviderSchemasRequest = req
if p.ValidateProviderSchemasFn != nil {
return p.ValidateProviderSchemasFn(ctx, req)
}

if p.ValidateProviderSchemasResponse != nil {
return *p.ValidateProviderSchemasResponse
}

return resp
}

func (p *MockClient) Stop() {
defer p.beginWrite()()
p.StopCalled = true
Expand Down
29 changes: 29 additions & 0 deletions internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type Client interface {
EvaluateResource(context.Context, EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) EvaluationResponse
EvaluateProvider(context.Context, EvaluationRequest[*proto.PolicyEvaluateProviderRequest_ProviderMetadata]) EvaluationResponse
EvaluateModule(context.Context, EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) EvaluationResponse
// ValidateProviderSchemas validates the loaded policies against the run's
// provider schemas, so a policy that references an attribute a provider does
// not have fails early. Called after Setup, once schemas are resolved.
ValidateProviderSchemas(context.Context, ValidateProviderSchemasRequest) ValidateProviderSchemasResponse
Stop()
}

Expand All @@ -29,6 +33,31 @@ type CallbackService interface {
RegisterCallbackService(context.Context) (*callback.Server, Diagnostics)
}

type (
// ValidateProviderSchemasRequest carries the provider schemas to validate the
// loaded policies against.
ValidateProviderSchemasRequest struct {
ProviderSchemas []ProviderSchema
}

// ProviderSchema is one provider's schema as cty object types: its
// configuration and the object type of each resource and data source it
// offers, plus the local names it is known by in configuration.
ProviderSchema struct {
Type string
LocalNames []string
Config cty.Type
Resources map[string]cty.Type
DataSources map[string]cty.Type
}

// ValidateProviderSchemasResponse carries the diagnostics from validating the
// loaded policies against the schemas.
ValidateProviderSchemasResponse struct {
Diagnostics Diagnostics
}
)

type (
SetupResponse struct {
// serverCapabilities contains the map of a policy path to the capabilities of the server.
Expand Down
Loading