03 - Target Configuration

This section explains how plugins receive target configuration.

What is a Target?

A target uniquely identifies a deployment location for your infrastructure. Think of it as "where" your resources live:

  • For AWS: a region like us-east-1
  • For Kubernetes: a cluster endpoint
  • For SFTP: a server URL like sftp://files.example.com:22

Targets are stored in formae's database in plaintext, so they must never contain secrets. The target identifies the location; credentials are provided separately.

Target vs Credentials

Target Credentials
Purpose Identifies deployment location Authenticates to the location
Storage Plaintext in database Environment variables / secrets
Example (SFTP) sftp://files.example.com:22 Username, password
Example (AWS) us-east-1 Access key, secret key

Target Configuration in Requests

Every request to your plugin includes a TargetConfig field:

type CreateRequest struct {
    ResourceType string
    Properties   json.RawMessage
    TargetConfig json.RawMessage  // <-- Target configuration (no secrets!)
}

Define the Target Config Type

For our SFTP plugin, the target is just the server URL. Add this to sftp.go:

// TargetConfig holds SFTP target settings.
// Contains only the deployment location, NOT credentials.
type TargetConfig struct {
    URL string `json:"url"` // sftp://host:port
}

func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) {
    var cfg TargetConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("invalid target config: %w", err)
    }
    if cfg.URL == "" {
        return nil, fmt.Errorf("target config missing 'url'")
    }
    return &cfg, nil
}

func parseURL(sftpURL string) (host string, port string, err error) {
    u, err := url.Parse(sftpURL)
    if err != nil {
        return "", "", fmt.Errorf("invalid URL: %w", err)
    }
    if u.Scheme != "sftp" {
        return "", "", fmt.Errorf("expected sftp:// URL, got %s://", u.Scheme)
    }
    host = u.Hostname()
    port = u.Port()
    if port == "" {
        port = "22"
    }
    return host, port, nil
}

Update sftp.go

Add the target configuration and credentials code. Update the imports and add the helper functions:

package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/url"
    "os"

    "github.com/platform-engineering-labs/formae/pkg/plugin"
    "github.com/platform-engineering-labs/formae/pkg/plugin/resource"
)

// TargetConfig holds SFTP target settings.
// Contains only the deployment location, NOT credentials.
type TargetConfig struct {
    URL string `json:"url"` // sftp://host:port
}

func parseTargetConfig(data json.RawMessage) (*TargetConfig, error) {
    var cfg TargetConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("invalid target config: %w", err)
    }
    if cfg.URL == "" {
        return nil, fmt.Errorf("target config missing 'url'")
    }
    return &cfg, nil
}

func parseURL(sftpURL string) (host string, port string, err error) {
    u, err := url.Parse(sftpURL)
    if err != nil {
        return "", "", fmt.Errorf("invalid URL: %w", err)
    }
    if u.Scheme != "sftp" {
        return "", "", fmt.Errorf("expected sftp:// URL, got %s://", u.Scheme)
    }
    host = u.Hostname()
    port = u.Port()
    if port == "" {
        port = "22"
    }
    return host, port, nil
}

func getCredentials() (username, password string, err error) {
    username = os.Getenv("SFTP_USERNAME")
    password = os.Getenv("SFTP_PASSWORD")
    if username == "" || password == "" {
        return "", "", fmt.Errorf("SFTP_USERNAME and SFTP_PASSWORD must be set")
    }
    return username, password, nil
}

// ... rest of Plugin implementation

Config Field Mutability

You can annotate target config fields to tell formae which fields can be changed in place and which require a full target replace. Add @formae.ConfigFieldHint annotations to your PKL Config class:

import "@formae/formae.pkl"

open class Config {
  hidden fixed type: String = "SFTP"

  /// The server URL. Changing this requires recreating everything.
  @formae.ConfigFieldHint { createOnly = true }
  hidden url: String

  fixed Type: String = type
  fixed URL: String = url
}
  • createOnly = true — immutable, triggers target replace (default for unannotated fields)
  • createOnly = false — mutable, allows in-place update

For our SFTP plugin, the URL is the only config field and it identifies the deployment location, so it should be immutable. For plugins with credential-like config fields (e.g. a named profile), marking those as createOnly = false lets users switch credentials without recreating resources.

See the schema reference for full details.

Verify

To verify that the target configuration code compiles correctly:

cd formae-plugin-sftp
make build

Next: 04 - Plugin Configuration