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;