Decision on API Design

This decision page is currently still a WIP.

Problem

This decision is about how to define an identity and "a common look" as mentioned here.

The ideal way would be, that we can re-use the exact same XRD for each set of services. For example all implementations for PostgreSQL have exactly the same API for the user. Due to the nature of the product, not all implementations provide the same functionality. The set of available features is most likely not the same between most providers. Also with Crossplane we’re a bit limited on what’s possible with translating the source API spec into the target API spec:

  • No conditionals in compositions

  • Only a limited set of transformations

  • Can’t create random values

This is not a complete list of limitations.

Requirements

  • APIs for a set of similar services feel similar

  • Only minimal configuration is needed for a default case

  • Unique features of a service provider can be leveraged

  • Expose as few options as possible and extend as we go

Proposals

Proposal 1: Use same XRD with provider specific fields

There’s exactly one XRD for a set of services. This XRD contains a minimal set of common parameters, as well as a field for provider specific configurations.

Example Claim
apiVersion: appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    majorVersion: '14'
    pgSettings: {}
    exoscale:
      plan: hobbyist-2
      type: pg
      maintenance:
        day: 2
        hour: 22
        minute: 0
      backup:
        hour: 20
        minute: 0
    vshn:
      minorVersion: '4'
Advantages
  • We can use the same XRD

  • Adding new providers to the list is easy

Disadvantages
  • We don’t really have a "common look"

  • The user needs to know some specifics of the underlying service provider.

  • There may only be 1–3 common fields for a given service

Proposal 2: Separate XRDs with Convention

This approach will have different XRDs for each provider. However, they will follow the same convention, as far as possible. All the top level fields will be the same, the differences will be apparent in the sub-fields. Thus creating groups of fields. These groups can be different for any given service type. They will also have the same kind, but a different APIVersion.

If a provider within a service type doesn’t support all the groups, then we’ll omit the group as a whole. This will also make it clear at one glance that the given provider doesn’t support this feature.

The fields below a group are not specified and depend on what the provider is capable of. Each group describes a very generic part of a service, like its size or maintenance.

It also depends on the provider if adding grouping makes actually sense, see examples.

Example Claim
# Exoscale example
apiVersion: exoscale.appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    service:
      version: '14'
      type: pg
      settings: {}
    size: (1)
      plan: hobbyist-2
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0

# Converged example
apiVersion: vshn.appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    service:
      version: '14.4'
      settings: {}
      pgbouncer:
        settings: {}
    size: (1)
      cpu: 2
      memory: 8Gi
      disk: 200Gi
      encrypted: true
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0

# Bucket without any grouped fields
apiVersion: appcat.vshn.io/v1
kind: ObjectBucket
metadata:
  name: my-cool-object-storage-bucket
  namespace: my-namespace
spec:
  parameters:
    bucketName: my-bucket-change-name
    region: rma (2)

# Managed Gitlab
apiVersion: vshn.appcat.vshn.io/v1
kind: Gitlab
metadata:
  name: gitlab-prod
spec:
  parameters:
    name: gitlab-prod
    service:
      url: https://gitlab.example.com
      runners: true
      pages: true
    size: (1)
      runners: 3
      disk: 300Gi
      ha: true
    clients: (3)
      - 192.168.1.0/24
      - 192.168.2.0/24
    #no maintenance or backup as of now
1 Size might be an example top-level parameter that can be used in many services, although its subfields are provider-specific. If a provider doesn’t make use of a given top-level parameter, then it may simply be omitted from the API scheme.
2 Object bucket is a very simple service that doesn’t provide many configuration options. Using any grouped top-level parameters doesn’t make sense.
3 Clients might be an example of a top-level parameter that only finds use for some very specific services.
Advantages
  • Same look between all providers, deploying PostgreSQL to any backend feels familiar

  • Specifics can be handled in a sensible way

Disadvantages
  • The user could get confused by objects with the same kind

  • The user needs to know some specifics of the underlying service provider.

Proposal 3: Composed XRDs

There’s a base XRD that handles the common configurations. All provider specific parameters are put in a separate XRD.

This idea may not be possible with Crossplane, but it’s here for completeness’ sake.

Example Claim
apiVersion: appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    version: '14'
    pgSettings: {}


apiVersion: exoscale.appcat.vshn.io/v1
kind: PostgreSQLConfig
metadata:
  name: my-instance
spec:
  parameters:
    instanceRef: my-instance
    plan: hobbyist-2
    type: pg
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0
Advantages
  • Clear separation of base and specific configuration parameters

Disadvantages
  • "same look" will only apply to the base XRDs

  • Pretty complicated to use for the end-user

  • There may only be 1–3 common fields for a given service

Proposal 4: Same XRD and Parse the Input

We use the same XRD for every provider. Most fields are simply strings, and they will then be parsed in the composition.

For example one provider has hobbyist-2 as a valid size, while other need CPU/Memory/Disk, this could be represented by custom-2-8-200. This is inspired by GCP Terraform.

Example Claim
# Exoscale example
apiVersion: appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    version: '14'
    pgSettings: {}
    # we pass the names of the plans for exoscale
    size: hobbyist-2
    type: pg
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0

# vshn converged example
apiVersion: appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    # vshn converged might need the minor version, too
    version: '14.4'
    pgSettings: {}
    # we derive the size of the instance from this string
    # custom-$cpu-$memory-$diskspace
    size: custom-2-8-300
    # vshn converged may not have a type
    type: ''
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0
Advantages
  • Exact same API for all services of the same set

Disadvantages
  • Awkward to use, there needs to be a lot of documentation

  • The complexity of the compositions increases drastically with the parsing rules

Proposal 5: Group Specific Settings under One Field

This is a sub-variant of proposal 2. Everything that has provider specific naming is grouped under a field called providerSpecific. If a provider doesn’t support a given top-level parameter (for example maintenance), then it’s omitted.

Example Claim
# Exoscale
apiVersion: exoscale.appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0
    providerSpecific:
      version: "14"
      service:
        type: pg
        settings: {}
        plan: hobbyist-2

# VSHN Converged
apiVersion: vshn.appcat.vshn.io/v1
kind: PostgreSQL
metadata:
  name: my-instance
spec:
  parameters:
    name: my-instance
    maintenance:
      day: 2
      hour: 22
      minute: 0
    backup:
      hour: 20
      minute: 0
    providerSpecific:
      version: "14.4"
      service:
        settings: {}
      resources:
        cpu: 2
        memory: 2048
        disk: 200G
Advantages
  • All provider specific fields are grouped

Disadvantages
  • There may only be 1–3 common fields for a given service, shifting everything under the providerSpecific field

  • Increases the nesting

Decision

We agreed on Proposal 2.

Additional points we’ve agreed on:

  • Exposing only needed fields, add more as needed

  • Sensible defaults, minimize required parameters.

To minimize confusion when querying the claims. The kind should also contain the provider as a pre-fix:

Example Kind
apiVersion: acmecorp.appcat.vshn.io/v1
kind: AcmecorpPostgreSQL

Addendum 1 Exposing Versions

We expose the versioning of the various services via the composite as an enum. The default of the field should always point to the most recent stable version.

Rationale

Proposal 2 provides the most flexibility for modelling the various limitations and differences that various providers might have. All other proposals are hard definitions what the API should look like and are not a convention. This makes it hard to tailor the various provider limitations and differences to fit the definitions.

To achieve the identity and same look for the various providers and services a flexible convention makes the most sense.

Avoiding Confusion with Specific Kinds

In Kubernetes there’s ambiguity if there are multiple CRDs with the same kind but different apiversion fields.

Given a Kubernetes cluster with these CRDs:

  • foo.a.io

  • foo.b.io

If a user invokes kubectl get foo only CRs of type foo.a.io are returned [1]. This could lead to "missing" CRs from a user perspective

Addendum 1 Exposing Versions

The main point for this approach is UX. Users get instant feedback if their desired version isn’t supported. Available versions as directly visible via kubectl and no other documentation has to be consulted.

One drawback for this is, that versions need to be adjusted if a provider changes them. This issue is much smaller for VSHN Managed Services, as new versions are controlled by ourselves.


1. As far as we were able to determine, the Kubernetes API server returns the first API group it finds by alphabetical order.