It is possible to explicitly configure all kinds of context by using the various configuration methods provided by a context.
Nevertheless, it might be useful to externalize the configuration and delegate it to some configuration objects. For example, the credential settings should be taken from a configuration file.
This is the task of another kind of context, the config context. It manages a sequence of applied configuration objects. A configuration object takes the burden to keep some configuration data and apply it to an appropriate configuration target.
Such a configuration target is typically again a context. For example a configuration object dealing with credential settings configures a credential context. But in general, a configuration target may be any kind of object. To apply a configuration, a configuration object is called for a desired target. The object may decide to apply itself to this target or to bypass the target object.
The configuration objects are typed and should be serializable. This enables the configuration context to use scheme objects, like the other context types to reconstruct configurations from a byte stream/textual representation.
Besides dedicated configuration object types provided by the various context types, the config context provides a generic configuration type, also. It is basically a list of other configuration objects, that can be reconstructed by their deserialization schemes. If applied to a target object, it just applies the contained configuration in the order of their appearance.
A first example how to use configuration objects can be found here.
It just configures a configuration object provided by the credential context able to configure credential settings.
cid := credentials.ConsumerIdentity{
ociid.ID_TYPE: ociid.CONSUMER_TYPE,
ociid.ID_HOSTNAME: "ghcr.io",
ociid.ID_PATHPREFIX: "mandelsoft",
}
// create a credential configuration object
// and configure it to provide some direct consumer credentials.
creds := ccfg.New()
creds.AddConsumer(
cid,
directcreds.NewRepositorySpec(cfg.GetCredentials().Properties()),
)
It just declares direct credentials for a dedicated consumer id (see the credentials example).
The ocm context can be used to get access to the appropriate configuration context, which is used to apply the configuration object.
octx := ocm.DefaultContext()
cctx := octx.ConfigContext()
err = cctx.ApplyConfig(creds, "explicit")
if err != nil {
return errors.Wrapf(err, "cannot apply config")
}
After the object has been applied, the result can be observed on the intended target object. For a credential configuration this is the credential context. Here, the credentials for the configured consumer id can be queried. In the example this is a credentials object valid for an OCI registry.
credctx := octx.CredentialsContext()
found, err := credctx.GetCredentialsForConsumer(cid, ociid.IdentityMatcher)
if err != nil {
return errors.Wrapf(err, "cannot extract credentials")
}
got, err := found.Credentials(credctx)
if err != nil {
return errors.Wrapf(err, "cannot evaluate credentials")
}
fmt.Printf("found: %s\n", got)
The first call provides a potentially dynamic credential source for the requested consumer. This object can then be used to determine actual credentials valid for an actual resource access. This indirection is introduced to support dynamic just-in-time credential generation processes.
The function credentials.CredentialsForConsumer
can be used, instead, to directly get access to the credential properties with a single function call.
The previous example just demonstrates the basic flow, it might not be very useful, because the consumer could directly be configured at the credential context.
The complete mechanism becomes valuable, if some kind of generic configuration handling is required. This could be, for example, to read configurations from external configuration sources, e.g. a central configuration file.
Instead of providing dedicated mechanism to configure various target environments the configuration context provides a uniform generic mechanism to handle arbitrary configuration settings for any target environment.
The configuration context provides a runtime.Scheme object to register known configuration types, which offer a deserialization. This allows storing configuration settings in files. The configuration context itself provides an aggregative configuration object, which can be used to host any other configuration object.
As already known by all the scheme based contexts (for example the repository specifications), the serialized form always features a type field. A configuration object for the configuration context could look as follows:
type: generic.config.ocm.software/v1
configurations:
- type: credentials.config.ocm.software
consumers:
- identity:
type: OCIRegistry
hostname: ghcr.io
pathprefix: mandelsoft
credentials:
- type: Credentials
properties:
username: mandelsoft
password: some-token
repositories:
- repository:
type: DockerConfig/v1
dockerConfigFile: "~/.docker/config.json"
propagateConsumerIdentity: true
If supports a single data field configurations
, which is a list
of serialized configuration objects. In the example, here are two entries,
the configuration object from the example above, and a credential repository
specification referring to a docker config file (as used in the credentials
example).
It can be applied as whole as shown in the following code snippet:
data, err := ioutil.ReadFile(CFGFILE)
if err != nil {
return errors.Wrapf(err, "cannot read configuration file %s", CFGFILE)
}
octx := ocm.DefaultContext()
cctx := octx.ConfigContext()
_, err = cctx.ApplyData(data, runtime.DefaultYAMLEncoding, CFGFILE)
if err != nil {
return errors.Wrapf(err, "cannot apply config data")
}
It uses the exactly same configuration mechanism shown in the previous example, so the query code looks all the same:
cid := credentials.ConsumerIdentity{
ociid.ID_TYPE: ociid.CONSUMER_TYPE,
ociid.ID_HOSTNAME: "ghcr.io",
ociid.ID_PATHPREFIX: "mandelsoft",
}
// as before
credctx := octx.CredentialsContext()
found, err := credctx.GetCredentialsForConsumer(cid, ociid.IdentityMatcher)
if err != nil {
return errors.Wrapf(err, "cannot extract credentials")
}
got, err := found.Credentials(credctx)
if err != nil {
return errors.Wrapf(err, "cannot evaluate credentials")
}
fmt.Printf("found: %s\n", got)
The complete example can be found here.
The OCM client tool supports reading configuration from a file ~/.ocmconfig
to configure the used OCM context.
This functionality is offered by a library function, also. The function
pkg.contexts.ocm.utils.Configure(ctx ocm.Context, path string, fss ...vfs.FileSystem) (ocm.Context, error)
searched for a configuration file and applies it. If not found it looks for a docker config file and applies an appropriate setting (see example above).
If the config data is already provided by some other means, it can be directly be applied with the function
pkg.contexts.ocm.utils.ConfigureByData(ctx ocm.Context, data []byte, info string) error
Both functions process the YAML content with spiff, an in-domain templating engine, which allows generating parts of the configuration.
It is very simple to provide own configuration types. It is just
a GO structure implementing the interface credentials.cpi.Config
. The main method
here is ApplyTo(configctx Context, target interface{}) error
, which is used to apply
the content to a dedicated target object. The method has to decide on its own
whether it applies to the passed object (type) at all, or what part of its content
is applied.
This way it is possible to provide configuration objects that configure multiple types of targets based on the same configuration information.
Any object may be used as target. If it is not accepted by any of the specified configuration objects, they are just ignored.
The typical use-case is to configure contexts. To be able to get up-to-date
with configuration settings applied after an object has been created (with
some initial configuration), the methods of a target object depending on potential
configuration, have to update the target configuration prior to their
execution. This is supported by an Update
object, which related to a
configuration context.
A complete example covering the following two sections can be found here.
A target type provides a connection to a configuration context using
an instance of the Updater
type. Additionally, it provides some
data, which is matter to some configuration.
type Target struct {
updater cpi.Updater
value string
}
func NewTarget(ctx cpi.Context) *Target {
return &Target{
updater: cpi.NewUpdate(ctx),
}
}
func (t *Target) SetValue(v string) {
t.value = v
}
func (t *Target) GetValue() string {
t.updater.Update(t)
return t.value
}
Whenever a method is called, which depends on potentially configurable
information the Update
method must be called on the updater instance.
The configuration context keeps track of a sequence of applied configuration
objects. The updater objects stored the sequence number of the latest executed
update. Calling the Update
method just replays the configuration objects
applied since the last update.
A configuration object then may look as follows:
const TYPE = "mytype.config.mandelsoft.org"
type Config struct {
runtime.ObjectVersionedType `json:",inline""`
Value string `json:"value"`
}
func (c *Config) ApplyTo(context cpi.Context, i interface{}) error {
if i == nil {
return nil
}
t, ok := i.(*Target)
if !ok {
return cpi.ErrNoContext(TYPE)
}
t.SetValue(c.Value)
return nil
}
var _ cpi.Config = (*Config)(nil)
func NewConfig(v string) *Config {
return &Config{
ObjectVersionedType: runtime.NewVersionedObjectType(TYPE),
Value: v,
}
}
The type must be uniquely chosen to support the aggregation of configuration objects in their serialized form.
With this preparing work, an application using the configuration context to configure this new target object could look like this:
ctx := config.DefaultContext()
target := NewTarget(ctx)
err := ctx.ApplyConfig(NewConfig("hello world"), "explicit1")
if err != nil {
return errors.Wrapf(err, "cannot apply config 1")
}
fmt.Printf("value is %q\n", target.GetValue())
err = ctx.ApplyConfig(NewConfig("hello universe"), "explicit2")
if err != nil {
return errors.Wrapf(err, "cannot apply config 2")
}
fmt.Printf("value is %q\n", target.GetValue())
newtarget := NewTarget(ctx)
fmt.Printf("value is %q\n", newtarget.GetValue())
Once a connection of the target object to a configuration context is established, it does not matter, whether the object is created before or after applying a configuration to the configuration context. Therefore, a configuration can be applied long before real targets are created.
The configuration context does never refer to potential targets, therefore, the garbage collection of target objects is not blocked by the existence of a configuration context for those objects.
If the new configuration object type is registered at the configuration context, it can even be used together with other configurations provided by a configuration file as shown in some example above.
For the default scheme used by the default context this can be done
by an init
function:
func init() {
cpi.RegisterConfigType(TYPE, cpi.NewConfigType(TYPE, &Config{}, "just provide a value for Target objects"))
}
It just creates a type object based on a prototype object and adds some documentation, which will automatically added to a command line documentation provided by cobra.
Now it is possible use a configuration file
type: generic.config.ocm.software/v1
configurations:
- type: mytype.config.mandelsoft.org
value: external configuration
to configure a dedicated context as shown in the second example:
data, err := ioutil.ReadFile(CFGFILE)
if err != nil {
return errors.Wrapf(err, "cannot read configuration file %s", CFGFILE)
}
_, err = ctx.ApplyData(data, runtime.DefaultYAMLEncoding, CFGFILE)
if err != nil {
return errors.Wrapf(err, "cannot apply config data")
}
fmt.Printf("value is %q\n", newtarget.GetValue())
When composing a new configuration context with a context builder, it is
possible to use another scheme instance than the default one,
configured by init
functions.