Handling major changes in Commodore components

Generally, it’s considered best practice to avoid changes which require manual intervention when upgrading the component.

This document provides best practices to avoid requiring manual intervention during the component upgrade when making changes of the following types:

For structural changes of parameters and replacing parameters, it’s considered best practice to apply the principle of forward only migration. We give examples of how such migrations can be implemented in the sections below.

Major version change of a dependency

When updating a component to use a new major version of a dependency, it’s considered best practice to make use of ArgoCD’s resource hooks to perform steps that are required to upgrade the dependency.

See upgrade.jsonnet in component cert-manager for an example sync hook definition that ensures that ArgoCD can update the existing CRDs on the cluster.

Structural changes of a component’s existing parameters

It’s considered best practice to support both the old and new structure of existing parameters, when a structural change is required. Structural change can be detected in Jsonnet by inspecting the parameters type or present fields.

To identify a parameter’s type in Jsonnet the standard library function std.type(), or the convenience wrappers, such as std.isString() can be used. To inspect the fields present in a parameter of type object, the Jsonnet function std.objectFields() can be used.

The parameter’s type can then be used to transform the old structure into the new structure in the implementation, giving users a window to migrate their configurations to the new structure.

Example

In this example, we want to change a parameter foo from having a value of type string to having a value of type object. The previous string value is moved to key value in the new object value. Additionally, the object value has a key name, which was previously hard-coded to FOO_VALUE.

The following Jsonnet snippet creates a local variable foo which will always hold parameter foo in the new object form:

// We assume that the component's parameters are available as local variable `params`.
local foo =
  if std.isString(params.foo) then
    // Transform legacy string parameter to object
    {
      name: 'FOO_VALUE', (1)
      value: params.foo,
    }
  else
    // Assume object type by default and use the provided object
    params.foo;
1 As mentioned above, we inject name: 'FOO_VALUE' to preserve the old behavior of the component for configurations which haven’t been updated to the new parameter structure yet.

Replacing a component parameter

It’s considered best practice to support both the old and the new parameter, when a component parameter needs to be replaced. In addition, the implementation should adhere to the following best practices:

  • If users supply the old parameter, it takes precedence over the new parameter.

  • The component defaults only contain the new parameter.

In Jsonnet, we don’t have a clean way to identify users who provide both the old and new parameter. Implementations can try to identify user-supplied values for the new parameter by comparing the rendered parameter value with the expected parameter default value. However, we don’t require best-practice implementations to do so, as there may be cases where it’s hard to identify a clear expected parameter default.

Example

In this example, we want to replace a parameter secret which takes a string with a parameter secretRef which takes an object. The value of the old parameter secret is moved to key name in the new parameter’s value. The component’s defaults only have the new parameter:

parameters:
  my_component:
    secretRef:
      name: my-secret

The following Jsonnet snippet creates a local variable secretRef which is constructed from the old parameter secret, if it exists, and holds the value of parameter secretRef otherwise:

// We assume that the component's parameters are available as local variable `params`.
local secretRef =
  if std.objectHas(params, 'secret') then
    { name: params.secret }
  else
    params.secretRef;