crapi

package module
v0.0.0-...-94a8b15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

CRAPI

CRAPI (CR <-> API) is a Go library for Kubernetes operators that need to bridge the gap between a Custom Resource (CR) and an external API.

When an operator manages resources backed by an HTTP API, it must translate the Kubernetes-shaped spec into an API request, and then map the API response back into a CR status (or a full spec+status round-trip). crapi makes both directions easy, including edge cases like resolving Kubernetes object references (e.g. GroupRef) and fetching sensitive values from Kubernetes Secret objects before forwarding them to the API.

How it works

The Translator interface is the core abstraction:

type Translator interface {
    // ToAPI converts a CR spec into an API request struct.
    // Dependency objects (Secrets, referenced CRs) are passed as extra arguments
    // so the translator can resolve references before producing the request.
    ToAPI(target any, source client.Object, objs ...client.Object) error

    // FromAPI converts an API response into a CR, populating both spec and status.
    // Any values extracted into separate Kubernetes objects (e.g. Secrets) are
    // returned as extra objects.
    FromAPI(target client.Object, source any, objs ...client.Object) ([]client.Object, error)
}

Create a Translator with NewTranslator (single API version) or NewPerVersionTranslators (multiple versions indexed by SDK major version). Both accept optional trailing TranslatorOptions (see Field-level transformers below).

Breaking change: NewPerVersionTranslators's versions parameter is a []string (previously a variadic ...string), to make room for the new trailing opts ...TranslatorOption parameter. Existing callers must update:

- NewPerVersionTranslators(scheme, crd, "v1", "v20250312", "v20250810")
+ NewPerVersionTranslators(scheme, crd, "v1", []string{"v20250312", "v20250810"})

NewTranslator is unaffected and remains fully backwards compatible.

Example

The examples below are derived from the test suite and use a Group (Atlas project) resource.

API response → CR (FromAPI)
// API response from the Atlas API
apiGroup := admin2025.Group{
    Id:           ptr("6127378123219"),
    Name:         "test-project",
    OrgId:        "60987654321654321",
    ClusterCount: 0,
    Created:      time.Date(2025, 1, 1, 1, 30, 15, 0, time.UTC),
    Tags: &[]admin2025.ResourceTag{
        {Key: "env", Value: "prod"},
    },
    WithDefaultAlertsSettings: ptr(true),
}

// Target CR to be populated
cr := &samplesv1.Group{
    Spec: samplesv1.GroupSpec{
        V20250312: &samplesv1.GroupSpecV20250312{
            ProjectOwnerId: ptr(""),  // read-only field preserved from spec
        },
    },
}

_, err := tr.FromAPI(cr, &apiGroup)
// cr.Spec.V20250312.Entry  → writable fields: Name, OrgId, Tags, ...
// cr.Status.V20250312      → read-only fields: Id, Created, ClusterCount, ...
CR spec → API request (ToAPI)
// CR with the desired state
cr := &samplesv1.Group{
    Spec: samplesv1.GroupSpec{
        V20250312: &samplesv1.GroupSpecV20250312{
            Entry: &samplesv1.GroupSpecV20250312Entry{
                Name:                      "project-name",
                OrgId:                     "60987654321654321",
                WithDefaultAlertsSettings: ptr(true),
                Tags: &[]samplesv1.Tags{
                    {Key: "env", Value: "prod"},
                },
            },
        },
    },
}

// Empty target struct to be filled
var req admin2025.Group

err := tr.ToAPI(&req, cr)
// req is now ready to pass directly to the Atlas SDK client
Resolving references and secrets (ToAPI with dependencies)

When a CR refers to another CR by name (e.g. GroupRef) or stores sensitive values in a Secret, pass those objects as dependencies. The translator resolves the references automatically:

cr := &samplesv1.GroupAlertsConfig{
    Spec: samplesv1.GroupAlertsConfigSpec{
        V20250312: &samplesv1.GroupAlertsConfigSpecV20250312{
            GroupRef: &crd2gok8s.LocalReference{Name: "my-project"},
            Entry: &samplesv1.GroupAlertsConfigSpecV20250312Entry{
                Notifications: &[]samplesv1.Notifications{
                    {
                        DatadogApiKeySecretRef: &samplesv1.PasswordSecretRef{
                            Name: "datadog-secret",
                        },
                        DatadogRegion: ptr("US"),
                    },
                },
            },
        },
    },
}

deps := []client.Object{
    // Referenced Group CR — its status.Id is injected as GroupId in the request
    &samplesv1.Group{
        ObjectMeta: metav1.ObjectMeta{Name: "my-project"},
        Status: samplesv1.GroupStatus{
            V20250312: &samplesv1.GroupStatusV20250312{Id: ptr("62b6e34b3d91647abb20e7b8")},
        },
    },
    // Secret referenced in the spec
    &corev1.Secret{
        ObjectMeta: metav1.ObjectMeta{Name: "datadog-secret", Namespace: "ns"},
        Data:       map[string][]byte{"password": []byte("dd-api-key-value")},
    },
}

var req admin2025.GroupAlertsConfig
err := tr.ToAPI(&req, cr, deps...)
// req.GroupId           == "62b6e34b3d91647abb20e7b8"  (resolved from GroupRef)
// req.Notifications[0].DatadogApiKey == "dd-api-key-value"  (resolved from Secret)
Field-level transformers

By default, translation works via a JSON round-trip through map[string]any. This works well when the CRD and SDK field types share a compatible JSON representation, but can produce unstable results when they don't — for example, a CRD stores DeleteAfterDate as *string (RFC3339 text) while the SDK expects *time.Time. Without transformers, the JSON round-trip may parse the string into a time.Time with a non-canonical format, causing spurious diffs on every reconciliation cycle.

Field-level transformers give you fine-grained control over how individual fields are converted. Register them via WithToAPIFieldTransformer and WithFromAPIFieldTransformer when constructing the translator.

Without transformers:

tr, err := crapi.NewTranslator(scheme, crd, "v1", "v20250312")
// The default JSON round-trip will deserialize "deleteAfterDate"
// into *time.Time via time.UnmarshalJSON, but the result may use a
// non-canonical format, triggering unnecessary spec updates.

With transformers:

// stringToTime parses a string RFC3339 value into time.Time for the SDK.
func stringToTime(layout string) crapi.FieldTransformer {
    return func(_ string, value any) (any, error) {
        s, ok := value.(string)
        if !ok {
            return nil, crapi.ErrNoMatch
        }
        t, err := time.Parse(layout, s)
        if err != nil {
            return nil, fmt.Errorf("parse time: %w", err)
        }
        return t, nil
    }
}

// timeToString normalizes a time-valued string field to a canonical string.
func timeToString(layout string) crapi.FieldTransformer {
    return func(_ string, value any) (any, error) {
        s, ok := value.(string)
        if !ok {
            return nil, crapi.ErrNoMatch
        }
        t, err := time.Parse(time.RFC3339, s)
        if err != nil {
            return nil, fmt.Errorf("parse time: %w", err)
        }
        return t.Format(layout), nil
    }
}

tr, err := crapi.NewTranslator(
    scheme, crd, "v1", "v20250312",
    crapi.WithToAPIFieldTransformer("deleteAfterDate", stringToTime(time.RFC3339)),
    crapi.WithFromAPIFieldTransformer("deleteAfterDate", timeToString(time.RFC3339)),
)

Returning crapi.ErrNoMatch from a transformer signals that it does not apply to the given value, allowing the next registered transformer to try (or leaving the value unchanged if none match). Transformers are matched by an anchored regular expression against the field's dotted path — "deleteAfterDate" matches the top-level field, "nested\\.deleteAfterDate" matches a field inside a nested object, and "items\\[\\]\\.deleteAfterDate" matches fields inside array elements.

License

Apache License 2.0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoMatch = errors.New("field transformer: no match")

ErrNoMatch is returned by a FieldTransformer to signal that it does not apply to the given field path or value, so the processor should try the next registered transformer (if any) instead of treating it as an error.

Functions

func NewPerVersionTranslators

func NewPerVersionTranslators(scheme *runtime.Scheme, crd *apiextensionsv1.CustomResourceDefinition, crdVersion string, versions []string, opts ...TranslatorOption) (map[string]Translator, error)

NewPerVersionTranslators creates a set of translators indexed by SDK versions.

Given the following example resource:

apiVersion: atlas.generated.mongodb.com/v1
kind: SearchIndex
metadata:
  name: search-index
spec:
  v20250312:
    ...
  v20250810:

In the above case crdVersion is "v1" and versions is ["v20250312", "v20250810"]. Optional opts ...TranslatorOption are applied to each translator created.

Types

type FieldTransformer

type FieldTransformer func(fieldPath string, value any) (any, error)

FieldTransformer transforms a field value found at fieldPath during translation (ToAPI or FromAPI). fieldPath is a dot-separated path from the root of the translated object map (e.g. "deleteAfterDate" or "foo.bar"), with array traversal represented by a trailing "[]" segment (e.g. "items[].deleteAfterDate").

Implementations must return ErrNoMatch (wrapped or not, checked via errors.Is) when they don't apply to this path/value, so that other registered transformers get a chance to run. Any other non-nil error aborts translation.

type Request deprecated

type Request struct {
	Translator   Translator
	Dependencies []client.Object
}

Request is deprecated do not use

Deprecated: request is no longer used in the ToAPI and FromAPI calls

type Translator

type Translator interface {
	// Scheme returns the Kubernetes scheme used to translate the CRD.
	Scheme() *runtime.Scheme

	// MajorVersion returns the pinned SDK major version
	MajorVersion() string

	// Mappings returns all the OpenAPi custom reference extensions, or an error
	Mappings() ([]*refs.Mapping, error)

	// ToAPI translates a source Kubernetes object into a target API structure.
	// It uses the spec only to populate ethe API request, nothing from the status.
	// The target is set to a API request struct to be filled.
	// The source is set to the Kubernetes CR value. Only the spec data is used here.
	// The request includes the translator and the dependencies associated with the
	// source CR, usually Kubernetes secrets.
	ToAPI(target any, source client.Object, objs ...client.Object) error

	// FromAPI translates a source API structure into a Kubernetes object.
	// The API source is used to populate the Kubernetes spec, including the
	// spec.entry and status as well.
	// The target is set to CR value to be filled. Both spec and status are filled.
	// The source is set to API response.
	// The request includes the translator and any dependencies associated with the
	// source CR.
	// Returns any extra objects extracted from the response as separate Kubernetes
	// objects, such as Kubernetes secrets, for instance. This list does not include
	// the mutated target, and will be empty if nothing else was extracted off the ç
	// response.
	FromAPI(target client.Object, source any, objs ...client.Object) ([]client.Object, error)
}

Translator allows to translate back and forth between a CRD schema and SDK API structures of a certain version. A translator is an immutable configuration object, it can be safely shared across goroutines

func NewTranslator

func NewTranslator(scheme *runtime.Scheme, crd *apiextensionsv1.CustomResourceDefinition, crdVersion string, majorVersion string, opts ...TranslatorOption) (Translator, error)

NewTranslator creates a translator for a particular CRD version. It is also locked into a particular API majorVersion.

Given the following example resource:

apiVersion: atlas.generated.mongodb.com/v1
kind: SearchIndex
metadata:
  name: search-index
spec:
  v20250312:

In the above case crdVersion is "v1" and majorVersion is "v20250312".

type TranslatorOption

type TranslatorOption func(*translator)

TranslatorOption configures optional behavior on a translator created via NewTranslator or NewPerVersionTranslators.

func WithFromAPIFieldTransformer

func WithFromAPIFieldTransformer(fieldPathPattern string, fn FieldTransformer) TranslatorOption

WithFromAPIFieldTransformer registers fn to run during FromAPI for any field path matching fieldPathPattern. See WithToAPIFieldTransformer for matching and precedence semantics.

func WithToAPIFieldTransformer

func WithToAPIFieldTransformer(fieldPathPattern string, fn FieldTransformer) TranslatorOption

WithToAPIFieldTransformer registers fn to run during ToAPI for any field path matching fieldPathPattern. fieldPathPattern is compiled as an anchored regular expression (^fieldPathPattern$). Multiple transformers can be registered; they run in registration order and the first one that does not return ErrNoMatch wins for that field.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL