Introduction
In this tutorial you will learn how to create and work with CUE modules, using a custom module registry.
Along the way you will:
- Define a module containing a CUE schema
- Push the module to a custom registry
- Define a top level module that depends on the first module
- Use
cue mod tidyto automatically add dependencies and their versions to themodule.cuefile - Publish a module containing a CUE template that depends on the schema
- Update the top level module to depend on the template
- Update the schema and its version, and update the top level module to depend on the new version
Prerequisites
- A tool to edit text files. Any text editor you have will be fine, for example VSCode.
- A command terminal.
cueworks on all platforms, so any terminal on Linux or macOS, and on PowerShell,cmd.exeor WSL in Windows. - An installed
cuebinary (installation details) - Some awareness of CUE schemata (Constraints and Definitions in the CUE tour)
This tutorial is written using the following version of cmd/cue:
$ cue version
cue version v0.14.2
...Create the module for the schema code
In this tutorial we will focus on an imaginary application called FrostyApp,
which consumes its configuration in YAML format.
You will define the configuration in CUE and use a CUE schema to validate it.
We would like to be able to share the schema between several consumers.
Create a directory to hold the schema code:
$ mkdir frostyconfig
$ cd frostyconfigEach module described in this tutorial will live in a separate directory, which you will create as they are needed.
Initialize the directory as a git repository and a CUE module:
$ git init -q
$ cue mod init --source=git glacial-tech.example/frostyconfig@v0In order to publish the module to a registry, the code must hold a
cue.mod/module.cue file declaring its module path. This is the path prefix to
use when importing packages from within the module.
Module paths are fully domain-name qualified, and it is good practice to place the module under a domain or a GitHub repository that you control.
We will use a custom registry in this tutorial, which has fewer restrictions on the module paths that can be used. By contrast a central shared registry may require proof of control of a domain before allowing updates to a module in that domain.
In our example we will assume that
we control the domain name glacial-tech.example
and place all module paths under that.
There are some other constraints on the names
that can be used for a module, due to OCI restrictions.
The module name must contain only
lower-case ASCII letters, ASCII digits, dots (.), and dashes (-).
The
OCI distribution spec
contains full details of the naming restrictions.
The --source=git flag tells cue to use the same file-inclusion rules as
git, when publishing this module.
Create the configuration schema:
package frostyconfig
// #Config defines the schema for the FrostyApp configuration.
#Config: {
// appName defines the name of the application.
appName!: string
// port holds the port number the application listens on.
port!: int
// debug holds whether to enable debug mode.
debug?: bool
// features holds optional feature settings
features?: {
// logging enables or disables logging.
logging?: bool
// analytics enables or disables analytics.
analytics?: bool
}
}The details of the schema are not too important. For the purposes of this tutorial,
it represents the schema of the configuration data expected by FrostyApp.
Choose an OCI registry
If you do not have access to an OCI registry, start one locally:
$ cue mod registry localhost:5001cue mod registry is a very simple in-memory OCI server.
If this command fails with a message mentioning “address already in use”,
then some other program on your computer is already using port 5001.
To resolve this, select a different port number and re-run the command using the new value.
You will also need to update any commands that use port 5001 as you follow this guide.
CUE should work with all OCI-compatible artifact registries, such as the Google Artifact Registry, as CUE uses the standard OCI protocols spoken by such registries. For example, here are some alternatives:
# running a local registry via docker
$ docker run -p 5001:5000 registry
# running a local registry via podman
$ podman run -p 5001:5000 registryIn our example we will run a local instance of the in-memory registry on port 5001.
If you need to run one locally, invoke the above docker command in a separate
terminal so the registry remains running while you follow the rest of this
tutorial.
Publish the module
Set up a required environment variable:
$ export CUE_REGISTRY=localhost:5001/cuemodulesThe CUE_REGISTRY variable tells the cue command which
registry to use when fetching and pushing modules.
In our example the modules will be stored in the registry under the prefix cuemodules.
In practice you would want this prefix to be some place of your choice -
or you could leave the prefix empty if you plan to dedicate the registry
to holding CUE modules.
Ensure the module.cue file is tidy:
$ cue mod tidyThis command checks that modules for all imported packages
are present in the cue.mod/module.cue file and that their versions
are correct. It is good practice to run this before publishing
a module. So, although this module does not
have any dependencies, we will run cue mod tidy anyway.
Create a git commit:
$ git add -A
$ git commit -q -m 'Initial commit'Earlier, you initialized this module with --source=git, which told the cue
command that it should publish only those files that git knows about. The git
commit you just created leaves the directory in a “clean” state, which is
necessary for cue to know exactly which files to include in the published
module.
Publish the first version of this module:
$ cue mod publish v0.0.1
...This command uploads the module to the registry and publishes it
under version v0.0.1. It will be published to the module
path we chose in cue mod init earlier - all we need to do in this command
is to decide which version we will publish.
The version follows semver syntax,
and it is good practice to follow semantic version conventions, which include
maintaining compatability with earlier minor versions of the same module.
The major version under which it is published
must match the major version specified in the module file.
For example it would be an error to use v1.0.1 here
because the module name ends in @v0.
The module has now been published to the registry. If you are running a
registry locally then you might have seen some output in the docker terminal
while the registry received and stored the module.
Create a new frostyapp module that depends on the first module
Define the actual FrostyApp configuration, constrained by the schema you just
published.
Create a directory and initalize a git repository and a new CUE module within it:
$ mkdir ../frostyapp
$ cd ../frostyapp
$ git init -q
$ cue mod init --source=git glacial-tech.example/frostyapp@v0Create the code for the new module:
package frostyapp
import "glacial-tech.example/frostyconfig@v0"
config: frostyconfig.#Config & {
appName: "alpha"
port: 80
features: logging: true
}This imports the frostyconfig package from the first
module you published and
defines some concrete values for the configuration,
constrained by the frostyconfig.#Config schema.
Ensure the module is tidy, pulling all dependencies:
$ cue mod tidyWe can see that the dependencies have now been added to the
cue.mod/module.cue file:
$ cat cue.mod/module.cue
module: "glacial-tech.example/frostyapp@v0"
language: {
version: "v0.14.2"
}
source: {
kind: "git"
}
deps: {
"glacial-tech.example/frostyconfig@v0": {
v: "v0.0.1"
}
}Our dependencies currently look like this:
Evaluate the configuration
Export the configuration as YAML:
$ cue export --out yaml
config:
appName: alpha
port: 80
features:
logging: trueWe can use this new module code just like any other CUE code.
Publish a frostytemplate module
Suppose we want to define a module that encapsulates some
default values for FrostyApp. We could just publish it as part of the
frostyconfig original module, but publishing it as a separate module will
be useful to demonstrate how dependencies work. Having different modules like
this can also be a useful separation of concerns when a schema comes from some
other source of truth.
Create a directory and initalize a git repository and a new CUE module within it:
$ mkdir ../frostytemplate
$ cd ../frostytemplate
$ git init -q
$ cue mod init --source=git glacial-tech.example/frostytemplate@v0This defines another module. We have named it frostytemplate
because CUE uses the term “template” to mean code that
defines default values and derived data but is not intended to
be the final configuration.
Define the CUE template:
package frostytemplate
import "glacial-tech.example/frostyconfig@v0"
// Config defines a set of default values for frostyconfig.#Config.
Config: frostyconfig.#Config & {
port: *80 | _
debug: *false | _
features: {
logging: *true | _
analytics: *true | _
}
}We import the schema to constrain the default values, just as we did with the
frostyapp module.
Tidy the module and create a git commit:
$ cue mod tidy
$ git add -A
$ git commit -q -m 'Initial commit'Publish the frostytemplate module:
$ cue mod publish v0.0.1
...
Update the frostyapp module
Update the frostyapp module to make use of this new template
module:
$ cd ../frostyapppackage frostyapp
import "glacial-tech.example/frostytemplate@v0"
config: frostytemplate.Config & {
appName: "alpha"
}The frostyapp module now gains the benefit of the new defaults. We can remove
some fields because they are now provided by the template, satisfying the
requirements of the configuration.
Resolve dependencies in frostyapp:
$ cue mod tidyRe-running cue mod tidy updates the dependencies in frostyapp to
use frostytemplate as well as frostyconfig.
Here is what the cue.mod/module.cue file now looks like:
$ cat cue.mod/module.cue
module: "glacial-tech.example/frostyapp@v0"
language: {
version: "v0.14.2"
}
source: {
kind: "git"
}
deps: {
"glacial-tech.example/frostyconfig@v0": {
v: "v0.0.1"
}
"glacial-tech.example/frostytemplate@v0": {
v: "v0.0.1"
}
}Re-render the configuration as YAML:
$ cue export --out yaml
config:
appName: alpha
port: 80
debug: false
features:
logging: true
analytics: trueWe can see that the values in the configuration reflect the new default values.
Add a new field to the schema
Suppose that FrostyApp has gained the ability to limit the amount of
concurrency it uses, configured with a new maxConcurrency field.
We will add that field to the schema and update the app to use it.
Update the schema to add a new maxConcurrency field:
$ cd ../frostyconfigpackage frostyconfig
// #Config defines the schema for the FrostyApp configuration.
#Config: {
// appName defines the name of the application.
appName!: string
// port holds the port number the application listens on.
port!: int
// debug holds whether to enable debug mode.
debug?: bool
// maxConcurrency specifies the maximum amount of
// concurrent requests to process concurrently.
maxConcurrency?: int & >=1
// features holds optional feature settings
features?: {
// logging enables or disables logging.
logging?: bool
// analytics enables or disables analytics.
analytics?: bool
}
}The schema is unchanged except for the new maxConcurrency field.
Tidy the module and create a git commit:
$ cue mod tidy
$ git add -A
$ git commit -q -m 'Second commit'Upload a new version of the frostyconfig schema:
$ cue mod publish v0.1.0
...We incremented the minor version to signify that a backwardly compatible feature has been added.
Update the frostyapp module to use the new schema version
Use the new version of glacial-tech.example/frostyconfig@v0:
$ cd ../frostyapp$ cue mod get glacial-tech.example/frostyconfig@v0.1.0CUE modules “lock in” the versions of any dependencies, storing
their versions in cue.mod/module.cue file. This gives predictability
and dependability but does mean that our frostyapp application
will not use the new schema version until it is explicitly updated to do so.
module.cue file manually, but in the
future the cue command will be able to perform this kind of update.Check that everything still works and that your configuration is still valid:
$ cue mod tidy
$ cue export --out yaml
config:
appName: alpha
port: 80
debug: false
features:
logging: true
analytics: trueSo exactly what happened above?
Recall that the glacial-tech.example/frostytemplate module remains unchanged:
its module still depends on the original v0.0.1 version of the schema. By
changing the version at the top level (frostyapp), you caused the new version
to be used.
In general, we will end up with the the most recent version of all the major versions mentioned in all dependencies. Put another way, there can be several different major versions of a given module, but only one minor version. This is the MVS algorithm used by CUE’s dependency resolution.
Related content
- Tutorial: Working with modules and the Central Registry
- Tutorial: Publishing modules to the Central Registry
- Reference: CUE Modules