Skip to content

Resources

A resource manages CRUD operations on an external object. It is implemented as an instance of the runtime.TypedResource[In, Out, Config any] interface.

The following is a runtime.TypedResource[File, *FileOutput, runtime.NoConfig]:

type File struct {
    Path    string
    Content string
}

type FileOutput struct {
    Path   string
    Size   int64
    Exists bool
}

func (f *File) SchemaVersion() int { return 1 }
func (f *File) ReplaceFields() []string { return []string{"path"} }

func (f *File) Create(ctx context.Context, cfg runtime.NoConfig) (*FileOutput, error) {
    return writeFile(f.Path, f.Content)
}

func (f *File) Read(
    ctx context.Context,
    cfg runtime.NoConfig,
    prior *FileOutput,
) (*FileOutput, error) {
    return readFile(prior)
}

func (f *File) Update(
    ctx context.Context,
    cfg runtime.NoConfig,
    prior runtime.Prior[File, *FileOutput],
) (*FileOutput, error) {
    return writeFile(f.Path, f.Content)
}

func (f *File) Delete(ctx context.Context, cfg runtime.NoConfig, prior *FileOutput) error {
    return removeFile(prior)
}

Register it:

Resources: map[string]runtime.ResourceRegistration{
    "file": runtime.MakeResource[File, *FileOutput, runtime.NoConfig](),
}

Use MakeResourceWith when each receiver needs a constructed client, fake, or other external state. The constructor runs each time the runtime needs a receiver, then the resource body is decoded into that receiver.

Read and not found

Return runtime.ErrNotFound from Read when the external object is absent. The runtime treats that as a request to create it again.

Read runs during planning for resources that already have state. Create, Update, Delete, and replacement work run only during apply.

Update

runtime.Prior[In, Out] includes:

  • Inputs, the prior evaluated inputs.
  • Outputs, the prior resource outputs.
  • Observed, the plan-time read result.

Use runtime.Changed(prior.Inputs.Field, current.Field) to compare decoded values.

Replace fields

ReplaceFields names input fields that require replacement when changed. Other changes call Update.

Apply-time input validation

For checks that need the decoded library config or an external lookup, implement runtime.InputValidator[Config] on the resource receiver:

func (f *File) ValidateInputs(ctx context.Context, cfg runtime.NoConfig) error {
    if f.Path == "" {
        return errors.New("path is required")
    }
    return nil
}

The runtime calls ValidateInputs after decoding the desired inputs and before Create, Update, or the create side of a replacement. For a replacement, validation runs before the prior object is deleted. It is not called for no-op or destroy steps.

Prefer schemas, defaults, and constraints for checks that are known from source. Use ValidateInputs for runtime checks that cannot be expressed in the compile-time input schema.

Equivalent inputs

Implement runtime.InputEquivalencer[In] when two different input values represent the same value for a resource field. For example, an AWS Lambda function where both its name and ARN are equivalent:

func (r *Alias) EquivalentInput(field string, prior, current Alias) bool {
    if field != "function-name" {
        return false
    }
    return equivalentFunctionNameOrARN(prior.FunctionName, current.FunctionName)
}

func equivalentFunctionNameOrARN(prior, current string) bool {
    if prior == current {
        return true
    }
    if name, ok := lambdaFunctionNameFromIdentifier(prior); ok && name == current {
        return true
    }
    if name, ok := lambdaFunctionNameFromIdentifier(current); ok && name == prior {
        return true
    }
    return false
}

field is the Unobin field name from the resource body. If the method returns true, the field does not count as an input change for update planning, replacement triggers, or the apply-time plan premise check. Return true only when the resource implementation treats the two values the same.

Resource plan modifiers

Implement runtime.ResourcePlanModifier[In, Out, Config] when a resource can tell the planner that an output will be recomputed at apply:

func (f *File) ModifyResourcePlan(
    req runtime.ResourcePlanRequest[File, *FileOutput, runtime.NoConfig],
    resp *runtime.ResourcePlanResponse,
) error {
    if req.HasPriorState && runtime.Changed(req.PriorInputs.Content, req.CurrentInputs.Content) {
        resp.MarkOutputUnknown("size")
    }
    return nil
}

The request contains the decoded config, prior inputs, current inputs, prior outputs, and whether prior state exists. The runtime calls the method during planning for a resource with prior state, after current inputs decode.

MarkOutputUnknown names output fields that apply will recompute. A later node that reads one of those fields waits for apply instead of using the prior value in the plan. Marking an output unknown also makes the resource a possible update unless replacement already applies.