Overview

CUE heavily relies on its order independence for package organization. Definitions and constraints can be split across files within a package, and even organized across directories.

Another key aspect of CUE’s package management is reproducibility. A module, the largest unit of organization, has a fixed location of all files and dependencies. There are no paths to configure. With configuration, reproducibility is key.

Within a module, CUE organizes files grouped by package. A package can be defined within the module or externally. In the latter case, CUE maintains a copy of the package within the module in a dedicated location.

Modules

A module contains a configuration laid out in a directory hierarchy. It contains everything that is needed to deterministically calculate the outcome of a CUE configuration. The root of this directory is marked by containing a cue.mod directory. The contents of this directory are mostly managed by the cue tool. In that sense, cue.mod is analogous to the .git directory marking the root directory of a repo, but where its contents are mostly managed by the git tool.

The use of a module is optional, but required if one wants to import files.

Creating a module

A module can be created by running the following command within the module root:

TERMINAL
$ cue mod init

A module path may be specified, but if it is omitted then a default value is used (currently cue.example). The module path is specified as an argument to the command:

TERMINAL
$ cue mod init some.module.prefix/with/optional/path/components@v0

The module path must be used in CUE code if a package within the module needs to import another package defined within the module. Module paths start with a fully-qualified domain name, continue with optional forward-slash-separated path components, and end with an optional major version suffix. If the optional major version suffix is omitted then the resulting module is treated as if the major version suffix @v0 were present. Ideally, the domain name and path components in a module path would map to a resource that’s controlled by the CUE user (e.g. a website, a version control repository, or similar), but this is not looked up or enforced by the cue command.

A module can also be created by setting up the cue.mod directory and module.cue file manually. This is not recommended.

The cue.mod directory

The module directory has the following contents:

cue.mod
|-- module.cue  // The module file
|-- pkg         // copies of external packages [DEPRECATED]
|-- gen         // files generated from external sources [DEPRECATED]
|-- usr         // user-defined constraints [DEPRECATED]

This directory is predominantly managed by the cue tool.

The module.cue file defines settings such as the module path, which allows packages defined within the module to be imported from within the module itself. It also holds version information of imported packages to determine the precise origin of imported files. The modules reference documentation fully specifies the contents of module.cue.

Deprecated directories

The pkg, gen and usr directories inside the cue.mod directory can hold packages that are facsimiles, derivatives, or augmentations of external packages. Use of these directories is deprecated as they interact poorly with shared dependencies (those managed by the cue mod command), and are not accessible if their containing module is used as a dependency.

(To demonstrate this inaccessibility, consider the example situation where module A depends on module B; module B depends on module C; and module C is stored in module B’s pkg directory. Because module B uses this deprecated method for storing its dependency, module A cannot use any packages from module B’s copy of module C without independently declaring its own dependency on module C through cue mod, or its own pkg/usr/gen directories. If module B had instead declared the dependency using the cue mod command and the module.cue file, then module C would be accessible to module A through the shared module cache.)

If a dependency is managed by the cue mod command but is also found in any of the pkg/gen/usr directories then an error occurs. These directories are still supported as they can be useful in a limited set of circumstances, but only when CUE’s current modules can’t handle a particular use-case. They are intended to be used for the following:

  • pkg: manually managed and imported external packages
  • gen: CUE generated from external definitions, such as protobuf or Go
  • usr: user-defined constraints for external packages

These three directories split files from the same package across different parallel hierarchies based on the origin of the content. But for all intent and purposes they should be seen as a single directory hierarchy.

The usr directory is a bit special here. The gen directory is populated by the cue tool and the pkg directory, whilst deprecated, can hold 3rd-party or external constraints. The usr directory, on the other hand, holds user-defined constraints for the packages defined in the other directories.

User-defined constraints can be used to fill gaps in generated constraints; as generation is not always a sure thing. They can also be used to enforce constraints on imported packages, for instance to enforce that a certain API feature is still provided or of the desired form. The usr directory allows for a cleaner organization compared to storing such user-defined constraints directly in the cue-managed directories.

Packages

Files belonging to a package

CUE files may define a package name at the top of their file. CUE uses this to determine which files belong together. If the cue tool is told to load the files for a specific directory, for instance:

cue eval ./mypkg

it will only look files with such a clause and ignore files without it.

If the package name within the directory is not unique, cue needs to know the name of the package as well

cue eval ./mypkg:packageName

If no module is defined then the cue command will only load the files in the current directory. If a module is defined then it will also load all files with the same package name in its ancestor directories up to the module root. As we will see below, this strategy allows for defining organization-wide schemas and policies.

Import path

Each package is identified by a globally unique name, called its import path. The import path consists of a unique location identifier followed by a colon (:) and its package name.

k8s.io/api/core/v1:v1

If the basename of the path and the package name are the same, the latter can be omitted.

k8s.io/api/core/v1

The unique location identifier consists of a domain name followed by a path.

Modules themselves also have a unique location identifier. A package inside a module can import another package from this same module by using the following import path:

<module identifier>/<relative position of package within module>:<package name>

So suppose our module is identified as example.com/transport and a package located at schemas/trains and has the package name track, then other packages can import this packages as:

import "example.com/transport/schemas/trains:track"

Putting it all together:

root                    // must contain:
|-- cue.mod
|   |-- module.cue      // module: "example.com/transport"
|-- schemas
|   |-- trains
|   |   |-- track.cue   // package track
...
|-- data.cue            // import "example.com/transport/schemas/trains:track"

The relative position may not be within the cue.mod directory.

Location on disk

CUE code can import a package by specifying its import path with the import statement. For instance,

import (
	"list"
	"example.com/path/to/package"
)

Packages for which the first path component is not a fully qualified domain name are builtin packages and are not stored on disk, as with list in the example above. Any CUE code can import builtin packages, whether part of a module or not.

In contrast, CUE code can only import non-builtin packages when that CUE code is part of a module. When a non-builtin package is imported, CUE determines its location on disk using several mechanisms in parallel:

  • If the module path is a prefix of the import path, then that prefix is removed from the import path and the remainder of the import path is treated as a directory path. If a package exists in that directory, relative to the module root, then the package is located in that directory.
  • If a dependency of the current module declares a module path that has the current module path as a prefix then:
    • the dependency is fetched from the configured module registry if it’s not already present in the shared module cache;
    • taking the dependency’s module path into account, the directory inside the dependency that matches the import path is examined;
    • if that directory contains a package then the package is located in that directory.
  • The package contents are looked up in the deprecated cue.mod/pkg, cue.mod/gen, and cue.mod/usr directories. If a package is found in any of these deprecated directories then the package content is unified from the relevant files across all of these directories.

If only one of these three mechanisms discovers an appropriate directory location then that directory location (and its package content) is used.

If more than one of these mechanisms discovers an appropriate directory location then an error occurs. If none of these mechanisms discovers an appropriate directory location then an error occurs.

Builtin Packages

CUE has a standard library of builtin packages that are compiled into the cue command.

A list of these packages can be found at pkg.go.dev/cuelang/go/pkg. The intention is to have this documentation in CUE format, but for now we are piggybacking on the Go infrastructure to host and present the CUE standard library documentatation.

To use a builtin package, import its path relative to cuelang.org/go/pkg and invoke the functions using its qualified identifier. For instance:

stdlib.cue
package example

import "strings"

A: "Hello, world!"
B: strings.ToUpper(A)
C: strings.HasPrefix(B, "HELLO")
TERMINAL
$ cue eval
A: "Hello, world!"
B: "HELLO, WORLD!"
C: true

File Organization

Instances

Within a module, all .cue files with the same package name are part of the same package. A package is evaluated within the context of a certain directory. Within this context, only the files belonging to that package in that directory and its ancestor directories within the module are combined. We call that an instance of a package.

Using this approach, the different kind of directories within a module can be ascribed the following roles:

  • module root: schema
  • medial directories: policy
  • leaf directories: data

The top of the hierarchy (the module root) defines constraints that apply across the organization. Leaf directories typically define concrete instances, inheriting all the constraints of ancestor directories. Directories between the leaf and top directory define constraints, like policies, that only apply to its subdirectories.

Because order of evaluation does not matter in CUE, leaf packages do not explicitly have to specify which parts of their parents they want to inherit from. Instead, parent directories can be seen to “push out” constraints to their subdirectories. In other words, parent directories define policies to which subdirectories must comply.