Clarive
Clarive Release 7.22 is now available. Read the release notes or download it now.

Servers, Variables and Permissions in HCL

Resources, project variables, credentials and roles in Clarive 7.22 can be kept as text, and a change to who may deploy to production shows up as a diff.

The last two posts in this series were about rules. Most of a Clarive installation is something else. It is servers and agents, repositories and projects, the variables a deploy reads on each environment and the roles that decide who is allowed to run it. In Clarive 7.22 all of that can be written as HCL, then exported and imported with the commands covered earlier in the series.

Servers and what they point at

Resources are by far the largest family. Clarive ships with 66 resource classes, and any class created in the interface is a resource too. This example from the documentation has four of them:

HCL
generic_server "web01" {
  hostname        = "web01.example.invalid"
  description     = "Front office box"
  os              = "win"
  connect_timeout = 60
  proxy           = generic_server.bastion
}

clax_agent "web01-agent" {
  basic_auth_enabled  = true
  basic_auth_username = "foo"
  port                = 11801
  server              = generic_server.web01
  basic_auth_password = b64("aHVudGVyMg==")
}

resource "GitRepository" "foo-core" {
  name           = "Foo Core"
  default_branch = "main"
  repo_dir       = "/srv/git/foo-core.git"
}

resource "AcmeCustomThing" "bar" {
  colour = "blue"
  size   = 3
}

Thirty-seven classes have a keyword of their own, like generic_server and clax_agent. Every other resource class is written as resource followed by the class name. That includes a class made in the interface with no code behind it, like AcmeCustomThing above, which accepts whatever attributes it is given.

The agent names the server it runs on with server = generic_server.web01, and the server names its proxy the same way. Across the shipped classes, 131 attributes hold an address instead of a value. On import each one is looked up on the installation receiving the file. A list works as well, so a cluster can name its instances as [web_instance.foo-1, web_instance.foo-2].

A stored server holds every attribute its class declares, most of them at their defaults. The file leaves those out. The generic_server class stores 24 attributes, and web01 above writes five. The architecture, the remote Perl path and the rest are still stored, at values nobody changed. connect_timeout = 60 is written because the default is 30. On import the missing attributes are filled back in from the same defaults, so the short block and a long one describe the same server.

Variables per environment

A project keeps the variables from its Variables tab under variables, one set per environment. The set under "*" applies to all of them:

HCL
project "foo" {
  variables = {
    "*"   = {
      db_host   = "foo-db"
      db_pass   = b64("aHVudGVyMg==")
      "foo.bar" = "baz"
    }
    PROD  = {
      app_server = generic_server.foo-server
      _natures   = {
        "nature.java" = { heap = "2g" }
      }
    }
    COMUN = {}
  }
}

Variables are written exactly as stored. A value of "1.10" stays a string and isn't turned into a number, and an environment with nothing set is still listed. A variable of type ci holds a resource, and its value is written as that resource's address, so app_server above reads back on PROD as whichever server is called foo-server there. Nature overrides sit under _natures, filed by the nature's address.

A variable of type password is a secret in every environment it appears in. It is exported in the same modes as any other credential, which the next section covers, except that --secret omit writes it as null instead of leaving it out. Written as null, it keeps whatever value the target already stores.

One thing to keep in mind when editing by hand is that the set of variables is saved whole, the same as a save from the Variables tab. A variable or an environment left out of variables is removed from the project on import. The plan shows it before anything is written.

Four ways to write a secret

Credentials are the part of a configuration that most needs care in a file. cla export --secret has four modes, shown here as the documentation shows them:

HCL
# --secret b64 (the default)
password = b64("aHVudGVyMg==")

# --secret plain
password = "hunter2"

# --secret encrypt --secret-key KEY
password = enc("U2FsdGVkX1...")

# --secret omit
# the attribute is not written at all

The default is b64, and base64 is an encoding. It is not encryption. In the documentation's words, it "keeps a password out of plain sight in a file someone might paste into a ticket, and nothing more." Anyone who has the file can read the password. For files that go into Git we recommend omit or encrypt.

omit suits most moves between installations, because TEST credentials are rarely PROD credentials. A credential missing from a file keeps its stored value on import, and so does one written as null. There is no way to blank a stored secret from a file. An export with --secret omit can carry every server's settings to PROD while PROD keeps its own passwords, and the documentation's guide to a first migration recommends it for that reason.

encrypt protects the values in the file itself. The export takes a --secret-key, and cla import, cla import-plan and cla diff need the same one to read the file. Without it, or with the wrong one, the plan is refused and nothing is imported.

User passwords are a separate case. They are never exported, in any mode. An import that finds a password or api_key on a user block ignores it with a warning and writes the rest of the user.

Roles as grants

A role is a list of grant blocks, one permission each:

HCL
role "deploy-manager" {
  name        = "Deploy Manager"
  description = "Runs deployments"
  mailbox     = "deploys@example.invalid"

  grant {
    action = "action.job.create"
    bounds {
      bl = "PROD"
    }
  }

  grant {
    action = "action.job.delete"
    deny   = true
  }
}

The first grant lets a deploy manager create jobs, limited to PROD. The second is an explicit denial. A grant with no bounds applies everywhere.

A bounds block is one scope. Two of them under the same grant mean either scope, so this role may roll back jobs on PROD or on PREP:

HCL
role "operador" {
  grant {
    action = "action.job.rollback"
    bounds {
      bl = "PROD"
    }
    bounds {
      bl = "PREP"
    }
  }

  grant {
    action = "action.dashboards.view"
  }
}

Bounds that name objects use addresses, like project.foo or status.nuevo, so they mean the same project and the same status on the installation receiving them. The environment in bl is a plain name.

Sometimes a stored bound holds the id of something that no longer exists, and there is no address to write for it. The export errs toward less access. A scope it can't write in full is left out whole, so the grant loses that scope instead of gaining a wider one, and the export reports it. When none of a grant's scopes can be written, the grant is dropped altogether, because a grant with no bounds would allow everything. That counts as an error. The role's block gets a comment saying what was dropped and why, everything else is still written and the command exits with 2. With --pedantic, an export with any error writes nothing.

Users and groups

A user is written in one of two ways. A user who belongs to groups gets a groups list and takes every permission from them. A user in no group carries grants of their own, each naming a role, a kind of scope and what it applies to, which in this example is two projects:

HCL
user "foo" {
  email  = "foo@example.invalid"
  groups = [group.auditors, group.developers]
}

user "bar" {
  grant {
    id_role = role.developer
    type    = "project"
    mid     = [project.acme, project.baz]
  }
}

A group lists the roles it hands its members. It doesn't list the members. Membership is written on each user and only there, so a set of files can never hold two versions of the same fact that disagree. When an import changes a group, every member's permissions are recomputed, as the group administration screen does it.

Permissions in review

In Git, a role is a short block in role.hcl. Letting deploy managers create jobs on one more environment is a second bounds block of three lines, and it reaches production through the same pull request and plan as any other change. A role that hasn't changed plans as unchanged, so the only roles in a plan are the ones someone edited. Before that, cla diff --filter 'role.*' shows whether PROD's roles still match the files, which is the first thing anyone auditing access wants to know.

The attribute tables for every resource class are in the resources reference, and roles, users and groups are in their own page. If you'd like help deciding how to handle credentials in your own export, get in touch.

We use cookies to improve your experience. By continuing to visit this site you agree to our use of cookies. Learn more