Skip to content

Repository files navigation

[[TOC]]

Description

Ligoj CLI makes REST calls to a remote Ligoj instance, with parameters and error handling.

Requirements

  • Python 3.11+
  • Connectivity and API keys to the target endpoints: Ligoj (required) and, for bootstrap actions, Nexus, Jenkins, SonarQube
  • Valid credentials

Installation

Ligoj CLI is published on PyPI as ligoj-cli and provides the ligoj command.

Install it as an isolated tool with uv (recommended):

uv tool install ligoj-cli
ligoj --version

Alternatively, use pipx or pip:

pipx install ligoj-cli
# or
pip install ligoj-cli

To try the latest pre-release published on TestPyPI:

pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ ligoj-cli

For local development from a checkout, see Development.

Updating

The CLI updates itself through uv:

ligoj update # upgrade to the latest release (uv tool upgrade)
ligoj update --target 1.3.0 # pin a specific version (uv tool install --force)

When the CLI was installed with pipx/pip instead of uv, update it with the matching command (pipx upgrade ligoj-cli / pip install -U ligoj-cli) — ligoj update reminds you of these when uv is not available.

Configuration

Credentials

Ligoj credentials are based on user/password or user/API Key.

Populate the configuration files with the API key created here #/api/token ("?" > "Api" > "Token")

You can also:

  • use the session login command to get a temporary session
  • use the token command to create durable API keys

For standard actions, only LIGOJ_ENDPOINT is required and can be set either in the configuration files, as an environment variable, or as a CLI option.

For bootstrap actions, more endpoints and credentials may be required in the configuration files.

Sample usage:

ligoj \
--api-user="ligoj-admin" \
--api-key="..." \
--endpoint="http://localhost:8080/ligoj" \
--version

Commands

Settings

Options are sourced in the following order of priority, from highest to lowest:

  1. Command line options – Overrides settings in any other location, such as the --output and --profile parameters.
  2. Environment variables – You can store values in your system's environment variables.
  3. Session file – default section or given profile name. The session file is located at ~/.ligoj/sessions on Linux or macOS and holds:
  • Session cookies set with the login command.
  • API keys set with the login command.
  1. Credentials file – default section or given profile name. The credentials file is located at ~/.ligoj/credentials on Linux or macOS.
  2. Configuration file – default section or given profile name. The config file is located at ~/.ligoj/config on Linux or macOS. Alternative file ~/.ligoj/cli-config is supported.

Configuration files

Sections in these .ini files correspond to profile names. When neither the --profile option nor the LIGOJ_PROFILE environment variable is provided, the default profile is default — except for the dev command, which defaults to the dev profile. See Profile for the full resolution order.

In the file ~/.ligoj/config, default configurations can be specified. No secrets are sourced from this file.

[default]output = "json"log_level = "DEBUG"endpoint=http://localhost:8080/ligoj
jenkins_endpoint=http://localhost:8086
sonar_endpoint=http://localhost:9000/
[some]output = "json"

Read-only secrets are stored in the ~/.ligoj/credentials file.

Temporary secrets (stored from the session command) are stored in the ~/.ligoj/sessions file.

While ~/.ligoj/credentials can contain configuration settings, secrets are only sourced from the ~/.ligoj/credentials and ~/.ligoj/sessions files.

Sample credential file:

[default]api_user = ligoj-user1
api_key = secret
jenkins_api_user = admin
jenkins_api_token = secret
sonar_api_token = secret

Note Leading spaces are ignored, and enclosing ' and " are removed. Empty strings are ignored.

Generic options

The generic options are available to all actions.

Output mode

Determines the output mode of the command. Use the --output option. The following modes are available:

  • json : JSON format
  • text : Text format

This option can also be specified in configuration files as output or in environment variable LIGOJ_OUTPUT

ligoj --output json --version
ligoj --version
{"version": "3.3.1-SNAPSHOT"}
ligoj --output text --version
3.3.1-SNAPSHOT

Log level

To configure the verbosity, use the --log-level option. The following levels are available:

  • TRACE level displays the in/out data. --verbose and --trace are shortcuts for this level.
  • DEBUG level displays the internal API calls --debug is a shortcut for this level.
  • INFO level displays the actions
  • WARN level displays the unexpected behaviors
  • ERROR level displays only fatal errors

This option can also be specified in configuration files as log-level or in environment variable LIGOJ_LOG_LEVEL

ligoj --log-level INFO ....
ligoj --verbose ....
ligoj --trace ....

If you want to pipe JSON result to jq, use --output json and --log-level ERROR options.

Insecure server connections

To allow insecure server connections when using SSL, use the --insecure option. This option can also be specified in configuration files as insecure or in environment variable LIGOJ_INSECURE

ligoj --insecure ....
ligoj --k ....

API user

Ligoj API user name. Use the --api-user option. This option can also be specified in configuration files as api-user or in environment variable LIGOJ_API_USER. By default is ligoj-admin.

ligoj --api-user ligoj-admin ....

API key

Provide an API key, which can be created here #/api/token ("?" > "Api" > "Token"). Use the --api-key option. This option can also be specified in configuration files as api-key or in environment variable LIGOJ_API_KEY

ligoj --api-key secret ....

API run as user

Ligoj API user name for impersonation. Use the --api-run-as-user option. This option can also be specified in configuration files as api-run-as-user or in environment variable LIGOJ_API_RUN_AS_USER.

Constraints are:

  • After the authentication succeeds with --api-key and --api-user
  • The current user must have POST /system.user authorization
  • --api-run-as-user must exist
  • The actions are executed in the name of --api-run-as-user and without needing the related credentials.

This option can also be specified in configuration files as api-run-as-user or in environment variable LIGOJ_API_RUN_AS_USER.

ligoj --api-run-as-user ligoj-user ....

API local roles

Restrict the computed roles to the local roles of the authenticated user. No plugin roles are involved. This flag makes the authentication independent of the configured plugins (e.g., availability, misconfiguration, etc.).

Since this flag reduces the set of available roles, there is no restriction on the usage.

This option can also be specified in configuration files as api-local-roles or in environment variable LIGOJ_API_LOCAL_ROLES.

ligoj --api-local-roles session get

Profile

A profile is a named section shared across the three .ini files under ~/.ligoj/ (config, credentials, sessions): the settings, secrets, and session for a given profile all live under the same [<profile>] section. Profiles let you keep several isolated sets of endpoints and credentials — for example one per Ligoj instance, or one for the local dev environment — and switch between them without editing files.

Select the profile with the --profile option or the LIGOJ_PROFILE environment variable:

ligoj --profile some node list
# or
LIGOJ_PROFILE=some ligoj node list

The profile name is resolved in the following order (first match wins):

  1. The --profile command-line option.
  2. The LIGOJ_PROFILE environment variable.
  3. The default profile, which depends on the command:
    • default for every command,
    • dev for the dev command — its local development stack reads and writes the [dev] section populated by dev init.

For example, ligoj dev demo uses the [dev] profile automatically, while ligoj node list uses [default]; either can still be overridden with --profile or LIGOJ_PROFILE.

From

JSON content to load. Use the --from option. The following forms are available:

  • Path to a local JSON file
  • Remote HTTP URL
  • Inline JSON string

After the content has been retrieved, it is interpolated with Jinja with current project (project) and environment variables (env) as context:

  • For example:
    • {{ project.id }} is replaced by the project identifier.
    • {{ env.ENV_VAR }} is replaced by the ENV_VAR environment variable value.
    • $${_not_existing_property_in_context_} is replaced by an empty string.
  • null values are considered as empty string
  • The context depends on the current action. Usually, all given parameters are added to the context.
  • The context is completed with environment variables.
  • Surrounding spaces inside {{..}} are ignored

No color

Disable colors in messages. Use the --no-color option. This option can also be specified in configuration files as no-color or in environment variable LIGOJ_NO_COLOR

ligoj --no-color ....

Fail on hook error

Fail (exit code 1) when any hook returns a failure status (X-Ligoj-Hook-*=FAILED). See hooks for more details. Use the --fail-on-hook-error option. This option can also be specified in configuration files as fail-on-hook-error or in environment variable LIGOJ_FAIL_ON_HOOK_ERROR

ligoj --fail-on-hook-error ....

Fail (exit code 1) when any hook returns a failure status (X-Ligoj-Hook-*=FAILED). See hooks for more details.

Hooks status and message are displayed with DEBUG log level:

[DEBUG] [ligoj] Hook 'audit_role_change' status: SUCCEED
[DEBUG] [ligoj] Hook 'audit_role_change' status: FAILED: Message for user

Session

Session operations with credentials and profile management.

Login with password

Verify the provided user and password and save the returned session cookie into the ~/.ligoj/sessions file for further API call without providing credentials.

Note Secrets like api-user and password are sourced from the CLI options, sessions and credential files, not from the configuration one.

ligoj --api-user "ligoj-admin" --profile default session login --password secret
ligoj --api-user "ligoj-admin" login session --password secret

Completed ~/.ligoj/sessions file:

[default]session = session_secret_value.node0
api_user = ligoj-admin

Login with api key

Verify the provided user and API key and save the provided API user and API keys into the ~/.ligoj/sessions file for further API call without providing credentials.

Note Secrets like api-user and password are sourced from the CLI options, sessions and credential files, not from the configuration one.

ligoj --api-user "ligoj-admin" --api-key "__api_key__" session login

Get session

Return user session details

ligoj session get
{
"applicationSettings": {
"buildNumber": "", "buildTimestamp": "", "buildVersion": "3.3.1-SNAPSHOT", "digestVersion": "rRXmeWPgn+...==",
"plugins": ["feature:welcome:data-rbac"]
}, "userSettings": {"security-agreement": "1"}, "uiAuthorizations": ["^id/container/group.*", "^id/user.*", "^id/delegate.*", "^message.*", "^id$", "^home.*", "^id/container/company.*", "^api.*", "^id/home.*", ".*"], "apiAuthorizations": [{"pattern": ".*", "method": "DELETE"}], "roles": ["ADMIN", "USER"], "userName": "ligoj-admin"
}

whoami

Return user identifier

ligoj session whoami
{"id": "ligoj-admin"}

System User

A system user can live without federated identity. After a successful login, a federated user can be managed with system roles and API keys.

Create a system user

Create a new system user with role names or identifiers.

ligoj user upsert --id ligoj-admin@sample.com --roles USER ADMIN

Optionally, at this time, an API key is generated but only once and only from administrator users.

ligoj user upsert --id ligoj-admin@sample.com --roles USER ADMIN --api_key_name cli

Output the API key only if not existing.

{"id": "__api_key__", "name": "cli"}

Note When role names are provided API calls are executed to retrieve their identifiers.

Delete a system user

Delete a system user. The command will not fail if the user is not found.

ligoj user delete --id ligoj-user

List system users

ligoj user list
ligoj user list --with-roles
{"recordsTotal": 3, "recordsFiltered": 3, "data": ["ligoj-admin", "ligoj-admin@sample.com", "ligoj-user"]}

Note When role names are provided API calls are executed to retrieve their identifiers.

Delete a system user

Create a new system user with role names or identifiers.

ligoj user delete --id ligoj-user

System Role

A system role holds the permissions (ui and api), and can be assigned to users or groups.

Create a system role

Create or update a system role.

Note Whereas Ligoj supports per HTTP method authorizations, this feature is not yet available from this CLI action.

# Deprecated `--id` option
ligoj role create --id ADMIN --api ".*" --ui ".*"
ligoj role create --name ADMIN --api ".*" --ui ".*"
ligoj role create --name SELF_TOKEN_RENEW --api "/api/token.*" --ui "/sys/token"

Output is the created/existing role identifier.

123

Delete a system role

Delete a system role. The command will not fail if the role does not exist.

ligoj role delete --id 1
ligoj role delete --name ADMIN

List system roles

ligoj role list

Get a system role

ligoj role get --id 1
ligoj role get --name ADMIN
{"id": 1, "createdBy": "_system", "createdDate": 1758279349911, "lastModifiedBy": "_system", "lastModifiedDate": 1758279349911, "name": "ADMIN"}

Info

Status

Return API server status

ligoj info status
{"status": "UP"}

Optionally, a wait for status can be defined. A regular poll to the server status is performed until reaching DOWN or UP status. When different from 0, the final status is returned.

ligoj info status --wait 20
{"status": "DOWN"}

Version

Return API server version

ligoj info version
ligoj --version
ligoj -v
{"version": "3.3.1-SNAPSHOT"}

API Specification

All Ligoj APIs are accessible with REST verbs.

Currently 3 specification formats are available:

  • Swagger : Web UI based on OpenAPI JSON file
  • OpenAPI JSON file
  • WADL
ligoj info api --output openapi --print content
ligoj info api
{
"openapi" : "3.0.1",
"info" : {
"title" : "Ligoj API application",
"description" : "REST API services of application. Includes the core services and the features of actually loaded plugins",
"contact" : {
"name" : "The Ligoj team",
"url" : "https://github.com/ligoj"
ligoj info api --output wadl --print url
ligoj info api --output openapi --print url
ligoj info api --output swagger --print url
http://localhost:8080/ligoj/rest?_wadl
http://localhost:8080/ligoj/rest/openapi.json
http://localhost:8080/ligoj/api-docs?url=openapi.json

Token

Manage API keys of current user.

Create a token

For expiration option:

  • Either a full ISO date, which corresponds to the furthest date the generated token can be trusted.
  • Either a duration starting from now, in a standard duration format. See pytimeparse

Expired tokens are neither listed nor returned even if they are not yet physically deleted.

ligoj token create --id cli_init
ligoj token create --id today_only --expiration 1d
ligoj token create --id SELF_TOKEN_RENEW --expiration 2029-12-31T23:59:59

Optionally, the created token can be saved into the current profile, replacing any previously existing one:

Output:

{"id": "__api_key__", "name": "cli_init"}

List tokens

ligoj token list
["cli_init", "test"]

Get a token value

ligoj token get --id cli_init
```json{"value": "__api_key__"}

Delete a token value

ligoj token delete --id cli_init

Version

Return API server version

ligoj info version
ligoj --version
ligoj -v
{"version": "3.3.1-SNAPSHOT"}

Configuration

Configure global values

Set value

ligoj configuration set --id "foo" --value "bar"
ligoj configuration set --id "plugins.repository-manager.nexus.search.url" --value "https://localhost/?g:org.ligoj.plugin"
ligoj configuration set --id "plugins.repository-manager.nexus.search.proxy.host" --value "my-proxy.local"
ligoj configuration set --id "plugins.repository-manager.nexus.search.proxy.port" --value "8080"
ligoj configuration set --id "plugins.repository-manager.nexus.artifact.url" --value "https://nexus.localhost:8443/repository/maven_corporate/org/ligoj/plugin/"
ligoj configuration set --id "cache.id-ldap-data.ttl" --value "3600"

Note When you change a configuration related to plugin management, invalidate the related caches to retrieve the up-to-date plugin versions.

Get value

Return the configuration value from its id, which can be a Java property name or a stored value in the S_CONFIGURATION table.

Return a specific value. Encrypted values are returned as decrypted.

ligoj configuration get --id "foo"
{"value": "bar"}

Return all values. Encrypted values are not returned.

ligoj configuration get
[{"name": "COMMAND_MODE", "value": "unix2003", "persisted": false, "secured": false, "overridden": false, "source": "systemEnvironment"},...]

Delete value

ligoj configuration delete --id "foo"

Cache

Invalidate cache

ligoj cache invalidate --id "node-parameters"
ligoj cache invalidate --id "nodes"
ligoj cache invalidate --id "iam-node-configuration"
ligoj cache invalidate --id "id-ldap-data"
ligoj cache invalidate --id "user-details"
ligoj cache invalidate --id "plugins-last-version-nexus"
ligoj cache invalidate

Get cache details

ligoj cache get --id "user-details"
{
"id": "user-details", "size": 1, "hitCount": 123, "missCount": 14, "hitPercentage": 89.78102, "missPercentage": 10.218978, "averageGetTime": 16.860365, "node": {"id": "00000000-0000-0000-0000-000000000000", "address": "[127.0.0.1]:5701", "version": "5.3.2", "cluster": {"id": "00000000-0000-0000-0000-000000000001", "state": "ACTIVE", "members": [{"id": "00000000-0000-0000-0000-000000000000", "address": "[127.0.0.1]:5701", "version": "5.3.2"}]}}
}
ligoj cache list
[
{"id": "terraform-version", "size": 0, "hitCount": 0, "missCount": 0, "hitPercentage": 0.0, "missPercentage": 0.0, "averageGetTime": 0.0, "node": {"id": "00000000-0000-0000-0000-000000000000", "address": "[127.0.0.1]:5701", "version": "5.3.2", "cluster": {"id": "00000000-0000-0000-0000-000000000001", "state": "ACTIVE", "members": [{"id": "00000000-0000-0000-0000-000000000000", "address": "[127.0.0.1]:5701", "version": "5.3.2"}]}}},
...
]

File

A file is a remote file readable and/or writable by the API container.

Related path must be authorized by the configuration value ligoj.file.path. This check is performed at upload and download times.

ligoj configuration set --id "ligoj.file.path" --value "^/home/files/.*,^/home/hooks/.*,^/home/ligoj/META-INF/resources/webjars/.*,^/home/ligoj/statics/.*"

Create or update file

Upload a local file to a remote file.

# Served by UI container, this file will be available from the URL: '/some/icon.png'
ligoj file put --from https://path/to/icon.png --path "/home/ligoj/statics/some/icon.png"# Served by API container, this file will be available from the URL: '/home/img/logo.svg'
ligoj file put --from docs/ui/logo.svg --path "/home/ligoj/META-INF/resources/webjars/home/img/logo.svg"

Delete file

ligoj file delete --path "/home/ligoj/icon.png"

Hook get

Download a remote file and save it to a local file.

ligoj file get --path "/home/ligoj/icon.png" --out "./icon2.png"

Hook

A hook is a command uploaded by a user, and triggered by a successfully invoked API call of Ligoj.

When this command is executed, it receives a PAYLOAD event as environment variable.

Related command must be authorized by the configuration value ligoj.hook.path. This check is performed at creation and execution time:

ligoj configuration set --id "ligoj.hook.path" --value "^/home/ligoj/hooks/.*"
ligoj configuration set --id "ligoj.file.path" --value "^/home/ligoj/hooks/.*"
ligoj file put --from docs/sample_hook_ligoj_audit.sh --path "/home/ligoj/hooks/ligoj_audit.sh" --executable

Payload structure:

{
"name":"audit_role_change",
"now":"2023-10-17T19:22:21Z",
"result":{"some_json":"some_value"},
"path":"system/security/role",
"api":"RoleResource#update",
"params":[{"id":2652, "name":"ADMIN_PROJECT1A"}, {}],
"method":"PUT",
"user":"ligoj-admin",
"timeout":30,
"inject": {"secret1":"value1","secret2":"value2"}
}'

Create hook

# Asynchronous hook
ligoj hook upsert --name "audit_role_change" --command "/home/ligoj/hooks/ligoj_audit.sh" --directory /var/log --timeout 10 --match '{"path":"system/security/role.*"}' --inject secret1 secret2
# Synchronous hook
ligoj hook upsert --name "audit_role_change" --command "/home/ligoj/hooks/ligoj_audit.sh" --directory /var/log --timeout 10 --match '{"path":"system/security/role.*", "method":"POST"}' --inject secret1 secret2 --delay 0

Note

  • For Docker image runtime, this program is executed by the container ligoj-api, and must be resolvable. Either this program is already packaged in the container, or it is mounted as a Docker volume to the host. Usually the mounted volume is /home/ligoj and points to the host path such as /var/path/to/ligoj. In the above hook sample, the user-level script would be /var/path/to/ligoj/ligoj_audit.sh.
  • --name The human readable hook name. Is displayed in logs and HTTP headers of synchronous executions.
  • --command The command to execute. Must be allowed by ligoj.hook.path configuration. This condition is checked at creation and execution time.
  • --directory The working directory where the hook is executed.
  • --inject Can relate to any configuration names supported by the configuration get command and will be provided in the payload variable.
  • --match Must be a valid JSON stringified object having :
    • at least the path property relating to a valid regular expression matching to one of the available Ligoj's endpoint.
    • an optional method property corresponding to ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"].
  • --delay The delay in seconds before the hook is executed. Default to 1. Use 0 for synchronous hooks.
  • --timeout The timeout in seconds before the hook is executed. Default to 10.

Sample executable hook script /home/ligoj/ligoj_audit.sh:

#!/bin/bash
payload="$(echo "$PAYLOAD"| base64 -d)"echo"$(echo "$payload"|jq -r '.now')$(echo "$payload"|jq -r '.method')$(echo "$payload"|jq -r '.path') [$(echo "$payload"|jq -r '.user')]">> ligoj_audit.log

For Docker runtime, to verify this hook would run as expected, try the following command from the host:

docker exec ligoj-api jq --version
# > jq-1.6
docker exec ligoj-api python3 --version
# > Python 3.11.5

Update hook

For update, id or name can be used. However, if name needs to be updated, provide the id as well.

ligoj hook upsert --id 4 --name "audit_role_change_new" --command "$(pwd)/docs/sample_hook_ligoj_audit.sh" --directory /var/log --match '{"path":"system/security/role.*"}' --inject "feature:iam:node:primary""my-secret"

Delete hook

Deletion can be done by id or name attribute:

ligoj hook delete --id 2
ligoj hook delete --name "audit_role_change"

Hook get

Optional id or name filters are accepted:

ligoj hook get
ligoj hook get --id 1
ligoj hook get --name "audit_role_change"
[{"id": 1, "name": "audit_role_change", "workingDirectory": "/var/log", "command": "/path/to/ligoj_audit.sh", "match": "{\"path\":\"system/security/role.*\"}", "injects": ["java.class.path"]}]

Plugin

List Ligoj plugins

ligoj plugin list
[
{
"id": "feature:ui",
"name": "Ui",
"plugin": {
"id": 4,
"createdBy": "_system",
"createdDate": 1758279349900,
"lastModifiedBy": "_system",
"lastModifiedDate": 1762277251346,
"version": "2025-10-31T14:25:16.306432087Z",
"key": "feature:ui",
"artifact": "plugin-ui",
"basePackage": "org.ligoj.app.plugin.ui",
"type": "FEATURE"
},
"location": "/path/to/ligoj-plugins/plugin-ui/target/classes/",
"deleted": false,
"nodes": 0,
"subscriptions": 0
},
...
{
"id": "service:id:ldap",
"name": "Ldap",
"plugin": {
"id": 155,
"createdBy": "_system",
"createdDate": 1762277251427,
"lastModifiedBy": "_system",
"lastModifiedDate": 1762450362232,
"version": "2025-11-05T22:06:40.787627578Z",
"key": "service:id:ldap",
"artifact": "plugin-id-ldap",
"basePackage": "org.ligoj.app.plugin.id.ldap.resource",
"type": "TOOL"
},
"location": "/path/to/ligoj-plugins/plugin-id-ldap/target/classes/",
"nodes": 2,
"node": {
"id": "service:id:ldap",
"name": "Identity LDAP",
"refined": {"id": "service:id","name": "Identity", "mode": "create", "uiClasses": "far fa-id-badge"},
"mode": "all"
},
"subscriptions": 5
}
]

Install Ligoj plugins

When an explicit version is not provided, the latest version available from Maven Central is used.

This lookup depends on the plugins.repository-manager.nexus.search configuration, while the download relies on plugins.repository-manager.nexus.artifact configuration.

ligoj plugin install --id "plugin-id" --repository "central" --version "2.2.10" --force
ligoj plugin install --id "plugin-id" --version "LATEST" --repository "nexus"
ligoj plugin install --id "plugin-id-ldap" --version "2.1.1" --repository "nexus" --force
ligoj plugin install --id "plugin-req-squash"
ligoj plugin install --id "plugin-req"
[INFO ] [ligoj] Plugin 'plugin-req' has been installed/updated, a restart is required

Two successful consecutive executions give this output:

[INFO ] [ligoj] Plugin 'plugin-id-ldap:2.0.3' is being installed
[INFO ] [ligoj] Plugin 'plugin-req:1.0.1' is installed but requires a restart to be available

Note After installing plugins, a restart is needed to use them.

Restart API

ligoj plugin restart
null

Optionally, a wait for status can be defined. A regular poll to the server status is performed until reaching DOWN or UP status. When different from 0, the final status is returned.

ligoj plugin restart --wait 20
{"status": "UP"}

Node

List nodes

ligoj node list
{
"recordsTotal": 23, "recordsFiltered": 23, "data": [
{"id": "service:build", "name": "Build", "mode": "link", "uiClasses": "fa fa-cogs", "enabled": true}, {"id": "service:build:jenkins", "name": "Jenkins", "refined": {"id": "service:build", "name": "Build", "mode": "link", "uiClasses": "fa fa-cogs"}, "mode": "all", "uiClasses": "fab fa-jenkins", "enabled": true}, ...
]
}

Optionally, parameters can be returned with provided mode and other filters

ligoj node list --parameters-mode "all"
ligoj node list --parameters-mode "all" --search jenkins
ligoj node list --parameters-output "map" --refined "service:build:jenkins"
{
"recordsTotal": 23, "recordsFiltered": 23, "data": [
{"id": "service:build", "name": "Build", "mode": "link", "uiClasses": "fa fa-cogs", "enabled": true, "parameters": []}, {"id": "service:build:jenkins", "name": "Jenkins", "refined": {"id": "service:build", "name": "Build", "mode": "link", "uiClasses": "fa fa-cogs"}, "mode": "all", "uiClasses": "fab fa-jenkins", "enabled": true, "parameters": []}, {"id": "service:build:jenkins:local", "name": "Jenkins Local", "refined": {"id": "service:build:jenkins", "name": "Jenkins", "refined": {"id": "service:build", "name": "Build", "mode": "link", "uiClasses": "fa fa-cogs"}, "mode": "all", "uiClasses": "fab fa-jenkins"}, "mode": "all", "enabled": true, "parameters": [
{"text": "-secured-", "parameter": "service:build:jenkins:api-token"}, {"text": "http://localhost:9190/", "parameter": "service:build:jenkins:url"}, {"text": "-secured-", "parameter": "service:build:jenkins:user"}
]
},
...
]
}

Configure/update a node

If the related node already exists, it is updated.

ligoj node upsert --id "service:id:ldap:remote1" --name "Remote1" --from ligoj-ldap.json
ligoj node upsert --id "service:id:ldap:remote1" --name "Remote1" --from https://path/to/ligoj-ldap.json
{"id": "service:id:ldap:remote1", "name": "Remote1", "mode": "all", "enabled": true}

Input --from JSON:

  • See --from for JSON loading options
  • JSON can be as list or dict (compact). See sample.
  • The parameters marked as sensitive are encrypted in database of Ligoj.

Content of sample ligoj-ldap.json file:

[
{
"parameter": "service:id:ldap:base-dn",
"text": "cn=Test"
},
{
"parameter": "service:id:ldap:uid-attribute",
"text": "uid"
}
]

Get node by identifier

ligoj node get --id "service:id"
ligoj node get --id "service:id:ldap"
ligoj node get --id "service:id:ldap:remote1"
{"id": "service:id:ldap:remote1", "name": "Remote1", "mode": "all", "enabled": true}

Optionally, parameters can be returned with provided mode

ligoj node get --id "service:id" --parameters-mode "all"
ligoj node get --id "service:id:ldap" --parameters-mode "all"
ligoj node get --id "service:id:ldap:remote1" --parameters-mode "all"
{
"id": "service:id:ldap:remote1", "name": "Remote1", "mode": "all", "enabled": true,
"parameters": [
{"text": "cn=Test", "parameter": "service:id:ldap:base-dn"}, {"bool": true, "parameter": "service:id:ldap:clear-password"}, {"text": "organizationalUnit", "parameter": "service:id:ldap:companies-class"},
{"text": "-secured-", "parameter": "service:id:ldap:user-dn"}]
}

Optionally, parameters can be returned with more details

ligoj node get --id "service:id:ldap:remote1" --parameters-mode "all" --parameters-output "full"
{
"id": "service:id:ldap:remote1", "name": "Remote1", "mode": "all", "enabled": true,
"parameters": [{
"parameter": {
"id": "service:id:group",
"type": "text",
"owner": {
"id": "service:id",
"name": "Identity",
"mode": "create",
"uiClasses": "far fa-id-badge"
},
"mandatory": true,
"secured": false,
"depends": []
}
},
{
"text": "cn=Test",
"parameter": {
"id": "service:id:ldap:base-dn",
"type": "text",
"owner": {
"id": "service:id:ldap",
"name": "Identity LDAP",
"refined": {
"id": "service:id",
"name": "Identity",
"mode": "create",
"uiClasses": "far fa-id-badge"
},
"mode": "all"
},
"mandatory": false,
"secured": false,
"depends": []
}
}
]
}

Optionally, parameter values can be decrypted. One API call is performed for each secured value.

This mode is only available for users having ADMIN role.

ligoj node get --id "service:id:ldap:remote1" --parameters-mode "all" --parameters-output map --parameters-secured
{
"id": "service:id:ldap:remote1", "name": "Remote1", "mode": "all", "enabled": true, "parameters": {
"service:id:ldap:base-dn": "cn=Test",
"service:id:ldap:clear-password": true,
"service:id:ldap:url": "ldap:localhost:1389",
"service:id:ldap:password": "secret",
"service:id:ldap:companies-class": "organizationalUnit", "service:id:ldap:companies-dn": "ou=people,dc=sample,dc=com"
}
}

Get node status

ligoj node status --id "service:id:ldap:remote1"
{"id": "down"}

Delegate node

Node delegates allow users, groups or companies to manage nodes.

Sub nodes inherit the delegate permissions.

List delegate nodes

ligoj delegate-node list
[
{"id": 1, "createdBy": "_system", "createdDate": 1758279349979, "lastModifiedBy": "_system", "lastModifiedDate": 1758279349979, "name": "service", "receiver": "ligoj-admin", "receiverType": "user", "canWrite": true, "canAdmin": true, "canSubscribe": true, "referenceID": "service"}
]

Create delegate node

Create a delegate with subscribe, administration, and creation rights for a receiver on an optional node and its sub-nodes.

The provided node does not need to exist yet.

ligoj delegate-node create --node service --can-subscribe --can-admin --can-write --receiver jdoe --receiver-type user
ligoj delegate-node create --node service:id --can-subscribe --receiver internal --receiver-type company
ligoj delegate-node create --node service:id:ldap:instance1 --can-admin --can-write --receiver group1 --receiver-type group

Delete delegate node

Delete a delegate node from its identifier.

ligoj delegate-node delete --id 1

Get a delegate node

Get a delegate node from its identifier.

ligoj delegate-node get --id 1
ligoj delegate-node get --node 1

Project

List projects

List projects with optional search criteria.

ligoj project list
ligoj project list --search project1
{
"recordsTotal": 1, "recordsFiltered": 1, "data": [
{
"id": 153, "createdDate": 1705853880193, "lastModifiedDate": 1705853880193, "createdBy": { "id": "ligoj-admin", ...}, "lastModifiedBy": {"id": "ligoj-admin", ...}, "name": "Project 1",
"teamLeader": {"id": "ligoj-admin", ...}, "pkey": "project1", "description": "",
"nbSubscriptions": 2
},
...
]
}

Get Project

ligoj project get --id 153
ligoj project get --id "project1"
{
"id": 153, "createdDate": 1705853880193, "lastModifiedDate": 1705853880193, "createdBy": { "id": "ligoj-admin", ...}, "lastModifiedBy": {"id": "ligoj-admin", ...}, "name": "Project 1",
"teamLeader": {"id": "ligoj-admin", ...}, "pkey": "project1", "description": "",
"manageSubscriptions": true, "subscriptions": [
{"id": 355, ...},
{"id": 362, ...}
]
}

Create Project

ligoj project create --name "project4" --team-leader "ligoj-admin" --pkey "sample:project4" --description "Sample project 4" --context='{"some":"value"}'
{
"id": 352, "creationContext": "{\"some\":\"value\"}", "name": "project4", "teamLeader": {"id": "ligoj-admin",...},
"pkey": "sample:project4", "description": "Sample project 4",
...
}

Delete Project

Delete project with optional search criteria.

ligoj project delete --id 252
ligoj project delete --id "sample:project4"
ligoj project delete --id "sample:project4" --with-data

Subscription operations

List subscriptions

Combined filters are accepted.

ligoj subscription list --node "service:id:ldap:remote1"
ligoj subscription list --tool "service:id:ldap"
ligoj subscription list --service "service:id"
ligoj subscription list --project "project1"
ligoj subscription list --project 104
[
{"id": 155, "node": "service:id:ldap:remote1", "project": 104}, {"id": 302, "node": "service:qa:sonarqube:8", "project": 104}
]

Get subscription details

ligoj subscription get --id 302
{"service:qa:sonarqube:project": "test", "service:qa:sonarqube:url": "http://127.0.0.1:9000/"}

With related project and node details

ligoj subscription get --id 302 --details
{"subscription": 302, "project": {"id": 104, "name": "Project1", "description": "Foo bar"}, "parameters": {"service:qa:sonarqube:project": "test", "service:qa:sonarqube:url": "http://127.0.0.1:9000/"}, "node": {"id": "service:qa:sonarqube:8", "name": "Sonar Local 8", "refined": {"id": "service:qa:sonarqube", "name": "SonarQube", "refined": {"id": "service:qa", "name": "Quality Assurance", "mode": "link", "uiClasses": "fas fa-tachometer-alt"}, "mode": "link"}, "mode": "link", "enabled": true}}

Create subscription

Configuration file can be a JSON file, a remote HTTP URL, or plain JSON. Both forms of parameters are accepted, as list or dict (compact).

Duplicate subscriptions are ignored: same project, node and parameters.

ligoj subscription create --project project1 --node "service:id:ldap:remote1" --from conf.json
ligoj subscription create --project project1 --node "service:id:ldap:remote1" --from "https://path/to/conf.json"
ligoj subscription create --project project1 --node "service:id:ldap:remote1" --from '[{"parameter": "service:id:group", "text": "project1-team"}, {"parameter": "service:id:ou", "text": "project1"}]'
ligoj subscription create --project project1 --node "service:id:ldap:remote1" --from '{"service:id:group": "project1-team", "service:id:ou": "project1"}'
...
362

Input --from JSON:

  • See --from for JSON loading options
  • JSON can be as list or dict (compact). See sample.
  • The parameters marked as sensitive are encrypted in database of Ligoj.

Delete a subscription

Delete a subscription from its identifier.

ligoj subscription delete --id 302

By default, only the link between Ligoj and the remote tool is removed. Optionally, when Ligoj has created data with the subscription, it can also be deleted with this operation.

For example, LDAP groups or a Jenkins job created at subscription time will be deleted.

ligoj subscription delete --id 302 --with-data

Get subscription statuses of a project

Return the last computed status of all subscriptions of given project.

ligoj subscription status --project 104
ligoj subscription status --project project1
{
"155": {"specifics": [], "value": "UP", "type": "status", "node": {"id": "service:id:ldap:remote1", "name": "TestAnnuaireCLI", "mode": "all"}, "subscription": 155}, "252": {"specifics": [], "value": "DOWN", "type": "status", "node": {"id": "service:qa:sonarqube:user", "name": "Sonar Local User", "mode": "link"}, "subscription": 252},
"157": {"specifics": [], "value": "UP", "type": "status", "node": {"id": "service:id:ldap:remote1", "name": "TestAnnuaireCLI", "mode": "all"}, "subscription": 157}
}

Request subscription status refresh

Retrieve the up-to-date status of a subscription.

Up subscription:

ligoj subscription refresh --id 155
{"id": 155, "status": "up", "node": "service:id:ldap:remote1", "project": 104, "data": {"members": 0}, "parameters": {...}}

Down subscription:

ligoj subscription refresh --id 252
{"id": 252, "status": "down", "project": 104, "data": {}, "parameters": {"service:qa:sonarqube:project": "test",...}}

Plugin id

Operations related to plugin-id and sub-plugins.

Plugin id:scope operations

Operations related to container scopes managed by service:id nodes.

Create container scope

Create a container scope

ligoj id:scope create --id "Unassigned" --type "group" --dn "ou=groups,dc=example,dc=com"
ligoj id:scope create --id "Projects" --type "group" --dn "ou=project,ou=groups,dc=example,dc=com"
ligoj id:scope create --id "Tools" --type "group" --dn "ou=tools,ou=groups,dc=example,dc=com"
ligoj id:scope create --id "Unassigned" --type "company" --dn "ou=people,dc=example,dc=com"
ligoj id:scope create --id "Internal" --type "company" --dn "ou=internal,ou=people,dc=example,dc=com"
ligoj id:scope create --id "Unassigned" --type "tree" --dn "dc=example,dc=com"

Get container scope

Return a container scope

ligoj id:scope get --id "SampleGroup2"
{"id": "samplegroup2", "name": "SampleGroup2", "scope": "Unassigned", "locked": false}

List container scopes

Return a list of container scopes

ligoj id:scope list --type "group"
{
"recordsTotal": 4, "recordsFiltered": 3, "data": [
{"id": 5, "name": "Unassigned", "dn": "ou=groups,dc=sample,dc=com", "type": "group", "locked": false}, {"id": 6, "name": "Project", "dn": "ou=project,ou=groups,dc=sample,dc=com", "type": "group", "locked": false}, {"id": 7, "name": "Technical", "dn": "ou=tools,ou=groups,dc=sample,dc=com", "type": "group", "locked": false}
]
}

Plugin id:group operations

Operations related to groups managed by service:id nodes

Group name is case insensitive.

Create group

Create a group.

ligoj id:group create --name "SampleGroup2" --scope "Unassigned"

Create a group inside a group

ligoj id:group create --name "SampleSubGroup" --scope "Unassigned" --parent "SampleGroup2"
ligoj id:group create --name "SampleSubGroup2" --scope "Unassigned" --parent "SampleSubGroup"

Delete a group

Delete a group. The command will not fail if the group does not exist.

ligoj id:group delete --name "SampleGroup2"

Get group

Retrieve a group

ligoj id:group get --name "SampleGroup2"
{"id": "samplegroup2", "name": "SampleGroup2", "scope": "Unassigned", "locked": false}

Delete group

Delete a group. If the group does not exist, the command will not return an error.

ligoj id:group delete --name "SampleGroup2"

List group

List groups

ligoj id:group list
{
"recordsTotal": 4,
"recordsFiltered": 4,
"data": [
{
"id": "sample group",
"name": "Sample Group",
"scope": "Unassigned",
"locked": false,
"countVisible": 3,
"count": 3,
"canWrite": true,
"canAdmin": true,
"containerType": "group"
},
{
"id": "samplegroup2",
"name": "SampleGroup2",
"scope": "Unassigned",
"locked": false,
"countVisible": 0,
"count": 0,
"canWrite": true,
"canAdmin": true,
"containerType": "group"
},
{
"id": "samplesubgroup",
"name": "SampleSubGroup",
"scope": "Unassigned",
"locked": false,
"countVisible": 0,
"count": 0,
"canWrite": true,
"canAdmin": true,
"containerType": "group",
"parents": [
"samplegroup2"
]
},
{
"id": "samplesubgroup2",
"name": "SampleSubGroup2",
"scope": "Unassigned",
"locked": false,
"countVisible": 0,
"count": 0,
"canWrite": true,
"canAdmin": true,
"containerType": "group",
"parents": [
"samplesubgroup",
"samplegroup2"
]
}
]
}

Plugin id:user operations

Operations related to users managed by service:id nodes

Create user

ligoj id:user create --id jdupont --firstname "Jean" --lastname "Dupont" --mail "jdupont@kloudy.io" --company "external" --groups "Sample Group,SampleGroup2"
ligoj id:user create --id jdupont2 --firstname "Jean" --lastname "Dupont" --mail "jdupont@kloudy.io" --company "external" --groups "Sample Group,SampleGroup2"

Delete user

Delete a user. If the user does not exist, the command will not return an error.

ligoj id:user delete --id jdupont2
ligoj id:user delete --mail jdupont@kloudy.io

List users

ligoj id:user list
ligoj id:user list --company "department1" --group "Sample Group" --criteria "@sample.com" --page-length 2
ligoj id:user list --company "department1" --page-length 2
{
"recordsTotal": 102, "recordsFiltered": 102,
"extensions": {"customAttributes": ["uidFonctionnel"]}},
"data": [
{"firstName": "John", "lastName": "Doe", "id": "jdoe", "company": "external", "mails": ["jdoe@sample.com"], "groups": ["Sample Group", "SampleGroup2"], "name": "jdoe"}, {"firstName": "Cli2", "lastName": "Name", "id": "cli2name", "company": "external", "mails": ["a@bc.org","cli2@sample.com"], "groups": ["Sample Group"], "name": "cli2name"}
]
}

Get user

Return a user. If the user does not exist, the command will return null.

ligoj id:user get --id jdupont
ligoj id:user get --mail jdupont@kloudy.io
{"firstName": "Jean", "lastName": "Dupont", "id": "jdupont", "company": "external", "mails": ["jdupont@kloudy.io"], "groups": ["Sample Group", "SampleGroup2"], "name": "jdupont"}

Add user to a group

The user and the group must exist. The command does not fail if the user is already in the group.

ligoj id:user add --id jdupont --groups "SampleGroup2"
ligoj id:user add --mail jdupont@kloudy.io --groups "SampleGroup2"
ligoj id:user add --mail cli10.name@sample.com --groups "Sample Group"

Remove user from a group

The user and the group must exist. The command does not fail if the user is not in the group.

ligoj id:user remove --id jdupont --groups "SampleGroup2"
ligoj id:user remove --mail jdupont@kloudy.io --groups "SampleGroup2"

Reset user password

For a specific user (need administrative rights):

The user must exist.

ligoj id:user reset-password --id jdupont
ligoj id:user reset-password --mail jdupont@kloudy.io

For current user:

ligoj id:user reset-password

Plugin prov

Operations related to plugin-prov and its provider sub-plugins (AWS, Azure, GCP, …), exposed under prov:<resource> services. They drive a provisioning quote identified by a subscription id (--subscription/-s), the project's subscription to a service:prov:* node.

ServiceActionsREST base
prov:quoteget, update, refresh, refresh-cost, locationsservice/prov/{subscription}
prov:instancelookup, create, update, delete, delete-allservice/prov/instance
prov:containerlookup, create, update, delete, delete-allservice/prov/container
prov:databaselookup, create, update, delete, delete-allservice/prov/database
prov:functionlookup, create, update, delete, delete-allservice/prov/function
prov:storagelookup, create, update, delete, delete-allservice/prov/storage
prov:supportlookup, create, update, delete, delete-allservice/prov/support
prov:usagelist, create, update, deleteservice/prov/{subscription}/usage
prov:budgetlist, create, update, deleteservice/prov/{subscription}/budget
prov:optimizerlist, create, update, deleteservice/prov/{subscription}/optimizer
prov:tagcreate, update, deleteservice/prov/{subscription}/tag
prov:cataloglist, status, update, cancelservice/prov/catalog
prov:uploadresourcesservice/prov/{subscription}/upload

Run ligoj prov:<service> --help (and ... <action> --help) for the full argument list. Every create/update also accepts --data '<json>' to set or override any backend field not exposed as a flag.

Quote configuration (prov:quote)

# Inspect the full quote (resources + total cost)
ligoj prov:quote get --subscription 12
# Set defaults and recompute
ligoj prov:quote update -s 12 --location "eu-west-1" --usage "dev" --license "BYOL"
ligoj prov:quote refresh -s 12

Compute, database, container, function

The typical flow is lookup a price → create the resource with that price id:

# 1. find the cheapest instance matching the requirement
ligoj prov:instance lookup -s 12 --cpu 2 --ram 4096 --os LINUX
# 2. create it from the returned price id
ligoj prov:instance create -s 12 --name "web" --price 4211 --cpu 2 --ram 4096 \
--os LINUX --internet PUBLIC --min-quantity 1 --max-quantity 3
# database / container / function follow the same pattern
ligoj prov:database create -s 12 --name "db" --price 5120 --cpu 1 --ram 2048 --engine MYSQL
ligoj prov:function create -s 12 --name "fn" --price 77 --runtime Python --nb-requests 5
# update / delete
ligoj prov:instance update -s 12 --id 99 --max-quantity 5
ligoj prov:instance delete --id 99
ligoj prov:instance delete-all -s 12

Storage, usage, budget, optimizer, tag

ligoj prov:storage create -s 12 --name "data" --type "gp2" --size 100 --instance 99
ligoj prov:usage create -s 12 --name "dev" --rate 50 --duration 12
ligoj prov:budget create -s 12 --name "2026" --initial-cost 10000
ligoj prov:tag create -s 12 --name "env" --value "prod" --type INSTANCE --resource 99

Catalog and bulk upload

# trigger and follow a provider catalog import
ligoj prov:catalog update --node "service:prov:aws:test" --force
ligoj prov:catalog status --node "service:prov:aws:test"# bulk-create resources from a CSV
ligoj prov:upload resources -s 12 --from ./resources.csv --merge update

Plugin build

Operations related to plugin-build and its CI provider sub-plugins (Jenkins, Travis), exposed under the build:job service. The build provider is taken from --provider, inferred from the --node identifier (service:build:<provider>:…), or resolved from the subscription's node.

Note: this drives the Ligoj build service (service/build/<provider>). The separate jenkins service talks directly to a Jenkins server instead.

ActionArgumentsREST
trigger--subscription [--provider]POST service/build/<provider>/build/{subscription}
find--node --criteria [--provider]GET service/build/<provider>/{node}/{criteria}
templates--node --criteria [--provider]GET service/build/<provider>/template/{node}/{criteria} (Jenkins)
get--node --id [--provider]GET service/build/<provider>/{node}/job/{id}
# Trigger the build configured for a subscription (provider inferred from the subscription)
ligoj build:job trigger --subscription 42
# Search jobs / templates on a node (provider inferred from the node)
ligoj build:job find --node "service:build:jenkins:dev" --criteria "my-app"
ligoj build:job templates --node "service:build:jenkins:dev" --criteria "template"# Return a single job by id
ligoj build:job get --node "service:build:jenkins:dev" --id "my-app"

Dev environment

dev init brings up, on Kubernetes, the backing services a Ligoj developer needs and wires the resulting endpoints and credentials into the [dev] section of ~/.ligoj/credentials. Every dev command uses the devprofile by default (no --profile needed), and the generated [dev] profile can be reused by any other command with --profile dev.

Unlike every other command, dev is purely local and does not require LIGOJ_ENDPOINT.

Two runtimes, both driven by podman — no heavyweight cluster unless you ask for Harbor:

  • podman kube play for every self-contained service. Each becomes a Pod (manifest written to ~/.ligoj/dev/k8s/<service>.yaml); PersistentVolumeClaims map to podman named volumes so data survives restarts. A pre-existing raw container of the same name is migrated to a pod automatically.
  • a kind cluster (podman provider, ligoj-dev) created on demand only for Harbor, which needs a real cluster; Harbor is installed with its Helm chart and exposed on localhost via a NodePort + kind extraPortMappings.

The init phase bootstraps its own tooling: a missing CLI (podman, and kind/helm/kubectl for Harbor) is installed with Homebrew, and the podman machine is initialized/started as needed — so a fresh machine can go from nothing to a running stack with one command. (If Homebrew is absent, you get a clear message to install the tool yourself.) The whole stack (GitLab + SonarQube + Nexus + Artifactory + kind at once) is heavy, so dev init also enforces the podman machine's resources: if it has fewer than 6 vCPU or 23 GB RAM it is stopped, resized up to that minimum, and restarted (a fresh machine is created already sized; a machine that already exceeds the minimum is left untouched). On macOS it also checks the tools dev demo needs later — Java 25 (installed as the Temurin JDK cask via Homebrew) and Maven 3.9.16 (installed via SDKMAN, bootstrapped if missing); these two are best-effort and never abort init. Pass --skip-prereqs to skip all these checks. Each service streams live progress and is idempotent — a running pod is reused; use --recreate to replace it.

ServicePod / releaseDefault portImageWhat dev init configures
postgresqlligoj-db5432postgres:17ligoj/ligoj user/db (what ligoj-api expects), persistent volume ligoj_db_data
openldapopenldap1389bitnamilegacy/openldap:latestManager / dc=sample,dc=com; generates the admin password if missing; volume openldap_data
keycloakkeycloak9083quay.io/keycloak/keycloak:26.6.1backed by the shared postgresql (dedicated keycloak database); realm ligoj, LDAP user federation, confidential ligoj client; prints Spring Boot properties
jenkinsjenkins8085jenkins/jenkins:2.570-slim-jdk25volume jenkins_home; provisions the admin user and generates an API token
sonarqubesonarqube9000sonarqube:26.6.0.123539-communitybacked by the shared postgresql (dedicated sonarqube database); changes the default admin password and creates an API token
gitlabgitlab8929 (+ssh 2289)gitlab/gitlab-ce:latestomnibus CE (single container), trimmed footprint; root password in [dev]
harborharbor (Helm, on kind)8088goharbor/harbor chartminimal Harbor (no trivy/metrics); admin password in [dev]
nexusnexus8181sonatype/nexus3:latestvolume nexus_data; resets the generated initial admin password and stores it in [dev] (host 8181 leaves 8081 free for the Ligoj API)
artifactoryartifactory8082jfrog/artifactory-oss:7.111.9 (pinned)backed by the shared postgresql (dedicated artifactory database, Derby is refused), volume artifactory_data; admin password in [dev]. Forces IPv4, self-heals a hung boot (see below), and is pinned below 7.125 to keep the web UI usable (see below)
argocdargocd (Helm, on kind)8083argo/argo-cd chartrole:ligoj RBAC + ligoj account & API token; Dex LDAP federation to OpenLDAP

Options

OptionDescription
--only, -OLimit to a subset, e.g. --only postgresql keycloak (default: all)
--recreate, -RDelete and recreate the pods / kind cluster (named volumes are kept)
--wait, -wHow long to wait for readiness, with live progress: omit = wait until done or Ctrl+C; 0 = no wait (skip readiness/token steps); N = up to N seconds. Also accepted by start / stop / restart.
--ldap-port / --jenkins-port / --sonar-port / --db-port / --keycloak-port / --gitlab-port / --harbor-port / --nexus-port / --artifactory-port / --argocd-portOverride a host port

Every value can also be set in the [dev] section or as an environment variable. The credentials are read before a secret is generated, so you stay in control:

ServiceInputs (option · env · [dev] key)
postgresqlDB_IMAGE·db_image, DB_PORT·db_port, POSTGRES_USER·db_user, POSTGRES_PASSWORD·db_password, POSTGRES_DB·db_name
openldapLDAP_IMAGE·ldap_image, LDAP_PORT·ldap_port, LDAP_ADMIN_USERNAME·ldap_admin_user, LDAP_ROOT·ldap_root, LDAP_ADMIN_PASSWORD·ldap_admin_password, LDAP_SCHEMA_DIR·ldap_schema_dir
keycloakKEYCLOAK_IMAGE·keycloak_image, KEYCLOAK_PORT·keycloak_port, KC_BOOTSTRAP_ADMIN_USERNAME·keycloak_admin_user, KC_BOOTSTRAP_ADMIN_PASSWORD·keycloak_admin_password, KEYCLOAK_LDAP_URL·keycloak_ldap_url, KEYCLOAK_DB_PASSWORD·keycloak_db_password (shared-DB role)
jenkinsJENKINS_IMAGE·jenkins_image, JENKINS_PORT·jenkins_port, JENKINS_API_USER·jenkins_api_user, JENKINS_ADMIN_PASSWORD·jenkins_admin_password, JENKINS_API_TOKEN·jenkins_api_token
sonarqubeSONAR_IMAGE·sonar_image, SONAR_PORT·sonar_port, SONAR_ADMIN_PASSWORD·sonar_admin_password, SONAR_DB_PASSWORD·sonar_db_password (shared-DB role)
gitlabGITLAB_IMAGE·gitlab_image, GITLAB_PORT·gitlab_port, GITLAB_SSH_PORT·gitlab_ssh_port, GITLAB_ROOT_PASSWORD·gitlab_root_password
harborHARBOR_PORT·harbor_port, HARBOR_NODE_PORT·harbor_node_port, HARBOR_ADMIN_PASSWORD·harbor_admin_password, HARBOR_REDIS_IMAGE·harbor_redis_image
nexusNEXUS_IMAGE·nexus_image, NEXUS_PORT·nexus_port, NEXUS_ADMIN_PASSWORD·nexus_admin_password
artifactoryARTIFACTORY_IMAGE·artifactory_image, ARTIFACTORY_PORT·artifactory_port, ARTIFACTORY_USER·artifactory_user, ARTIFACTORY_PASSWORD·artifactory_password, ARTIFACTORY_DB_PASSWORD·artifactory_db_password (shared-DB role), ARTIFACTORY_HEAL_AFTER·artifactory_heal_after (boot self-heal grace, default 90, 0 disables)
argocdARGOCD_PORT·argocd_port, ARGOCD_NODE_PORT·argocd_node_port (LDAP fields reuse the openldap keys)

In return, dev initwrites to [dev]: db_* (host/port/name/user/password/url), ldap_url and ldap_admin_password, keycloak_endpoint / keycloak_admin_password / keycloak_client_secret / keycloak_issuer_uri / keycloak_db_password, jenkins_endpoint / jenkins_admin_password / jenkins_api_token, sonar_endpoint / sonar_admin_password / sonar_api_token / sonar_db_password, gitlab_endpoint / gitlab_root_password / gitlab_token, harbor_endpoint / harbor_admin_password, nexus_endpoint / nexus_admin_password, artifactory_endpoint / artifactory_password / artifactory_db_password, and argocd_endpoint / argocd_admin_password / argocd_account / argocd_api_token. Keycloak, SonarQube and Artifactory each get a dedicated role + database in the shared postgresql (reached from their pods via host.containers.internal). For Jenkins, SonarQube, GitLab and ArgoCD an API token is generated (and reused on later runs), so --profile dev can drive their APIs straight away. The GitLab token is a root personal access token minted via the Rails console (api, read_api, read_repository, write_repository scopes).

# Bring up the whole local stack (Harbor pulls in a kind cluster)
ligoj dev init
# Only the database and Keycloak, on custom ports
ligoj dev init --only postgresql keycloak --db-port 5432 --keycloak-port 9083
# Recreate Jenkins to apply a freshly configured admin token
JENKINS_API_USER=admin JENKINS_API_TOKEN="$(cat token.txt)" ligoj dev init --only jenkins --recreate
# Reuse the generated credentials in subsequent commands
ligoj --profile dev sonar project list

Footprint: GitLab (~4 GB) and Harbor (a kind node + ~7 pods) are heavy; running all services at once needs a roomy podman machine. Use --only to bring up just what you need.

Data safety: for postgresql, an existing ligoj-db keeps its current data source (named volume or host bind mount) and image even on --recreate, so a --recreate never orphans the database nor swaps the PostgreSQL major version under an existing data directory.

Status

dev status reports, for every service, its runtime state, a health probe from the host and the URL to reach it — handy after an init or to see what is still up:

SERVICE STATUS HEALTH URL
---------- -------------- ------ ---------------------------------------
postgresql running OK postgresql://ligoj@localhost:5432/ligoj
openldap running OK ldap://localhost:1389
keycloak running OK http://localhost:9083
jenkins running OK http://localhost:8085
sonarqube running OK http://localhost:9000
gitlab running OK http://localhost:8929
harbor running (kind) OK http://localhost:8088
nexus running OK http://localhost:8181
artifactory running OK http://localhost:8082/artifactory

STATUS is the pod (or, for Harbor, the kind cluster) state — running / stopped / absent; HEALTH is probed from your machine (an HTTP check for web services, a TCP connect for PostgreSQL/LDAP). It reads the ports/URLs recorded in [dev], so it works without contacting podman.

Artifactory boot self-heal

Artifactory's JFrog microservice mesh occasionally deadlocks on boot: the internal services fail to join over localhost (they try the IPv6 ::1 loopback and get connection refused), so the container stays up but its router never binds :8082 — the port dev and the Ligoj node probe — and the readiness wait would otherwise hang forever. Two mitigations are built in: the pod is started with -Djava.net.preferIPv4Stack=true, and if the router is still not listening after a grace period (default 90 s, set ARTIFACTORY_HEAL_AFTER seconds, 0 disables) the wait restarts the pod once — a fresh boot usually wins the race (~40 s). A healthy-but-slow boot (which answers 503 while starting) is never restarted.

Artifactory version pin (usable UI)

The image is pinned to artifactory-oss:7.111.9, not:latest, on purpose. From ~7.125 the OSS web UI's frontend service (jffe) is stuck in an unbounded retry loop on a "first-time entitlement fetch" — a licensing/entitlements gRPC call that returns 404 UNIMPLEMENTED because that service ships only with the commercial JFrog Platform, not OSS. Until it "succeeds" (it never does) jffe won't serve UI data, so every/ui/api/v1/ui/* call hangs ~12 s and the UI is effectively unusable (long JFrog splash, every screen crawls). The REST API and all dev demo Maven operations are unaffected — only the browser UI. Tested across versions: 7.146/7.133/7.125 all hang; 7.111.9 is the newest OSS tag whose UI answers in milliseconds with no entitlement loop. (The many 404s the browser logs for xray, mc, distribution, apptrust, … microfrontends are unrelated and harmless — those are commercial modules absent from OSS.) Override the pin with ARTIFACTORY_IMAGE / [dev] artifactory_image if you need a specific version; a downgrade needs a fresh volume and database (podman pod rm -f artifactory, podman volume rm artifactory_data, and drop the artifactory DB), because a newer schema/master-key won't start on an older binary.

Per-service config

dev config <service> prints the key properties of a single service — URL, admin user and password, and the other connection details — read straight from [dev]:

# sonarqube
url http://localhost:9000
admin user admin
admin password v1JE…
api token squ_…

Works for postgresql, openldap, keycloak, jenkins, sonarqube, gitlab, harbor, nexus, artifactory and argocd. Each service adds its own relevant fields — e.g. PostgreSQL the host/port/database, OpenLDAP the bind/base DN, Keycloak the realm + issuer URI + client id/secret, GitLab the SSH URL, Harbor the registry host.

Omit the service to get a summary table of every service at once:

SERVICE URL USER PASSWORD TOKEN/SECRET
---------- --------------------------------------- ------- -------- ------------
postgresql postgresql://ligoj@localhost:5432/ligoj ligoj ligoj -
keycloak http://localhost:9083 admin admin z1n7…
jenkins http://localhost:8085 admin cfu_… 11651…
sonarqube http://localhost:9000 admin v1JE… squ_…
…

Restart, stop and start

dev restart restarts every service; pass a service to restart just one:

ligoj dev restart # all services
ligoj dev restart jenkins # one kube-play service -> podman pod restart
ligoj dev restart argocd # one kind service -> kubectl rollout restart

kube-play services are restarted with podman pod restart; harbor/argocd with a kubectl rollout restart of their deployments/statefulsets (the kind node is started first if it was stopped). A service that hasn't been created yet is reported and skipped.

dev stop stops everything to free resources; pass a service to stop just one:

ligoj dev stop # all services
ligoj dev stop gitlab # one kube-play service -> podman pod stop
ligoj dev stop harbor # one kind service -> scale workloads to 0

A kube-play service is stopped with podman pod stop; a single kind service has its workloads scaled to 0. Stopping all also stops the kind node (pausing Harbor + ArgoCD together while keeping their replica counts).

dev start is the inverse — it starts stopped services without the full init reconcile:

ligoj dev start # all services
ligoj dev start sonarqube # one kube-play service -> podman pod start
ligoj dev start harbor # one kind service -> start node + scale workloads to 1

A kube-play service is started with podman pod start; a kind service starts the node (if the whole-cluster stop stopped it) and scales its workloads back to 1. (dev init also brings everything up, additionally re-running the chart upgrades and token/realm steps.)

All of init, start, stop and restart take --wait and stream live progress while waiting for the target state (services up, or down for stop): omit it to wait until done (or Ctrl+C), 0 to return immediately, or N to cap the wait at N seconds — e.g. dev restart --wait 120.

Whole-environment up / down

dev down and dev up power the environment off and on at the podman-machine level rather than service by service:

ligoj dev down # hard stop: stop the podman machine, then quit Podman Desktop
ligoj dev up # start podman + its machine, launch Podman Desktop, then start every service

dev down is a hard stop of the whole environment: it stops the podman machine (a single VM stop takes every service and the kind node down at once) and then quits the Podman Desktop app — which otherwise keeps the machine managed/alive — force-terminating it if it does not quit cleanly.

dev up is the inverse: it starts podman and the machine (installing/creating them if missing, same as init, but without the resource resize), launches Podman Desktop, waits for the machine to be ready, then runs the dev start actions to bring the pods and kind workloads back. dev up takes --wait like start. (Podman Desktop is only touched on macOS, and only when it is installed.)

Configure Ligoj with dev demo

While dev init brings up the backing services, dev demo configures a running Ligoj instance to use them. The Ligoj API can run either as a container or from IntelliJ — the command only needs it reachable at the configured --endpoint (default http://localhost:8080/ligoj).

ligoj dev demo # configure every installed plugin that has a demo
ligoj dev demo --list # just list installed plugins (id, name, version) and exit
ligoj dev demo --only plugin-id-ldap plugin-build-jenkins

It (1) checks Ligoj is up via /manage/health, (2) lists the installed plugins with plugin list, (3) runs the demo registered for each one, then (4) creates the demo projects and their link subscriptions. Connection values (URLs, users, passwords/tokens) are read back from the [dev] credentials section that dev init wrote, so the created nodes point at the local services.

Each plugin's demo lives in its own module under ligojcli/dev_demo/:

Plugin artifactWhat the demo does
plugin-id-ldapUpserts the service:id:ldap:local node (from ligojcli/data/nodes/ldap.local.json, with the live URL / bind DN / password), makes it the primary IAM, restarts the context, then creates the reference OUs, company/group container scopes and technical groups
plugin-build-jenkinsUpserts the service:build:jenkins:local node (url / user / api-token)
plugin-scm-gitlabUpserts the service:scm:gitlab:local node (url / user / auth-key)
plugin-registry-harborUpserts the service:registry:harbor:local node (url / user / password)
plugin-registry-nexusUpserts the service:registry:nexus:local node (from ligojcli/data/nodes/nexus.local.json, with the live url / user / password)
plugin-registry-artifactoryUpserts the service:registry:artifactory:local node (from ligojcli/data/nodes/artifactory.local.json, with the live url / user / password)
plugin-prov-awsUpserts the service:prov:aws:local node from the [dev] AWS credentials — aws_access_key_id, aws_secret_access_key, aws_account_id (all required)
plugin-prov-azureUpserts the service:prov:azure:local node from the [dev] Azure service principal — azure_tenant_id, azure_subscription_id, azure_application_id, azure_client_secret (required) and azure_resource_group (optional)

Each demo is idempotent (nodes are upserted, OUs/scopes/groups skip when they already exist), so re-running is safe. A plugin whose required values are missing (e.g. no Jenkins API token in [dev]) is skipped with a warning instead of aborting the run. Use --wait to bound the LDAP context restart (defaults to 60s).

Demo projects and link subscriptions

After the nodes are configured, the demo also creates three projects — Démo #1 (demo-1), Démo #2 (demo-2) and Démo #3 (demo-3) — owned by the current API user. (Ligoj project keys match ^([a-z]|\d+-?[a-z])[a-z\d\-]*$, so demo:1 is written demo-1.)

Only demo-1 receives subscriptions; demo-2 / demo-3 stay empty. For each active tool node a subscription is created in link mode, which requires the referenced resource to already exist on the remote tool — so the demo first provisions that resource via the tool's own REST API (using the [dev] credentials), then links it. The two provisioning plugins are the exception: they subscribe in create mode (a new, empty quote) — which Ligoj rejects until a price catalog has been imported for that provider, so that step is best-effort and reported when the catalog is missing. The plugins are processed in parallel, one worker per plugin:

PluginResource created on the toolLink parameter(s)
plugin-id-ldapa group demo-1 (Project scope)service:id:group
plugin-build-jenkinsa free-style job demo-1service:build:jenkins:job
plugin-scm-gitlaba project demo-1service:scm:gitlab:repository
plugin-scm-github— (links the public ligoj/plugin-ui repo)service:scm:github:repository
plugin-qa-sonarqubea dedicated ligoj admin user + the empty project org.ligoj.plugin:plugin-uiservice:qa:sonarqube:project
plugin-registry-harbora project demo-1 (docker/OCI only)type = docker, registry
plugin-registry-nexushosted repositories demo-1-docker, demo-1-mavenone subscription per type (type, registry)
plugin-registry-artifactory— (not usable on OSS; subscription skipped)type, registry (Pro only)
plugin-prov-awsan empty provisioning quote on demo-1 (create mode)(none — needs a catalog first)
plugin-prov-azurean empty provisioning quote on demo-1 (create mode)(none — needs a catalog first)

For registry plugins, the demo provisions and subscribes one repository per supported type (currently docker and maven, intersected with what the tool advertises — Harbor is docker-only). The supported types and the parameter definitions are discovered from the node itself (node/<id>/parameter/link), so the demo adapts to each plugin. Anything that cannot be created is skipped with a warning and never aborts the run.

Artifactory OSS cannot be used by the Ligoj registry plugin at all — it needs Pro-only REST APIs. OSS blocks both repository creation (PUT /api/repositories/<key>) and per-repository reads (GET /api/repositories/<key>), and the plugin's artifactory-registry validator relies on the latter — so even a Maven repo you create by hand in the UI cannot be linked, and OSS has no Docker package type at all (Docker is Pro-only, hence greyed out in the UI). Artifactory OSS is therefore only useful here as a plain Maven deploy target (the seed pushes to its default example-repo-local over the non-Pro deploy API). For demo registry subscriptions, use Nexus (docker + maven) and Harbor (docker), which work fully.

The GitHub link points at the real public ligoj/plugin-ui repository, which GitHub validates against its API — so a token is required ([dev]github_token / GITHUB_TOKEN, or the locally authenticated gh CLI). Without one, the GitHub demo is skipped.

The SonarQube plugin authenticates with a login + password that must have admin rights, so the demo — using the sonar_api_token from dev init — provisions a dedicated ligoj admin user (password sonar_demo_password, default Ligoj-Demo-Pass1!; SonarQube requires ≥ 12 characters with upper/lower/digit/special) rather than reusing the admin account, then links the org.ligoj.plugin:plugin-ui project. That Sonar project is created empty up front (link mode needs it to exist); the seed phase's sonar:sonar analysis fills it in afterwards. Without a token, the SonarQube demo is skipped.

Every subscription is created idempotently and its status is refreshed (validated) right after linking.

Tool data seeding

Finally, dev demo fills the tools with real data so the demo project has something to show. This step is heavy (image pulls, two Maven builds, a Sonar analysis and two git mirrors — several minutes) and runs on a full dev demo (it is skipped when you pass --only). The tools are seeded in parallel, best-effort — a failing tool logs a warning and never aborts the rest:

ToolData seeded
Harborpulls 2 small public images (busybox, alpine) and pushes them into the demo-1 project
Nexus (docker)pushes the same 2 images to the Nexus docker registry connector (port 8182, see --nexus-docker-port)
Nexus / Artifactory (maven)builds plugin-ui and plugin-id and deploys the artifacts to demo-1-maven (Nexus) and to example-repo-local (Artifactory OSS's default repo)
SonarQuberuns mvn … sonar:sonar on both plugins (project keys org.ligoj.plugin:plugin-ui / plugin-id)
GitLabmirrors the github.com/ligoj/plugin-ui and plugin-id repositories

Prerequisites: podman, mvn (JDK 21), git, network access to Docker Hub / GitHub, and the plugin sources under LIGOJ_PLUGINS_DIR (default ~/git/ligoj-plugins). Nexus Community Edition requires its EULA (accepted automatically by dev init) and a docker connector port; Artifactory OSS has no docker registry, so it only receives the Maven artifacts.

Keycloak realm, federation and client

For the keycloak service, dev init drives the Keycloak Admin REST API to create (idempotently):

  • the ligoj realm;
  • an LDAP user federation bound to the OpenLDAP pod — reachable from inside the Keycloak pod through ldap://host.containers.internal:<ldap-port> — using cn=<ldap_admin_user>,<ldap_root> (READ_ONLY, users DN <ldap_root>, inetOrgPerson, subtree);
  • a confidential ligoj OIDC client (standard flow, redirect URIs for the UI on :5173 and the API on :8080) whose generated secret is stored in [dev] keycloak_client_secret.

It then prints ready-to-paste Spring Boot properties:

security=OAuth2Bff
ligoj.security.oauth2.username-attribute = email
ligoj.security.login.url = /oauth2/authorization/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:9083/realms/ligoj
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.client-id=ligoj
spring.security.oauth2.client.registration.keycloak.client-secret=<generated>
spring.security.oauth2.client.registration.keycloak.scope=openid

Debug the Ligoj apps from the IDE (dev debug) — macOS

While dev init brings up the backing services, dev debug drives the local application stack you actually debug: IntelliJ IDEA plus the two Ligoj Spring Boot apps and the Vite dev server.

ComponentStarted by dev debugEndpoint / path
PostgreSQLthe ligoj-db pod (podman pod start, machine too)localhost:5432
OpenLDAPthe openldap pod (podman pod start)localhost:1389
IntelliJ IDEAopen -a "IntelliJ IDEA" <project> (if stopped)~/git/ligoj
ligoj-apithe dedicated launcher app, in Debug modehttp://localhost:8081/ligoj-api
ligoj-uithe dedicated launcher app, in Debug modehttp://localhost:8080/ligoj
Vite (app-ui)npm run dev in app-ui/src/main/webapphttp://localhost:5173/ligoj/
ligoj dev debug init # compile the dedicated launcher app (one-time; re-run after renaming a config)
ligoj dev debug start # start the ligoj-db + openldap pods + IntelliJ + API/UI/Vite (only those stopped)
ligoj dev debug status # show what is running (process) and reachable (port), no changes
ligoj dev debug stop # stop the API/UI/Vite apps AND the two pods (IntelliJ stays open)
ligoj dev debug restart # stop then start the apps (the pods are left running)
ligoj dev debug start -w 60 # same live '--wait' as the other dev commands (0 = no wait)

Backing services first.start brings the dev PostgreSQL (ligoj-db pod) and OpenLDAP (openldap pod) up before anything else — ligoj-api cannot boot without the DB, and the LDAP identity backend should be there too — starting the podman machine when it is down. A service already answering on its port is a no-op; a pod that was never created only warns (run ligoj dev init --only <service>) so the IDE stack still launches. Symmetrically, dev debug stop also stops both pods (skipped quietly when podman itself is down); restart only bounces the apps and leaves the pods running.

Why init / the launcher app. IntelliJ has no headless "run this configuration" command, and scripting its UI needs the broad macOS Accessibility permission (control any app + read the screen). Instead of granting that to your whole terminal, dev debug init compiles a tiny dedicated app (default ~/Applications/Ligoj Debug.app) that drives IntelliJ's Run ▶ Debug… chooser for ligoj-api / ligoj-ui (skipping any already running). You grant Accessibility to that app only — the first dev debug start triggers the macOS prompt — and can then revoke your terminal's grant. When dev debug start launches a cold IntelliJ, the launcher first waits (up to 180 s) for the IDE to become UI-ready — its Run menu populated, i.e. the project frame is up — before sending any keystroke, so a not-yet-started IDE no longer drops the Debug commands. Because that logic is baked into the compiled app, dev debug start warns and asks you to re-run dev debug init whenever the installed launcher predates this behavior (also re-run it after renaming a run config).

Everything else needs no permission: all four components are detected by process (org.ligoj.boot.api.Application / …web.Application, the project's vite process) and by TCP port, so status/stop/restart cover them however they were started. stop (and the stop half of restart) terminates the API/UI processes even though the IDE launched them. Overridable via [dev]/env: LIGOJ_PROJECT_DIR·ligoj_project_dir, IDEA_APP·idea_app, LIGOJ_DEBUG_APP·ligoj_debug_app (launcher app location).

The Ligoj API dev app binds 8081, so the nexus service publishes 8181 (not its own 8081) to avoid the clash — run both at once without conflict.

Scaffold a new plugin (dev plugin create)

dev plugin create <plugin> generates a brand-new Ligoj plugin project in the current directory. The <plugin> is the full Maven artifact and must start with plugin-; its fragments decide the type, exactly like the real plugins:

  • one fragment → a service plugin, e.g. plugin-km (like plugin-id)
  • two+ fragments → a tool plugin, e.g. plugin-km-confluence (like plugin-id-ldap), which extends the service plugin-<first-fragment> via a provided Maven dependency.

You're prompted for a display name and description (or pass --name / --description):

ligoj dev plugin create plugin-km # a service plugin
ligoj dev plugin create plugin-km-confluence # a tool plugin of plugin-km
ligoj dev plugin create plugin-foo --name "Ligoj - Plugin Foo" --description "Foo service."

It generates a complete, buildable project (mvn verify compiles, tests, and builds the Vue bundle), with 100% coverage of the generated code:

AreaFiles
Mavenpom.xml (correct parent + coordinates; a tool adds the provided parent-service dep)
Javathe AbstractServicePlugin / AbstractToolPluginResource skeleton (+ the <Service>ServicePlugin interface for a service), package org.ligoj.app.plugin.<fragment>
Java testplain JUnit 5 asserting getKey()
Vue UI (ui/)index.js + service.js (@ligoj/host integration), i18n en/fr, package.json (+ generated package-lock.json), vite.config.js, eslint.config.js, and a Vitest test mocking @ligoj/host
Resourcescsv/node.csv (+ csv/parameter.csv for a tool)
ProjectREADME.md, LICENSE (MIT), .gitignore, .codeclimate.yml, GitHub SonarCloud workflow

Options: --name, --description, --dir (parent directory, default the current one). The generated vite.config.js / Vitest expect the Ligoj UI host as a sibling checkout at ../../../ligoj (same convention as the existing plugins), and npm is used once at creation to produce the lockfile the Maven frontend build (npm ci) requires. After creation: cd <plugin> && mvn verify.

Build plugin frontends (dev plugin build)

Each Ligoj plugin ships a frontend under <plugin>/ui/ built with npm run build (Vite). dev plugin build runs that build for every live plugin — the ones installed in the running Ligoj instance (system/plugin) that also have a local <plugin>/ui/ under the plugins directory:

# Rebuild the frontend of every live plugin (in parallel)
ligoj dev plugin build
# Build EVERY locally checked-out plugin that has a ui/ (no Ligoj instance needed)
ligoj dev plugin build --all
# Build only specific plugins (also skips the live lookup)
ligoj dev plugin build --only plugin-ui plugin-id
# Limit parallelism
ligoj dev plugin build --jobs 2

Two flags select what to build, and both work offline, straight from the working copies — like dev plugin renovate --all, they never query the Ligoj API:

  • --all / -A — every directory under the plugins dir that has a ui/package.json. Because it reads the checkouts rather than the running instance, it also covers plugins that are not (or not yet) installed in Ligoj.
  • --only / -O — an explicit artifact list.

They are mutually exclusive. With neither, the set comes from the running Ligoj (system/plugin), so the instance must be reachable — the error names both escapes if it isn't.

Dependencies are installed automatically on first build (npm ci when a package-lock.json is present, otherwise npm install) before npm run build. Builds run in parallel (default min(4, CPUs), --jobs to change) and each plugin is reported OK / FAILED independently — one failing frontend never aborts the others. The plugins directory is LIGOJ_PLUGINS_DIR / [dev] ligoj_plugins_dir (default ~/git/ligoj-plugins), and npm must be on the PATH.

Renovate a plugin's dependencies (dev plugin renovate)

dev plugin renovate updates a plugin's dependency descriptors, ported from the renovate mode of commands/release.sh but scoped to editing the files — it does not commit, leaving the edits in the working tree for you to review:

  • pom.xml — bumps the org.ligoj.api:plugin-parent<version> to the target (default: the latest local org.ligoj.api:parent release; override with --parent-version). A current version newer than the target is left alone; the project's own <version> is never touched.
  • package.json — re-pins only the npm dependency constraints that are also declared by the host UI to the host's versions (none added/removed; scripts and non-shared deps untouched).
  • package-lock.json — regenerated from package.json (npm install --package-lock-only) so the plugin's npm ci stays in sync.
# Renovate the plugin in the current directory
ligoj dev plugin renovate
# A specific plugin (artifact under LIGOJ_PLUGINS_DIR, or a path)
ligoj dev plugin renovate plugin-km
# Every plugin under LIGOJ_PLUGINS_DIR
ligoj dev plugin renovate --all

The host UI referential is LIGOJ_HOST_PACKAGE_JSON (default ~/git/ligoj/app-ui/src/main/webapp/package.json, override with --host-package-json), the plugins root is LIGOJ_PLUGINS_DIR / --plugins-dir (default ~/git/ligoj-plugins), and npm must be on the PATH. A plugin must be an org.ligoj.api:plugin-parent project — anything else is skipped (with --all) or reported as an error (when named).

Build the app container images (dev package)

dev package builds the two Ligoj application container images locally, straight from the app-api/ and app-ui/ Dockerfiles with podman (or docker) — no external script. It produces exactly the images dev test start runs, and pushes nothing:

ligoj dev package # build ligoj-api + ligoj-ui for the native arch (fast)
ligoj dev package --only api # build just one image
ligoj dev package --tag 4.0.2-rc1 # override the image tag
ligoj dev package --platform all # multi-arch manifest (linux/amd64 + linux/arm64, podman)

Every option resolves from the flag, then the environment, then [dev]:

OptionDefaultEnv / [dev] key
--project DIR (Ligoj checkout)~/git/ligojLIGOJ_DIR / ligoj_dir
--tag TAGproject <version> without -SNAPSHOTLIGOJ_PACKAGE_TAG / ligoj_package_tag
--only api|uiboth
--platform (single arch, comma list, or all)host native arch
--runtime docker|podmandocker if present, else podmanLIGOJ_TEST_RUNTIME / ligoj_test_runtime

The API image bundles all three JDBC drivers (db-postgresql, db-mysql, db-mariadb): the build passes --build-arg MAVEN_PROFILES=… to force those Maven profiles. They are activeByDefault in app-api/pom.xml, but Maven silently disables every default profile as soon as a settings.xml<activeProfiles> (or any -P) is in play — which had dropped all drivers from the war and made the API fail at boot with ClassNotFoundException: org.postgresql.Driver. Forcing them makes the image DB-capable regardless of the build's settings.xml. ~/.ligoj/plugin-vendors.p12 is bundled into the API image when present (password from PLUGIN_VENDORS_STOREPASS, the ligoj.release.vendors-storepass keychain entry, or changeit).

Only the host arch is built by default — much faster than a QEMU-emulated multi-arch build, and all dev test needs. This builds local images only; for a full release (deploy, tags, Docker Hub) use the release helper (commands/release.sh).

Test the released app containers (dev test)

While dev debug runs the apps from your IDE, dev test runs the released Docker images (ligoj/ligoj-api + ligoj/ligoj-ui) against the local dev stack — the quickest way to smoke-test a published build. It starts both containers in the background, waits until both are healthy, then opens the UI in your browser at http://localhost:<ui-port><context>/ (default context /ligoj):

ligoj dev test start # run both, wait for health, open the browser
ligoj dev test stop # stop and remove both containers
ligoj dev test start --tag 4.0.2-SNAPSHOT-101 --port 8089 --api-port 8088
ligoj dev test start --context /ligoj2 # serve the UI under /ligoj2 (CONTEXT_URL; '/' = root)
ligoj dev test start --no-browser --no-wait # start detached, don't wait or open the browser
ligoj dev test -h # full option + '-D' reference (mirrors ligoj/DOC.md)# Free-form JVM options, grouped per container with --api / --ui:
ligoj dev test start \
--api -Dlog.level=INFO -Dligoj.sslVerify=false \
--ui -Dsecurity=Trusted -Dlog.level=info

The containers mount LIGOJ_HOME (default ~/.ligoj, override with --home / LIGOJ_HOME / [dev] ligoj_home) at /home/ligoj, with hooks/ and files/ subdirectories — this replaces the /var/lib/ligoj of the DOC.md sample commands with your user home. Under podman they run as --user 0 so the app can write that host-owned mount. The UI's ENDPOINT points at the API on --api-port.

Database: by default the API is wired to the dev-stack PostgreSQL from [dev] (-Djdbc.vendor=postgresql + host/port/db/user/password + the PG dialect), so it targets the dev DB out of the box. The API image must bundle the PostgreSQL JDBC driver (org.postgresql.Driver) or it fails at boot with ClassNotFoundException — the released ligoj-api image defaults to MySQL only, so add the driver to your build. Pass an explicit --api … group to take full control of the DB options.

Networking adapts to the runtime: docker/Linux uses --network=host (the API reaches the dev DB on localhost); podman-machine publishes ports (-p <port>:<port>) so the mac can reach localhost:<port>, and the containers reach the machine host — the other container and the dev DB — via host.containers.internal. Override with --net host|publish.

Every option resolves from the CLI flag, then the environment, then ~/.ligoj/config / ~/.ligoj/credentials:

OptionDefaultEnv / [dev] key
--port (UI port, also the browser port)8089LIGOJ_UI_PORT / ligoj_ui_port
--api-port (UI ENDPOINT + API exposed port)8088LIGOJ_API_PORT / ligoj_api_port
--context (UI context path, the image's CONTEXT_URL)/ligojLIGOJ_UI_CONTEXT / ligoj_ui_context
--home (LIGOJ_HOME)~/.ligojLIGOJ_HOME / ligoj_home
--tag / --api-tag / --ui-tagnewest local build, else latest publishedLIGOJ_TEST_TAG / ligoj_test_tag
--runtimedocker if present, else podmanLIGOJ_TEST_RUNTIME / ligoj_test_runtime
--net (host | publish)publish for podman, else hostLIGOJ_TEST_NETWORK / ligoj_test_network
--api / --ui-D… (replace the defaults)see belowLIGOJ_TEST_API_OPTS / ligoj_test_api_opts
--wait N / --no-wait / --no-browser / --pullwait 300s, open browser

The --api / --ui groups take any number of -D… options (each replaces that container's defaults). Defaults: API --enable-preview -Dlog.level=INFO <single-connection LDAP pool> -Dligoj.sslVerify=false; UI -Dsecurity=Trusted -Dlog.level=info. Some properties have well-known values (from DOC.md's Application level properties): UI -Dsecurity=Trusted|Rest|OAuth2Bff, both -Dlog.level=trace|debug|info|warn|error and -Dlogging.level.<category>=<level>, API -Djdbc.vendor=mysql|postgresql|mariadb, -Djpa.hbm2ddl=update|none|validate, -Dligoj.plugin.repository=central|nexus. Run ligoj dev test -h for the full annotated list.

-Dsecurity=Trusted (the UI default here) runs Ligoj without password verification (RBAC still enforced) — convenient for local testing, never for a publicly reachable instance.

Back up & restore a service's data (dev backup)

dev backup snapshots, lists and reloads the database rows owned by a Ligoj service, using the PostgreSQL client tools (psql / pg_dump, from brew install libpq). Only service:prov is supported today (its catalog is expensive to re-import). Everything lives under one command:

CommandWhat it does
dev backuptake the snapshot (no sub-command)
dev backup listlist the snapshots stored locally
dev backup restorereload a snapshot into the target DB

The services to snapshot are selected with --services / -S, which takes a list — space- or comma-separated, with the service: prefix optional. Omit it to act on every supported service.

ligoj dev backup # every supported service
ligoj dev backup --services service:prov # -> ~/.ligoj/backup/prov-<timestamp>/
ligoj dev backup -S prov # same, short form
ligoj dev backup list # list what is stored locally
ligoj dev backup restore # list backups, pick one interactively
ligoj dev backup restore --service service:prov # same, explicit service
ligoj dev backup restore prov-20260706-232820 # restore a specific backup

Because --services takes a list, write the sub-command firstdev backup list --services prov, not dev backup --services prov list (the latter would read list as a service name; the CLI says so and names the fix).

dev backup restore uses the singular --service / -S (same value forms — prov or service:prov), because a restore targets exactly one backup id; its only positional is the optional backup_id. Omit it to pick one from an interactive list.

Listing backups (dev backup list)

dev backup list lists the snapshots under ~/.ligoj/backup, newest first, in three renderings selected with --format / -F. It takes the same --services / -S filter as dev backup:

ligoj dev backup list # table (default), all services
ligoj dev backup list -F text # tab-separated rows, for cut/awk/grep
ligoj dev backup list -F json # full records, for jq
ligoj dev backup list --services service:prov # restrict to one (or more) services
ID CREATED SERVICE SUBS TABLES ROWS SIZE
-------------------- ---------------- ------------ ---- ------ -------- -------
prov-20260727-105521 2026-07-27 10:57 service:prov 13 34 24360846 580.0MB
prov-20260706-232820 2026-07-06 23:28 service:prov 12 29 2126023 116.8MB
[INFO ] [backups] 2 backup(s), 813.6MB on disk in /Users/me/.ligoj/backup

SUBS is the number of backed-up subscriptions, TABLES the number of ligoj_prov_* tables the snapshot holds (a useful tell that a backup predates a plugin-prov schema change), ROWS their total row count and SIZE the compressed dump. A snapshot whose prov-data.sql.gz is missing is flagged INCOMPLETE and called out as not restorable, rather than failing later during a restore.

The text mode emits the same columns tab-separated (ligoj dev backup list -F text | cut -f1 gives just the ids); json adds the raw dump_bytes / disk_bytes, duration_seconds, source_db, path and complete fields, and prints [] rather than an error when there is nothing stored. When --format is omitted, an explicit global -o/--output is honoured — so ligoj -o json dev backup list works like the rest of the CLI — and table is now accepted by -o too.

What a service:prov backup captures. Every ligoj_prov_* table (the whole catalog + quotes), dumped in bulk with pg_dump — the price tables reach tens of millions of rows, so their content is stored compressed and restored verbatim. The table set is globbed from the catalog, never hard-coded, so tables a newer plugin-prov adds are captured automatically. Alongside, the cross-referenced core rows that keep the quotes valid are exported as CSV: the ligoj_subscription rows on prov nodes (referenced by ligoj_prov_quote and the other subscription-scoped prov tables), their ligoj_node rows, the ligoj_parameter_value rows of those nodes/subscriptions, and the ligoj_project rows behind them. A metadata.json records the id, timestamp, and per-table row counts; the whole thing lands under ~/.ligoj/backup/<id>/. Progress, per-table stats, and a duration summary are printed throughout.

Restore is an id-aware reload (the target is a live Ligoj DB with its own ids), run as a single transaction (all-or-nothing):

  1. drop the prov foreign keys, empty the ligoj_prov_* tables, and delete the current prov subscriptions / instance nodes / parameter values (+ their transient status events);
  2. projects are matched by pkey or name and reused (their id is remapped into the restored rows), else inserted; nodes are inserted only when missing (string ids);
  3. subscriptions are inserted with fresh ids (the backup's ids may already be taken by unrelated subscriptions in the target) and their project remapped — the subscription-scoped parameter values that reference them are repointed to the new ids; a node's parameter values are dropped then re-inserted;
  4. the ligoj_prov_* tables are bulk-reloaded verbatim, then every prov column with a foreign key to ligoj_subscription is repointed at the new ids. That list is discovered from the catalog, not hard-coded — today it covers quote, quote_view, quote_snapshot, comparison and lookup_error (the last two through both a subscription and a main_subscription column), and it picks up whatever plugin-prov adds next. This matters because the foreign keys are re-added NOT VALID (instant — no rescan of the huge tables), so a missed column would leave rows silently pointing at unrelated subscriptions instead of raising an error. Finally every touched sequence is bumped to max(id) + allocation-size so Hibernate's next id block can't collide.

The restore log shows only the five phases, each with its own duration (the noisy per-statement ALTER / DELETE / COPY output is suppressed); a >> [n/5] … done (Xs) line marks each phase.

Restore is resilient to a Postgres / schema mismatch between the backup and the target — e.g. a backup taken against a newer server restored into an older one:

  • the version-specific session settings pg_dump writes in its header (such as PG 17's SET transaction_timeout) are stripped from the bulk dump before load, so an older server doesn't abort on an unknown parameter;
  • the core rows are loaded by column name (from each CSV header, intersected with the target table), not by position — so a differing column order or an extra/missing column can't shift a value into the wrong column;
  • the bulk prov COPY blocks are aligned to the target's columns the same way: a column the target lacks (e.g. a field a newer prov plugin added) is dropped from the block's header and every data row (a WARN reports it — that column's data is lost), a table the target doesn't have is skipped, and any target column the backup lacks keeps its default;
  • the prov UNIQUE constraints are dropped for the load (like the FKs) and each is re-added only if the reloaded rows satisfy it; one the backup's older data violates (e.g. two same-named storages in one quote, which a newer model now forbids) is left dropped with a WARNING naming it, so the restore still completes — dedup and re-add it by hand if you need it.

Without an id, restore lists the available backups (id, creation time, subscription count, prov row count, size) for keyboard selection. The target database comes from the active --profile's section when it carries db_* keys, else [dev] for backup and [restore] for restore — so a typical restore-into-another-DB is ligoj --profile restore dev backup restore --service service:prov. Each section needs db_host / db_port / db_name / db_user / db_password, and the target must be a real Ligoj DB with the provisioning plugin installed (its parameter definitions and provider nodes must already exist).

Harbor and ArgoCD on kind

Harbor and ArgoCD are the services that need a real cluster. They share a single-node kind clusterligoj-dev (podman provider), created on first use with the host→NodePort mappings for both baked in (Harbor 8088→30088, ArgoCD 8083→30083) — mappings are immutable after creation, so the cluster carries all of them up front. Each service is then helm upgrade --installed.

dev init --only harbor installs the Harbor chart in namespace harbor — a minimal install (trivy and metrics disabled), TLS off, externalURL=http://localhost:8088; stores harbor_endpoint and harbor_admin_password.

Apple Silicon (arm64): Harbor's official images are amd64-only. Most run under emulation, but goharbor/redis-photon segfaults under QEMU, so dev init swaps the internal cache for a multi-arch redis image (HARBOR_REDIS_IMAGE·harbor_redis_image, default docker.io/redis:7.4.1). The remaining components run amd64-under-QEMU (functional, slower).

dev init --only argocd installs the ArgoCD chart in namespace argocd (insecure/HTTP, NodePort), then:

  1. configures a Dex LDAP connector to the OpenLDAP pod — reachable from inside kind via the podman host IP (resolved from host.containers.internal on the node), bind cn=<ldap_admin_user>,<ldap_root>;
  2. creates a role:ligoj RBAC role (apps/projects/clusters/repos) bound to a ligoj account (apiKey, login) and the ligoj LDAP group;
  3. reads the admin password from argocd-initial-admin-secret and generates an API token for the ligoj account — both stored in [dev].

Adding ArgoCD to a cluster that only had Harbor (or vice-versa) requires --recreate once, since the new service's port mapping must be baked into the cluster. After that both coexist.

After a podman machine restart the kube-play pods and the kind node are left stopped; just re-run dev init (or dev init --only <kind-service>) — it restarts the pods, starts the kind node and refreshes its kubeconfig automatically.

Tear the cluster down with kind delete cluster --name ligoj-dev (or dev init --only harbor argocd --recreate to rebuild it).

Bootstrap

The following commands can be executed to perform several API commands following a complex workflow.

Most bootstrap arguments like --jenkins-endpoint, corresponding configuration file option such as jenkins_endpoint is accepted, and environment variable JENKINS_ENDPOINT too.

Bootstrap init

Initialize Ligoj with basic group management, containers, and companies hierarchy.

Sample Docker command:

ligoj bootstrap init --base-dn="dc=sample,dc=com"

Note--base-dn argument can also be defined as ligoj_ldap_base_dn in configuration file and LIGOJ_LDAP_BASE_DN environment variable.

Hierarchy tree sample for base DN dc=sample,dc=com

DN LDAPScope nameScope type
ou=peopleUnassignedcompany
ou=technical-users,ou=peopleTechnicalcompany
ou=external,ou=peopleExternalcompany
ou=groupsUnassignedgroup
ou=project,ou=groupsProjectgroup
ou=tools,ou=groupsTechnicalgroup
cn=jenkins-administrators,ou=tools,ou=groups(inherited)group
cn=nexus-administrators,ou=tools,ou=groups(inherited)group
cn=nexus-administrators-paris,cn=nexus-administrators,ou=tools,ou=groups(inherited)group
cn=nexus-administrators-paris-8,cn=nexus-administrators,ou=tools,ou=groups(inherited)group

Via ligoj bootstrap init

ligoj bootstrap init --base-dn="dc=sample,dc=com" --users-base-dn "ou=people" --internal-users-base-dn "" --technical-users-base-dn "ou=technical-users" --external-users-base-dn "ou=external" --groups-base-dn "ou=groups" --technical-groups-base-dn "ou=tools" --projects-base-dn "ou=project" --technical-groups "sonar-administrators""jenkins-administrators""nexus-administrators"

Via ligoj id commands

## Users and companies### OU LDAP intermediate
ligoj id:ou create --name "people" --parent-dn "dc=sample,dc=com"
ligoj id:ou create --name "external" --parent-dn "ou=people,dc=sample,dc=com"
ligoj id:ou create --name "technical-users" --parent-dn "ou=people,dc=sample,dc=com"### Companies
ligoj id:scope create --name "Unassigned" --type "company" --dn "ou=people,dc=sample,dc=com"
ligoj id:scope create --name "External" --type "company" --dn "ou=external,ou=people,dc=sample,dc=com"
ligoj id:scope create --name "Technical" --type "company" --dn "ou=technical-users,ou=people,dc=sample,dc=com"## Groups### OU LDAP intermediate
ligoj id:ou create --name "groups" --parent-dn "dc=sample,dc=com"
ligoj id:ou create --name "projects" --parent-dn "ou=groups,dc=sample,dc=com"
ligoj id:ou create --name "tools" --parent-dn "ou=groups,dc=sample,dc=com"### Scope functionals for groups
ligoj id:scope create --name "Unassigned" --type "group" --dn "ou=groups,dc=sample,dc=com"
ligoj id:scope create --name "Project" --type "group" --dn "ou=project,ou=groups,dc=sample,dc=com"
ligoj id:scope create --name "Technical" --type "group" --dn "ou=tools,ou=groups,dc=sample,dc=com"### Technical groups and sub-groups
ligoj id:group create --name "jenkins-administrators" --scope "Technical"
ligoj id:group create --name "nexus-administrators" --scope "Technical"
ligoj id:group create --name "nexus-administrators-paris" --scope "Technical" --parent "nexus-administrators"
ligoj id:group create --name "nexus-administrators-paris-8" --scope "Technical" --parent "nexus-administrators-paris"

Bootstrap welcome-user

Configure a new project and its administrator.

Sequence

ligoj bootstrap welcome-user --id jdupont --project project1 --name "Project 1" --group-suffix="-team"

Optionally, the project key can be validated with DNS within a defined DNS zone :

ligoj bootstrap welcome-user --id jdupont --project project1 --name "PIProject 1" --verify-project-with-dns "PROJECT_KEY.holder.kloudy.io,PROJECT_KEY.holder2.kloudy.io" --group-suffix="-team"
{
"admin_user": "jdupont", "script_user": "project1-script", "script_api_key": "...", "reader_user": "project1-reader", "reader_password": "...", "project_key": "project1", "project_id": 6552
}

Optionally, Ligoj nodes such as Jenkins and SonarQube can be created during this step.

ligoj bootstrap welcome-user --id jdupont --project project1 --name "Project 1" --verify-project-with-dns "PROJECT_KEY.holder.kloudy.io,PROJECT_KEY.holder2.kloudy.io" --group-suffix="-team" --jenkins-create-node --jenkins-endpoint http://localhost:8086 --jenkins-api-token="" --sonar-create-node --sonar-endpoint http://localhost:9000 --sonar-api-token="" --reset-reader-password 

Notes

  • reader_password result is provided only for new user and cannot be retrieved by Ligoj.
  • To generate another password, use the --reset-reader-password flag.
  • reader_password (or reset password) is used to create API tokens saved in Ligoj nodes
  • --jenkins-api-token and --sonar-api-token can be provided with this command but should be related to the reader_user or --jenkins-api-user value.
  • All endpoints and tokens are also sourced from configuration file and environment variables.

Bootstrap create-project

Create new groups within a new project related to another one.

Considering this use case :

  • Create a new project having project-a as key and Project A as name.
  • Team leader (administrator) will be cli100.name@sample.com. Actual username is resolved automatically from email.
  • Initial groups within this project are admin, dev and test.
  • The parent project's key (used as context) is project1. This project must be managed by the user: ligoj-user

The corresponding command is:

ligoj bootstrap create-project --project project-a --name "Project A" --groups "admin""dev""test" \
--parent-project "project1" \
--parent-admin "ligoj-user" \
--team-leader cli100.name@sample.com \

Note When parent-admin is provided, this operation exploits the run-as feature of Ligoj to check the administrator of parent-project. In such a case, the session user must be a system administrator.

Bootstrap delete-project

Delete a project including all groups, not only the references.

ligoj bootstrap delete-project --project project-a --parent-admin "ligoj-user"

Note When parent-admin is provided, this operation exploits the run-as feature of Ligoj to check the administrator of parent-project. In such a case, the session user must be a system administrator.

Bootstrap create-roles

Create mapped roles in various tools

Supported services are:

  • Jenkins
  • SonarQube
  • Nexus
  • Alfresco
  • GitLab

Sequence

Created contents by tools

ToolContent typeNote
JenkinsFolder RBAC PermissionsSee CasC notes
JenkinsGlobal RBAC PermissionsSee CasC notes
JenkinsFolder credentialsSee supported credentials
JenkinsGlobal credentialsSee supported credentials
JenkinsFoldersNested folders structure supported
SonarQubeGroups and RBAC
SonarQubeProjects
SonarQubeTemplates
GitLabWrapper project Groups
GitLabLDAP project Groups
GitLabProject Groups
Sonatype NexusRoles
Sonatype NexusRepositories
AlfrescoRoles
AlfrescoSites
ArgoCDPermissionsOptional permission=deny and application scope
ArgoCDProjects
HarborProjects
HarborProjects members

Group and role configuration JSON file conf.json. See --from for JSON loading options

ligoj bootstrap create-roles --project project-a --from conf.json \
--argocd-token="$ARGOCD_TOKEN" \
--argocd-user="$ARGOCD_USER" \
--alfresco-endpoint="$ALFRESCO_ENDPOINT" \
--alfresco-user="$ALFRESCO_USER" \
--alfresco-password="$ALFRESCO_PASSWORD" \
--nexus-endpoint="$NEXUS_ENDPOINT" \
--nexus-user="$NEXUS_USER" \
--nexus-password="$NEXUS_PASSWORD" \
--gitlab-endpoint="$GITLAB_ENDPOINT" \
--gitlab-token="$GITLAB_TOKEN" \
--jenkins-home="$JENKINS_HOME" \
--jenkins-endpoint="$JENKINS_ENDPOINT" \
--sonar-endpoint="$SONAR_ENDPOINT" \
--sonar-api-token="$SONAR_API_KEY" \
--harbor-endpoint="$HARBOR_ENDPOINT" \
--harbor-user="$HARBOR_USER" \
--harbor-password="$HARBOR_PASSWORD"
ligoj bootstrap create-roles --project project-a --from "https://path/to/conf.json"
ligoj bootstrap create-roles --project project-a --from '[{"text": "organizationalUnit","parameter": "service:id:ldap:companies-class"},...]'

For detailed tool specific options, execute usage command:

ligoj bootstrap create-roles --help
usage: Ligoj CLI bootstrap create-roles [-h] [--project PROJECT] [--group-suffix GROUP_SUFFIX] [--groups [GROUPS ...]] [--from FROM] [--schema SCHEMA] [--alfresco-endpoint ALFRESCO_ENDPOINT] [--alfresco-user ALFRESCO_USER] [--alfresco-password ALFRESCO_PASSWORD]
[--alfresco-ticket ALFRESCO_TICKET] [--gitlab-endpoint GITLAB_ENDPOINT] [--gitlab-token GITLAB_TOKEN] [--gitlab-base-group GITLAB_BASE_GROUP]
[--gitlab-project-group-prefix GITLAB_PROJECT_GROUP_PREFIX] [--gitlab-project-subgroup-prefix GITLAB_PROJECT_SUBGROUP_PREFIX]
[--jenkins-home JENKINS_HOME] [--jenkins-crumb JENKINS_CRUMB] [--jenkins-endpoint JENKINS_ENDPOINT]
[--jenkins-api-user JENKINS_API_USER] [--jenkins-api-token JENKINS_API_TOKEN]
...
options:
-h, --help show this help message and exit
--project PROJECT, -p PROJECT
Associated project key
...

--includes and --excludes options

Each supported tool can be included or excluded from the bootstrap commands:

  • By default, all discovered JSON's content is considered, no exclusion. Implicit --includes "*".
  • Special value * means all.
  • Multiple includes and excludes values can be provided
  • The excludes option has higher priority than includes.
  • When the resolved endpoint is empty or null, the tool is ignored.

Constraints

Checked constraints:

  • Referenced groups must be defined at root level. This constraint ensures a correct definition and avoids typos.
  • Empty permissions set are not allowed.
  • Given JSON must validate the JSON Schemadocument schema.json.
  • JSON Schema can be merged with custom additions: --schema "JSON Schema string, file or URL".

Sample constraint limiting Alfresco sites to 1: --schema='{"properties":{"alfresco":{"properties":{"sites":{"maxItems": 1}}}}}'

Jenkins

Supported resources are:

  • Nested folders
  • Credentials with or without values
  • Roles, group mapping and permissions, at folder or global level

Configuration

ParameterEnvironment variableNoteDefault
--jenkins-endpointJENKINS_ENDPOINTHTTPS endpointCurrent Jenkins
--jenkins-api-userJENKINS_API_USERUsername
--jenkins-api-tokenJENKINS_API_TOKENToken generated from /user/_me_/configure
Sourced from Jenkins credential JENKINS_API.
--jenkins-homeJENKINS_HOMEJENKINS home location for CasC update
--jenkins-crumbJENKINS_CRUMBCrumb protection enablementauto

Folders

  • Recursive folders are supported; however, each folder must be unique. This is not an implementation limit, but it makes folder reorganization possible.
  • Folder maximal depth is 4, but it is not a hard limit
  • Folder names are encoded, special chars are supported
  • Supported folder types are com.cloudbees.hudson.plugins.folder.Folder and jenkins.branch.OrganizationFolder. Folder mode update is not supported. Other folder modes might work, but have not been tested.
  • There is no permission limitation; internal identifiers must be used, such as hudson.model.Item.Build, hudson.model.Hudson.Administer, etc.:

Credentials

There is no limitation for credentials type, the supported configuration is:

  • Any parameter type but file
  • Tested types are:
    • com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
    • org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
    • com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
  • Supported scope values are global and system (case insensitive). When not provided, the default behavior of this credential type is applied.
  • Attached domain is always All domains: _

Security and Configuration as Code

Requires the installed, configured and assigned Jenkins plugin Role-based Authorization Strategy. Underlying role management API is executed.

CaC (Configuration as Code) YAML file management is supported:

  • CaC file location override support is JENKINS_CASC_FILE variable, then CASC_JENKINS_CONFIG variable, then $JENKINS_HOME/jenkins.yaml as default.
  • Only update mode is supported after all API calls, not creation
  • Both current and in-memory CaC files YAML structure must contain this path: jenkins.authorizationStrategy.roleBased.roles
  • Both current and in-memory CaC files YAML structure must contain this path: jenkins.securityRealm.ldap.configurations
  • The script must be able to write the backup file $path_to_cac_file.ligoj, which will be overwritten.
  • A unified diff is computed and printed for current and in-memory CaC files
  • Backup and update are executed only if there is at least one change detected in the computed unified diff
  • By default, the related sub-folder access is granted from the parent folder, and the related pattern is suffixed with the (/.*)? expression. Set recursive to false to block this behavior.
  • Roles use the folder identifier, case is insensitive
securityRealm:
ldap:
configurations:
- displayNameAttributeName: "cn"groupMembershipStrategy:
fromGroupSearch:
filter: "(| (member={0}) (uniqueMember={0}) (memberUid={1}))"groupSearchBase: "ou=groups"groupSearchFilter: "(& (cn={0}) (| (objectClass=groupOfNames) (objectClass=groupOfUniqueNames)\\ (objectClass=posixGroup)))"inhibitInferRootDN: falsemanagerDN: "cn=Manager,dc=sample,dc=com"managerPasswordSecret: "{...}"rootDN: "dc=sample,dc=com"server: "ldap://localhost:1389"userSearchBase: "ou=people"disableMailAddressResolver: falsedisableRolePrefixing: truegroupIdStrategy: "caseInsensitive"userIdStrategy: "caseInsensitive"

SonarQube

Supported resources are:

  • Projects
  • Template
  • Roles, group mapping and permissions, at project or global level

Configuration

ParameterEnvironment variableNote
--sonar-endpointSONAR_ENDPOINTHTTPS endpoint
Sourced from Jenkins build parameter.
--sonar-api-tokenSONAR_API_TOKENAPI key generated from /account/security API
- Type is User Token
- User rights : System Administrator
Sourced from Jenkins credential SONAR_API.

Sonatype Nexus

Supported resources are:

  • Repository
  • Roles, group mapping and permissions, at repository level or global level

Repository configuration must follow the [/#admin/system/api](http://localhost:8681/#admin/system/api) of your Nexus instance of:

  • POST /vi/repositories/docker/hosted
  • POST /vi/repositories/maven/hosted
  • ... By default, repository mode is hosted and can be overridden with mode property.

Configuration

ParameterEnvironment variableNoteDefault
--nexus-endpointNEXUS_ENDPOINTHTTPS endpoint
Sourced from Jenkins build parameter.
--nexus-userNEXUS_USERLDAP or internal user nameadmin
Sourced from Jenkins credential NEXUS_API.
--nexus-passwordNEXUS_PASSWORDLDAP or internal password
Sourced from Jenkins credential NEXUS_API.

Harbor

Supported resources are:

  • Projects
  • Roles and group mapping, at project level

Configuration

ParameterEnvironment variableNoteDefault
--harbor-endpointHARBOR_ENDPOINTHTTPS endpoint
Sourced from Jenkins build parameter.
--harbor-userHARBOR_USERLDAP or internal user nameadmin
Sourced from Jenkins credential HARBOR_API.
--harbor-passwordHARBOR_PASSWORDLDAP or internal password
Sourced from Jenkins credential HARBOR_API.

GitLab

No resources are supported, only roles. GitLab groups and sub-groups are created according to the given groups and naming guidelines. Real Git repository projects are not managed by this CLI.

Configuration

ParameterEnvironment variableNoteDefault
--gitlab-endpointGITLAB_ENDPOINTHTTPS endpoint
Sourced from Jenkins global GITLAB_ENDPOINT environment variable.
--gitlab-tokenGITLAB_TOKENAccess token with following constraints:
- Type: Personal or Group Access Token
- Scope is ${gitlab_base_group} or root level
- Role: owner role
- Access level: api
Sourced from Jenkins credential GITLAB_API.
--gitlab-base-groupGITLAB_BASE_GROUPBase group where created groups sit/
--gitlab-wrapper-groupGITLAB_WRAPPER_GROUPPath of created wrapper group. Ignored if undefinedligoj
--gitlab-wrapper-group-nameGITLAB_WRAPPER_GROUP_NAMEName of created wrapper group
--gitlab_project_subgroup_prefixGITLAB_PROJECT_SUBGROUP_PREFIXPath prefix of created groups. No wrapper if undefinedligoj-

Created hierarchy

Project hierarchy for a project project1:

Gitlab pathPath patternDefault
/base${gitlab_base_group}/
|_ project1${gitlab_project_group_prefix}${project_key}No prefix
|— any-repoGit repository, not managed
|— any-groupUser group, not managed
|_ ligoj${gitlab_wrapper_group}ligoj
|_ ligoj-project1-dev${gitlab-project-subgroup-prefix}${project_key}-${group}Prefix ligoj-

Sub-groups are created with project_creation_level flag set to noone and with a specific avatar. See GitLab API create-a-subgroup

ArgoCD

Supported resources are:

  • Projects
  • Roles and permissions, at project level only
    • action: [delete, get]
    • permission: [allow,deny], by default allow
    • application:, by default *

Sample JSON part:

{
"projects": [
{
"name": "project1",
"description": "Project description",
"roles": {
"dev": {
"permissions": [
{
"action": "get"
},
{
"action": "delete",
"application": "app1",
"permission": "deny"
}
]
},
"test": {
"permissions": [
{
"action": "get"
}
]
}
}
}
]
}

Configuration

ParameterEnvironment variableNote
--argocd-endpointARGOCD_ENDPOINTHTTPS endpoint.
Sourced from Jenkins build parameter.
--argocd-userARGOCD_USERUsername. Not recommended, see ARGOCD_TICKET
--argocd-passwordARGOCD_PASSWORDPassword. Not recommended, see ARGOCD_TICKET
--argocd-ticketARGOCD_TICKETTicket generated by /alfresco/s/api/login API
Generated automatically if ARGOCD_PASSWORD is provided
Sourced from Jenkins credential ARGOCD_API.

Alfresco

Supported resources are:

  • Sites
  • Roles and permissions, at site level only
ligoj bootstrap create-roles --schema='{ "properties": { "groups": { "items": { "enum": [ "dev", "admin", "test", "securite" ] } }, "alfresco": { "properties": { "sites": { "maxItems": 1 } } } }}' --project "project1" --group-suffix="-team" --from="conf/sample.conf.alfresco.json"

Configuration

ParameterEnvironment variableNote
--alfresco-endpointALFRESCO_ENDPOINTHTTPS endpoint.
Sourced from Jenkins global ALFRESCO_ENDPOINT environment variable.
--alfresco-userALFRESCO_USERUsername. Not recommended, see ALFRESCO_TICKET
--alfresco-passwordALFRESCO_PASSWORDPassword. Not recommended, see ALFRESCO_TICKET
--alfresco-ticketALFRESCO_TICKETTicket generated by /alfresco/s/api/login API
Generated automatically if ALFRESCO_PASSWORD is provided
Sourced from Jenkins credential ALFRESCO_API.

Alfresco ticket generation by API:

  • either from Swagger API explorer /?urls.primaryName=Authentication%20API#/authentication/createTicket,
  • either with cURL command:
curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin"}'"https://alfresco.sample.com/alfresco/s/api/login"

Bootstrap delete-roles

Delete mapped roles from various tools symmetrically as create-roles operation.

Sequence

See --from for JSON loading options

By default, only roles are deleted; to perform a full cleanup, see the --with-data option

ligoj bootstrap delete-roles --project project-a --from conf.json \
--with-data "jenkins""sonar" \
--argocd-token="$ARGOCD_TOKEN" \
--argocd-user="$ARGOCD_USER" \
--alfresco-endpoint="$ALFRESCO_ENDPOINT" \
--alfresco-user="$ALFRESCO_USER" \
--alfresco-password="$ALFRESCO_PASSWORD" \
--nexus-endpoint="$NEXUS_ENDPOINT" \
--nexus-user="$NEXUS_USER" \
--nexus-password="$NEXUS_PASSWORD" \
--gitlab-endpoint="$GITLAB_ENDPOINT" \
--gitlab-token="$GITLAB_TOKEN" \
--jenkins-home="$JENKINS_HOME" \
--jenkins-endpoint="$JENKINS_ENDPOINT" \
--sonar-endpoint="$SONAR_ENDPOINT" \
--sonar-api-token="$SONAR_API_KEY" \

Deleted contents by tools

ToolContent typeDeletion modeOnly with-data
JenkinsFolder RBAC PermissionsOne by one
JenkinsGlobal RBAC PermissionsOne by one
JenkinsFolder credentialsOne by one
JenkinsGlobal credentialsOne by one
JenkinsFoldersOne by one
SonarQubeGroups and RBACOne by one
SonarQubeProjectsOne by one
SonarQubeTemplatesOne by one
GitLabWrapper project GroupsCascade
GitLabLDAP project GroupsOne by one
GitLabProject GroupsOne by one
Sonatype NexusRolesCascade
Sonatype NexusRepositoriesOne by one
AlfrescoRolesCascade
AlfrescoSitesOne by one
ArgoCDPermissionsOne by one
ArgoCDProjectsOne by one

--with-data

Each supported tool can be included (by default) or excluded, as described in create-roles includes/excludes option documentation. For excluded tools, no role and no data are deleted.

By default, only roles are deleted. To symmetrically delete the created contents, --with-data must be set to * or a specific list of plugins. In this case, all discovered JSON content is considered for deletion, including Jenkins folders, Nexus repositories, etc.

  • By default, no data is deleted
  • When --with-data is set to *, all discovered JSON's content for included plugins is considered
  • When --with-data is set to plugin1 plugin2, only these plugins are considered for data deletion, only if these plugins are included

Bootstrap roundtrip

The purpose of this documentation is only for troubleshooting and understanding the full behavior of bootstrap operations:

Commands from zero to zero:

ligoj -V bootstrap init --base-dn="dc=sample,dc=com"
ligoj -V bootstrap welcome-user --id "jdupont" --project "pic-master" --name "PIC Master" --script-custom-attributes '{"uidFonctionnel":"pic-master-script"}' --reader-custom-attributes '{"uidFonctionnel":"pic-master-reader"}' --jenkins-create-node --sonar-create-node --reset-reader-password ligoj -V bootstrap create-project --project "project-module1" --name "ProjetNew" --groups "dev""admin" --parent-project "pic-master" --parent-admin "jdupont" --team-leader jdupont
ligoj -V bootstrap create-roles --project "project-module1" --from "conf/ligoj/sample.conf.json" --excludes gitlab alfresco argocd
ligoj -V bootstrap create-roles --project "project-module1" --from "conf/ligoj/sample.conf.json" --excludes gitlab
ligoj -V bootstrap delete-roles --project "project-module1" --from "conf/ligoj/sample.conf.json" --excludes gitlab
ligoj -V project delete --id "project-module1" --parent-admin "jdupont" --with-data '*'
ligoj -V project delete --id "pic-master" --with-data '*'

Ligoj SSL Certificates

This section covers the case of running Ligoj with interaction with HTTPS services using self-signed certificates or issued by internal Certificate Authorities.

For each HTTPS website, run the following command. It requires keytool to be available on the host.

python plugins/ssl.py keycloack.sample.com 443 ./ligoj.jks changeit

When successive calls are done, the target TrustStore JKS file contains all aggregated certificates and can be provided to ligoj-ui and/or ligoj-api containers.

# Copy the TrustStore file in the mounted Ligoj home directory
cp ./ligoj.jks /var/lib/instance_datas/ligoj/
# Start the container with the TrustStore reference
docker run -e CUSTOM_OPTS='-Djavax.net.ssl.trustStore=/home/ligoj/ligoj.jks' \

Development

Everything goes through the Makefile, powered by uv. make init installs uv if it is missing, then creates the virtual environment and installs the exact runtime and dev dependencies from pyproject.toml — no manual venv / pip / pyenv steps.

make init # install uv if needed + create the venv + install deps
make run ARGS="--version"# run the CLI from the source tree

Run make help to list every target:

CommandDescription
make initInstall uv if missing, create the venv, install deps
make runRun the CLI, e.g. make run ARGS="info status"
make formatAuto-format and apply safe fixes with ruff
make lintStatic analysis with ruff and flake8
make testFull local gate: lint, format check, build, twine check
make buildBuild the sdist and wheel into dist/
make release-testPublish a dev build to TestPyPI and wait until live
make releaseCut a PyPI release (bump, tag, publish, wait)
make cleanRemove build artifacts and caches

Releasing

Releases are driven entirely from make; the GitHub Actions workflows only build and publish via PyPI Trusted Publishing (OIDC, no API token). Both commands print step-by-step progress and block until the package is actually live on the index. See RELEASE.md for one-time setup and troubleshooting.

StageCommandIndexWorkflow
Test publishmake release-testTestPyPIdeploy-test.yml
Releasemake releasePyPIdeploy.yml

Test publish (TestPyPI)

make release-test

Pushes the current HEAD to develop, which builds a unique .dev<run-number> version and uploads it to TestPyPI. The command waits until that build is live and prints the install line.

Release (PyPI)

make release # bump the minor version (e.g. 1.0.2 -> 1.1.0)
make release PART=patch # or bump patch / major instead

This runs the full quality gate, bumps version in pyproject.toml, commits, tags vX.Y.Z, pushes, creates the GitHub Release (which triggers deploy.yml), then waits until the version is live on PyPI. You are asked to confirm before anything is pushed — pass YES=1 to skip the prompt.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages