Skip to content

Developer Guide

Alexandru-Claudius Virtopeanu edited this page Oct 28, 2022 · 22 revisions

This page is meant to provide a guideline for developing the IONOS Cloud CLI, a guideline in sync with decisions took inside the SDKs & Tooling Team regarding this product. 🥇

New proposals are always welcomed in order to improve the IONOSCTL!

Overview

In order to integrate a new feature in the IONOS Cloud CLI, it is important to understand commands and flags management in the tool.

Commands

IONOS Cloud CLI uses cobra framework for creating commands.

It is recommended that the commands in IONOS Cloud CLI to have the following template:

ionosctl <service-namespace> <resource-name> <verb>

Example: ionosctl k8s cluster list -> <service-namespace> is k8s, <resource-name> is cluster, <verb> is list.

In order to create a new command, IONOS Cloud CLI uses a core package, which is a wrapper around cobra commands. The wrapper for the cobra.Command is defined in pkg/core/command_builder.go file, in CommandBuilder structure and used in the NewCommand() function (pkg/core/command_runner.go file).

The CommandBuilder structure contains fields similar with the cobra.Command fields (e.g.: short description, long description, example, aliases). Any fields that we want to set at the cobra.Command level (e.g. deprecated) should be added in the CommandBuilder structure and handled in the NewCommand() function.

The cobra.Command.Use of a command is defined in the CommandBuilder.Verb field.

The cobra.Command.PreRun of a command is defined in the CommandBuilder.PreRun field. PreRun function is mostly used for validations of the flags set, before running the command.

The cobra.Command.Run of a command is defined in the CommandBuilder.Run field. Run function is used to indicate what the command is excepted to do, when is used.

Flags

IONOS Cloud CLI uses viper framework for managing flags. Flags are a way to display options to the user, in a Command Line Interface tool.

IONOS Cloud CLI has a core package, which is a wrapper around viper support for adding flags.

In the CommandBuilder structure, there are the Namespace, Resource and Verb fields. These fields are determining the command "levels" or "namespaces". In order to make sure we have correctly managed a flag, using viper, we save the flags values with an unique identifier per command.

For a global flag (global flag refers to a flag set at multiple commands, set at a parent command level), the unique identifier has the following format: <verb>.<flag-name>.

For a local flag (local flag refers to a flag set at the command level, set at a child command level), the unique identifier has the following format: <namespace>.<resource>.<verb>.<flag-name>.

Methods such as core.GetGlobalFlagName() and core.GetFlagName() return the expected unique identifiers for flags.

Regarding options for a flag (e.g. deprecated, required), use core.<Option-Name>FlagOption() when adding the flag. Flag options are and they will be defined in the pkg/core/flag.go file.

Integration of a new service

Step 1. - Add service wrapper

First step for integration of a new service is to add a wrapper for IONOS Cloud service.

  • in the services directory, add a new directory named <service-name>
  • in the services/<service-name> directory, create a new directory named resources. The resources package will contain interfaces for the IONOS Cloud SDKs Go functions, and units tests for that and auto-generated mocks. Each resource methods and interface will be stored in a <resource-name>.go file.
  • in the tools/<service-name> directory, add regenerate_mocks.sh` file - specifying the source files from which mocks will be generated (e.q. mockgen -source .go > mocks/Service.go)
  • in the tools/<service-name> directory, add .mk file - adding the command for updating the mocks
  • in the services/<service-name> directory, add constants.go file - containing constants for default values and for options names and short names for the service commands
  • in the services/<service-name> directory, add services.go file - initializing all the services defined in the <service-name>/resources package. Add the Services struct defined in this file into the pkg/core/command_runner.go file, in the CommandConfig structure and initialize it in the NewCommandCfg() function.
  • in the services/<service-name> directory, add test.go file - initializing mocks for all the services defined in the <service-name>/resources package. Add the ResourceMocks struct defined in this file into the pkg/core/test.go file, in the ResourcesMocksTest structure and initialize it in the initMockResources() and initMockServices() function.

Note: For a new service integration, a corresponding SDK Go for the new service will be used. Add it into the go.mod file and use make vendor.update to update the dependencies.

Step 2. - Add commands

The next step is to add commands for the new service resources.

  • in the commands directory, add a new directory named <service-name>
  • in the commands/<service-name> directory, add <resource-name>.go file which will contain the commands for that resource. The PreRun and Run implementatios will be added.
  • in the commands/<service-name> directory, add completer directory, which contains functions for the completion support of the CLI (e.g.: for filters - if supported by the API, for resources ids)
  • in the commands/<service-name> directory, add query directory, which contains functions for the query options of the CLI, for list commands (e.g.: for filters, for depth options)
  • in the commands/<service-name> directory, add waiter directory, which contains functions for the waiting options of the CLI, for create/update/delete commands, waiting for requests - if supported by the API, and waiting for AVAILABLE or ACTIVE state, for deletion of the resources, etc.
  • finally, call <resource-name>Cmd() function in the commands/root.go file, in the addCommands() function, in order for the commands to be available with the ionosctl root command.

Recommendations

In order to keep a consistent behaviour over the ionosctl commands, please be aware of the following recommendations:

  • for the list commands, add query parameters support (if available in the API), add options like --depth, --filters, --max-results, etc.
  • for the list commands, add --no-headers option - this option is especially useful in Bash Scripting
  • for the list commands, it is recommended to have --all option, for resources that are dependent on other resources (e.g. nodepools, using --all - the user should see a list of all nodepools managed under his account, from all clusters)
  • for the get commands, add --no-headers option - this option is especially useful in Bash Scripting
  • for the create, add, attach commands, add --wait-for-request option - if this is implemented at the API level
  • for the update commands, add --wait-for-request option - if this is implemented at the API level
  • for the delete, remove, detach commands, add --wait-for-request option - if this is implemented at the API level
  • for the delete commands, it is recommended to have --all option, to delete all resources (TBD, regarding consistency between this option and list --all option)
  • for the delete commands, it is recommended to have --wait-for-deletion option, to wait until the resource no longer exists and a 404 HTTP Status Code is returned
  • for debug support, add logs in the commands for the properties set
  • for easier usage of the tool, consider adding aliases for commands and flags
  • for easier usage of the tool, consider adding completion functions for flags
  • for easier usage of the tool, for options that involve unit measures, consider supporting multiple unit measures (e.g. for RAM size, MB, GB, etc)

Step 3. - Add tests

Right now, IONOS Cloud CLI has support for unit tests, using mocks, validating if the options set by the user are correctly sent to the API. Add unit tests for services, for commands, completer, waiter functions.

In the tools/<service-name> directory, in the .mk file add the commands for testing.

In order to generate mocks use:

make mocks_update

Note: when using mocks in unit tests, make sure to specify the expected calls in the correct order, and to give the expected parameters according to what has been set as flags (using viper.Set). The unit tests will run at every CI, and release and they will not create resources on IONOS Cloud.

In order to run the tests use:

make test

Step 4. - Add documentation

The documentation for each command is generated based on the input added in commands/<service-name>/<resource-name>.go file. Usage, short, long descriptions, examples, aliases, options, descriptions for options - are all included in the generated documentation.

In the tools/<service-name> directory, add doc.go file that writes the documentation, that generates the documentation.

In order to generate documentation for a new service, we might need to add updates in the tools/regenerate_doc.sh. The tool generates the documentation and moves, copies the file in specific <service-entire-name> directories. Also, in the tools/<service-name> directory, in the .mk file we need to add the command for updating the documentation.

In order to generate/update documentation use:

make docs_update

After generating the files for a new service, update the docs/summary.md file to link the files.

General Recommendations

  • "format" go imports aligned with best practices, using goimports -l -w before merging (you can use sudo apt install golang-golang-x-tools for installation of the tool)
  • use feat/fix/doc/test as prefix for new commits and for names in PRs - this helps keeping a clean history of the commits and on the updates when releasing a new version of the tool;
  • for PRs, use squash and merge option - this helps keeping a clean history of the commits;
  • for PRs, try to make a PR for a change, not adding multiple changes to the same PR - this helps keeping a transparent history of the updates to the users and other developers as well;
  • make sure to follow SonarCloud recommendations when possible;
  • consistency over the code base and documents is highly encouraged.

Happy Developing! 🎉

Settings for Goland IDE to avoid import bugs

Make sure your repo exists in $GOPATH

Make sure Go Modules integration is enabled in settings (Preferences / Settings | Go | Go Modules),

Disable GOPATH indexing (Preferences / Settings | Go | GOPATH | Index entire GOPATH).

use "Sync Dependencies of ..." quick option on any import