diff --git a/README.md b/README.md index 262d249..773743d 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Before you proceed further, please take note of the following considerations to When executing _Ansible_ commands using the _go-ansible_ library inside a container, ensure that the container has configured an init system. The init system is necessary to manage the child processes created by the _Ansible_ commands. If the container does not have an init system, the child processes may not be correctly managed, leading to unexpected behavior such as zombie processes. -You can read more about that in the issue [139](https://github.com/apenella/go-ansible/issues/139). +You can read more about that in the issue [139](https://github.com/apenella/go-ansible/issues/139) and [here](https://github.com/ansible/ansible/issues/49270#issuecomment-462306244). ### Disable pseudo-terminal allocation diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5db7e78..6e090ee 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -44,6 +44,7 @@ Version 2.0.0 of *go-ansible* introduces several disruptive changes. Read the up - `github.com/apenella/go-ansible/v2/pkg/execute/result/json` package has been introduced. This package offers the component for printing execution results from the JSON stdout callback. It supersedes the `JSONStdoutCallbackResults` function that was previously defined in the `github.com/apenella/go-ansible/v2/pkg/stdoutcallback` package. - `github.com/apenella/go-ansible/v2/pkg/execute/stdoutcallback`. package has been introduced and offers multiple decorators designed to set the stdout callback for Ansible executions. - `github.com/apenella/go-ansible/v2/pkg/execute/workflow` package has been introduced and allows you to define a workflow for executing multiple commands in a sequence. +- `github.com/apenella/go-ansible/v2/pkg/galaxy/role/install` package has been introduced. This package allows you to install Ansible roles from the Ansible Galaxy. - `NewAnsibleAdhocCmd`, `NewAnsibleInventoryCmd` and `NewAnsiblePlaybookCmd` functions have been introduced. These functions are responsible for creating the `AnsibleAdhocCmd`, `AnsibleInventoryCmd` and `AnsiblePlaybookCmd` structs, respectively. - `ResultsOutputer` interface has been introduced in the `github.com/apenella/go-ansible/v2/pkg/execute/result` package. This interface defines the criteria for a struct to be compliant in printing execution results. - A utility to generate the code for the configuration package has been introduced. This utility is located in the `utils/cmd/configGenerator.go`. diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/Makefile b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/Makefile new file mode 100644 index 0000000..d06c0b1 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/Makefile @@ -0,0 +1,50 @@ + +DOCKER_COMPOSE_BINARY := $(shell docker compose version > /dev/null 2>&1 && echo "docker compose" || (which docker-compose > /dev/null 2>&1 && echo "docker-compose" || (echo "docker compose not found. Aborting." >&2; exit 1))) + +PROJECT_NAME := go-ansible-$(shell basename ${PWD}) + +# dafault target +.DEFAULT_GOAL: help + +help: ## Lists available targets + @echo + @echo "Makefile usage:" + @grep -E '^[a-zA-Z1-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[1;32m%-20s\033[0m %s\n", $$1, $$2}' + @echo + +build: ## Build the Docker compose environment + @$(DOCKER_COMPOSE_BINARY) build + +generate-keys: cleanup-keys ## Generate the SSH key pair required to autheneticate to SSH server + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) run --rm openssh-client -t rsa -q -N "" -f id_rsa -C "apenella@$(PROJECT_NAME).test" + +cleanup-keys: ## Cleanup the SSH key pair + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) run --rm --entrypoint /bin/sh openssh-client -c 'rm -rf $$(ls)' + +up: generate-keys ## Create and start containers + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) up --detach --build + +down: ## Stop and remove containers, networks, and volumes + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) down --volumes --remove-orphans --timeout 3 + +restart: down up ## Restart the containers + +ps: ## List containers + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) ps + +logs: ## Show all logs + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) logs + +attach-ansible: ## Attach to the ansible container + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) exec --workdir /code/examples/$$(basename $$(pwd)) ansible /bin/sh + +attach-server: ## Attach to the server container + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) exec server /bin/sh + +project-name: ## Show the project name + @echo $(PROJECT_NAME) + +run: ## Run the example + @$(DOCKER_COMPOSE_BINARY) --project-name $(PROJECT_NAME) exec --workdir /code/examples/$$(basename $$(pwd)) ansible go run $$(basename $$(pwd)).go + +start-and-run: up run ## Start the environment and run the example diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker-compose.yaml b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker-compose.yaml new file mode 100644 index 0000000..6655a19 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker-compose.yaml @@ -0,0 +1,41 @@ +name: ansibleplaybook-ssh + +services: + ansible: + build: + context: docker/ansible + command: ["tail", "-f", "/dev/null"] + # command: ["ansible-playbook", "--help"] + volumes: + - ssh:/ssh + - ../..:/code + working_dir: /code + ## Set the init flag to true lets the process 1 to reap all the zombie processes + init: true + depends_on: + - server + + server: + build: + context: docker/server + # command: ["cat", "/etc/ssh/sshd_config"] + volumes: + - ssh:/ssh + depends_on: + - openssh-client + # ports: + # - "22220:22" + + openssh-client: + build: + context: docker/openssh-client + working_dir: /ssh + volumes: + - ssh:/ssh + deploy: + resources: + limits: + memory: 10M + +volumes: + ssh: \ No newline at end of file diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/ansible/Dockerfile b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/ansible/Dockerfile new file mode 100644 index 0000000..41c9850 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/ansible/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-alpine3.19 + +RUN apk add --update --no-cache \ + openssh-client \ + git \ + && rm -rf /var/cache/apk/* + +RUN pip3 install -U pip setuptools \ + && pip3 install --no-cache-dir \ + setuptools-rust \ + cryptography \ + # Required library to execute ansible community.general.dig plugin + dnspython \ + ansible \ + && ln /usr/local/bin/ansible-playbook /usr/bin/ansible-playbook + +RUN apk add --update --no-cache \ + go + +# Configure Go +ENV GOROOT /usr/lib/go +ENV GOPATH /go +ENV PATH /go/bin:$PATH + +RUN mkdir -p ${GOPATH}/src ${GOPATH}/bin diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/openssh-client/Dockerfile b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/openssh-client/Dockerfile new file mode 100644 index 0000000..4736669 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/openssh-client/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:3.19 + +RUN apk add --update --no-cache \ + openssh-client \ + git \ + && rm -rf /var/cache/apk/* + +ENTRYPOINT [ "/usr/bin/ssh-keygen" ] +CMD [ "--help" ] \ No newline at end of file diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/Dockerfile b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/Dockerfile new file mode 100644 index 0000000..0ca7432 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/Dockerfile @@ -0,0 +1,20 @@ +FROM python:bookworm + + +RUN adduser --disabled-password --gecos "" ansible + +RUN apt-get update \ + && apt-get install -y \ + openssh-server \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir /var/run/sshd \ + && ssh-keygen -A + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Expose the SSH port +EXPOSE 22 + +CMD ["/usr/local/bin/entrypoint.sh"] diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/entrypoint.sh b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/entrypoint.sh new file mode 100644 index 0000000..722dc2f --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/docker/server/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +home=$(getent passwd "$(whoami)" | cut -d: -f6) + +mkdir -p "${home}/.ssh" +cat /ssh/id_rsa.pub > "${home}/.ssh/authorized_keys" + +/usr/sbin/sshd -D \ No newline at end of file diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/inventory.yml b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/inventory.yml new file mode 100644 index 0000000..a186167 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/inventory.yml @@ -0,0 +1,9 @@ +--- + +all: + hosts: + server: + ansible_host: server + ansible_user: root + ansible_ssh_private_key_file: /ssh/id_rsa + diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/site.yml b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/site.yml new file mode 100644 index 0000000..3955d55 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/site.yml @@ -0,0 +1,8 @@ +--- + +- hosts: server + + tasks: + - name: Install nginx + ansible.builtin.import_role: + name: geerlingguy.go diff --git a/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/workflowexecute-ansibleplaybook-with-galaxy-install-role.go b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/workflowexecute-ansibleplaybook-with-galaxy-install-role.go new file mode 100644 index 0000000..13fabc8 --- /dev/null +++ b/examples/workflowexecute-ansibleplaybook-with-galaxy-install-role/workflowexecute-ansibleplaybook-with-galaxy-install-role.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/apenella/go-ansible/v2/pkg/execute" + "github.com/apenella/go-ansible/v2/pkg/execute/workflow" + galaxy "github.com/apenella/go-ansible/v2/pkg/galaxy/role/install" + "github.com/apenella/go-ansible/v2/pkg/playbook" +) + +func main() { + + ansiblePlaybookOptions := &playbook.AnsiblePlaybookOptions{ + Inventory: "inventory.yml", + SSHCommonArgs: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null", + } + + galaxyInstallRolesCmd := galaxy.NewAnsibleGalaxyRoleInstallCmd( + galaxy.WithRoleNames("geerlingguy.go"), + galaxy.WithGalaxyRoleInstallOptions(&galaxy.AnsibleGalaxyRoleInstallOptions{ + Force: true, + }), + ) + + galaxyInstallRolesExec := execute.NewDefaultExecute( + execute.WithCmd(galaxyInstallRolesCmd), + ) + + playbookCmd := playbook.NewAnsiblePlaybookExecute("site.yml"). + WithPlaybookOptions(ansiblePlaybookOptions) + + err := workflow.NewWorkflowExecute(galaxyInstallRolesExec, playbookCmd).WithTrace().Execute(context.TODO()) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} diff --git a/pkg/execute/workflow/workflowExecute.go b/pkg/execute/workflow/workflowExecute.go index c984b72..3e477fc 100644 --- a/pkg/execute/workflow/workflowExecute.go +++ b/pkg/execute/workflow/workflowExecute.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/apenella/go-ansible/v2/pkg/execute" + "github.com/fatih/color" ) type WorkflowExecute struct { @@ -12,6 +13,8 @@ type WorkflowExecute struct { ExecutorList []execute.Executor // ContinueOnError is a flag to continue on error ContinueOnError bool + // Trace is a flag to trace the execution + Trace bool } // NewWorkflowExecute creates a new WorkflowExecute @@ -33,11 +36,22 @@ func (e *WorkflowExecute) WithContinueOnError() *WorkflowExecute { return e } +// WithTrace sets the trace flag to true +func (e *WorkflowExecute) WithTrace() *WorkflowExecute { + e.Trace = true + return e +} + // Execute runs the executors func (e *WorkflowExecute) Execute(ctx context.Context) error { var errList []error = make([]error, 0) - for _, executor := range e.ExecutorList { + for executionNum, executor := range e.ExecutorList { + + if e.Trace { + color.Blue(fmt.Sprintf("\n\u2022 executing task %d out of %d\n", executionNum+1, len(e.ExecutorList))) + } + err := executor.Execute(ctx) if err != nil { errList = append(errList, err) diff --git a/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd.go b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd.go new file mode 100644 index 0000000..8b70c31 --- /dev/null +++ b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd.go @@ -0,0 +1,108 @@ +package galaxyroleinstall + +import ( + "fmt" +) + +const ( + // DefaultAnsibleGalaxyRoleInstallBinary is the ansible-galaxy binary file default value + DefaultAnsibleGalaxyRoleInstallBinary = "ansible-galaxy" + + //AnsibleGalaxyRoleInstallCommand is the ansible-galaxy command to install roles + AnsibleGalaxyRoleInstallCommand = "role install" +) + +// AnsibleGalaxyRoleInstallOptionsFunc is a function to set executor options +type AnsibleGalaxyRoleInstallOptionsFunc func(*AnsibleGalaxyRoleInstallCmd) + +// AnsibleGalaxyRoleInstallCmd object is the main object which defines the `ansible-galaxy` command to install roles. +type AnsibleGalaxyRoleInstallCmd struct { + // Binary is the ansible-galaxy binary file + Binary string + + // RoleNames is the ansible-galaxy's role names to be installed + RoleNames []string + + // GalaxyRoleInstallOptions are the ansible-galaxy's role install options + GalaxyRoleInstallOptions *AnsibleGalaxyRoleInstallOptions +} + +// NewAnsibleGalaxyRoleInstallCmd creates a new AnsibleGalaxyRoleInstallCmd instance +func NewAnsibleGalaxyRoleInstallCmd(options ...AnsibleGalaxyRoleInstallOptionsFunc) *AnsibleGalaxyRoleInstallCmd { + cmd := &AnsibleGalaxyRoleInstallCmd{} + + for _, option := range options { + option(cmd) + } + + return cmd +} + +// WithBinary set the ansible-galaxy binary file +func WithBinary(binary string) AnsibleGalaxyRoleInstallOptionsFunc { + return func(p *AnsibleGalaxyRoleInstallCmd) { + p.Binary = binary + } +} + +// WithGalaxyRoleInstallOptions set the ansible-galaxy role install options +func WithGalaxyRoleInstallOptions(options *AnsibleGalaxyRoleInstallOptions) AnsibleGalaxyRoleInstallOptionsFunc { + return func(p *AnsibleGalaxyRoleInstallCmd) { + p.GalaxyRoleInstallOptions = options + } +} + +// WithRoleNames set the ansible-galaxy role names +func WithRoleNames(roleNames ...string) AnsibleGalaxyRoleInstallOptionsFunc { + return func(p *AnsibleGalaxyRoleInstallCmd) { + p.RoleNames = append([]string{}, roleNames...) + } +} + +// Command generate the ansible-galaxy role install command which will be executed +func (p *AnsibleGalaxyRoleInstallCmd) Command() ([]string, error) { + cmd := []string{} + + // Use default binary when it is not already defined + if p.Binary == "" { + p.Binary = DefaultAnsibleGalaxyRoleInstallBinary + } + + cmd = append(cmd, p.Binary, "role", "install") + + // Add the options + if p.GalaxyRoleInstallOptions != nil { + options, err := p.GalaxyRoleInstallOptions.GenerateCommandOptions() + if err != nil { + return nil, err + } + cmd = append(cmd, options...) + } + + // Add the role names + cmd = append(cmd, p.RoleNames...) + + return cmd, nil +} + +// String returns the ansible-galaxy role install command as a string +func (p *AnsibleGalaxyRoleInstallCmd) String() string { + + // Use default binary when it is not already defined + if p.Binary == "" { + p.Binary = DefaultAnsibleGalaxyRoleInstallBinary + } + + str := fmt.Sprintf("%s %s", p.Binary, AnsibleGalaxyRoleInstallCommand) + + if p.GalaxyRoleInstallOptions != nil { + str = fmt.Sprintf("%s %s", str, p.GalaxyRoleInstallOptions.String()) + } + + // Include the role names + for _, roleName := range p.RoleNames { + str = fmt.Sprintf("%s %s", str, roleName) + } + + return str +} diff --git a/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd_test.go b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd_test.go new file mode 100644 index 0000000..3fdcc5c --- /dev/null +++ b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallCmd_test.go @@ -0,0 +1,145 @@ +package galaxyroleinstall + +import ( + "testing" + + errors "github.com/apenella/go-common-utils/error" + "github.com/stretchr/testify/assert" +) + +func TestNewAnsibleGalaxyRoleInstallCmd(t *testing.T) { + cmd := NewAnsibleGalaxyRoleInstallCmd( + WithBinary("ansible-galaxy-binary"), + WithRoleNames("nginx"), + WithGalaxyRoleInstallOptions(&AnsibleGalaxyRoleInstallOptions{ + Force: true, + }), + ) + + expect := &AnsibleGalaxyRoleInstallCmd{ + Binary: "ansible-galaxy-binary", + RoleNames: []string{"nginx"}, + GalaxyRoleInstallOptions: &AnsibleGalaxyRoleInstallOptions{ + Force: true, + }, + } + + assert.Equal(t, expect, cmd) +} + +func TestAnsibleGalaxyRoleInstallCmdCommand(t *testing.T) { + + tests := []struct { + desc string + cmd *AnsibleGalaxyRoleInstallCmd + command []string + err error + }{ + { + desc: "Testing generate a command for AnsibleGalaxyRoleInstallCmd with all flags", + cmd: NewAnsibleGalaxyRoleInstallCmd( + WithBinary("ansible-galaxy-binary"), + WithRoleNames("nginx"), + WithGalaxyRoleInstallOptions(&AnsibleGalaxyRoleInstallOptions{ + ApiKey: "apikey", + Force: true, + ForceWithDeps: true, + IgnoreCerts: true, + IgnoreErrors: true, + KeepSCMMeta: true, + NoDeps: true, + RoleFile: "rolefile", + RolesPath: "rolespath", + Server: "server", + Timeout: "timeout", + Token: "token", + Verbose: true, + VerboseV: true, + VerboseVV: true, + VerboseVVV: true, + VerboseVVVV: true, + Version: true, + }), + ), + err: &errors.Error{}, + command: []string{"ansible-galaxy-binary", "role install", + ApiKeyFlag, "apikey", + ForceFlag, + ForceWithDepsFlag, + IgnoreCertsFlag, + IgnoreErrorsFlag, + KeepSCMMetaFlag, + NoDepsFlag, + RoleFileFlag, "rolefile", + RolesPathFlag, "rolespath", + ServerFlag, "server", + TimeoutFlag, "timeout", + TokenFlag, "token", + VerboseVVVVFlag, + VersionFlag, + "nginx", + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Log(test.desc) + + command, err := test.cmd.Command() + + if err != nil && assert.Error(t, err) { + assert.Equal(t, test.err, err) + } else { + assert.Equal(t, test.command, command, "Unexpected command value") + } + }) + } +} + +func TestAnsibleGalaxyRoleInstallCmdS(t *testing.T) { + tests := []struct { + desc string + cmd *AnsibleGalaxyRoleInstallCmd + command string + }{ + { + desc: "Testing generate a command for AnsibleGalaxyRoleInstallCmd with all flags", + cmd: NewAnsibleGalaxyRoleInstallCmd( + WithBinary("ansible-galaxy-binary"), + WithRoleNames("nginx"), + WithGalaxyRoleInstallOptions(&AnsibleGalaxyRoleInstallOptions{ + ApiKey: "apikey", + Force: true, + ForceWithDeps: true, + IgnoreCerts: true, + IgnoreErrors: true, + KeepSCMMeta: true, + NoDeps: true, + RoleFile: "rolefile", + RolesPath: "rolespath", + Server: "server", + Timeout: "timeout", + Token: "token", + Verbose: true, + VerboseV: true, + VerboseVV: true, + VerboseVVV: true, + VerboseVVVV: true, + Version: true, + }), + ), + command: "ansible-galaxy-binary role install --api-key apikey --force --force-with-deps --ignore-certs --ignore-errors --keep-scm-meta --no-deps --role-file rolefile --roles-path rolespath --server server --timeout timeout --token token -vvvv -v -vv -vvv -vvvv --version nginx", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Log(test.desc) + + command := test.cmd.String() + + assert.Equal(t, test.command, command, "Unexpected command value") + }) + } +} diff --git a/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions.go b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions.go new file mode 100644 index 0000000..57f3373 --- /dev/null +++ b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions.go @@ -0,0 +1,300 @@ +package galaxyroleinstall + +import ( + "fmt" + + errors "github.com/apenella/go-common-utils/error" +) + +const ( + + // ApiKeyFlag represent the API key to use to authenticate against the galaxy server. Same as --token + ApiKeyFlag = "--api-key" + + // ForceFlag represents the command line flag for forcing overwriting an existing role or role file. + ForceFlag = "--force" + + // ForceWithDepsFlag represents the command line flag for forcing overwriting an existing role, role file, or dependencies. + ForceWithDepsFlag = "--force-with-deps" + + // IgnoreCertsFlag represent the flag to ignore SSL certificate validation errors + IgnoreCertsFlag = "--ignore-certs" + + // IgnoreErrorsFlag represents the command line flag for continuing processing even if a role fails to install. + IgnoreErrorsFlag = "--ignore-errors" + + // KeepSCMMetaFlag represent the flag to use tar instead of the scm archive option when packaging the role. + KeepSCMMetaFlag = "--keep-scm-meta" + + // NoDepsFlag represents the command line flag for not installing dependencies. + NoDepsFlag = "--no-deps" + + // RoleFileFlag represents the command line flag for specifying the path to a file containing a list of roles to install. + RoleFileFlag = "--role-file" + + // RolesPathFlag represents the command line flag for specifying where roles should be installed on the local filesystem. + RolesPathFlag = "--roles-path" + + // ServerFlag represent the flag to specify the galaxy server to use + ServerFlag = "--server" + + // TimeoutFlag represent the time to wait for operations against the galaxy server, defaults to 60s + TimeoutFlag = "--timeout" + + // TokenFlag represent the token to use to authenticate against the galaxy server. Same as --api-key + TokenFlag = "--token" + + // VerboseFlag verbose mode enabled + VerboseFlag = "-vvvv" + + // VerboseVFlag verbose with -v is enabled + VerboseVFlag = "-v" + + // VerboseVVFlag verbose with -vv is enabled + VerboseVVFlag = "-vv" + + // VerboseVVVFlag verbose with -vvv is enabled + VerboseVVVFlag = "-vvv" + + // VerboseVVVVFlag verbose with -vvvv is enabled + VerboseVVVVFlag = "-vvvv" + + // VersionFlag show program's version number, config file location, configured module search path, module location, executable location and exit + VersionFlag = "--version" +) + +// AnsibleGalaxyRoleInstallOptions represents the options that can be passed to the ansible-galaxy role install command. +type AnsibleGalaxyRoleInstallOptions struct { + + // ApiKey represent the API key to use to authenticate against the galaxy server. Same as --token + ApiKey string + + // Force represents whether to force overwriting an existing role or role file. + Force bool + + // ForceWithDeps represents whether to force overwriting an existing role, role file, or dependencies. + ForceWithDeps bool + + // IgnoreCerts represent the flag to ignore SSL certificate validation errors + IgnoreCerts bool + + // IgnoreErrors represents whether to continue processing even if a role fails to install. + IgnoreErrors bool + + // KeepSCMMeta represent the flag to use tar instead of the scm archive option when packaging the role. + KeepSCMMeta bool + + // NoDeps represents whether to install dependencies. + NoDeps bool + + // RoleFile represents the path to a file containing a list of roles to install. + RoleFile string + + // RolesPath represents the path where roles should be installed on the local filesystem. + RolesPath string + + // Server represent the flag to specify the galaxy server to use + Server string + + // Timeout represent the time to wait for operations against the galaxy server, defaults to 60s + Timeout string + + // Token represent the token to use to authenticate against the galaxy server. Same as --api-key + Token string + + // Verbose verbose mode enabled + Verbose bool + + // Verbose verbose mode -v enabled + VerboseV bool + + // Verbose verbose mode -vv enabled + VerboseVV bool + + // Verbose verbose mode -vvv enabled + VerboseVVV bool + + // Verbose verbose mode -vvvv enabled + VerboseVVVV bool + + // Version show program's version number, config file location, configured module search path, module location, executable location and exit + Version bool +} + +// GenerateCommandOptions generates the command line options for the ansible-galaxy role install command. +func (o *AnsibleGalaxyRoleInstallOptions) GenerateCommandOptions() ([]string, error) { + + errContext := "(galaxy::GenerateCommandOptions)" + options := []string{} + + if o == nil { + return nil, errors.New(errContext, "AnsibleGalaxyRoleInstallOptions is nil") + } + + if o.ApiKey != "" { + options = append(options, ApiKeyFlag, o.ApiKey) + } + + if o.Force { + options = append(options, ForceFlag) + } + + if o.ForceWithDeps { + options = append(options, ForceWithDepsFlag) + } + + if o.IgnoreCerts { + options = append(options, IgnoreCertsFlag) + } + + if o.IgnoreErrors { + options = append(options, IgnoreErrorsFlag) + } + + if o.KeepSCMMeta { + options = append(options, KeepSCMMetaFlag) + } + + if o.NoDeps { + options = append(options, NoDepsFlag) + } + + if o.RoleFile != "" { + options = append(options, RoleFileFlag, o.RoleFile) + } + + if o.RolesPath != "" { + options = append(options, RolesPathFlag, o.RolesPath) + } + + if o.Server != "" { + options = append(options, ServerFlag, o.Server) + } + + if o.Timeout != "" { + options = append(options, TimeoutFlag, o.Timeout) + } + + if o.Token != "" { + options = append(options, TokenFlag, o.Token) + } + + verboseFlag, err := o.generateVerbosityFlag() + if err != nil { + return nil, errors.New(errContext, "", err) + } + + if verboseFlag != "" { + options = append(options, verboseFlag) + } + + if o.Version { + options = append(options, VersionFlag) + } + + return options, nil +} + +// generateVerbosityFlag return a string with the verbose flag. Higher verbosity (more v's) has precedence over lower +func (o *AnsibleGalaxyRoleInstallOptions) generateVerbosityFlag() (string, error) { + if o.Verbose { + return VerboseFlag, nil + } + + if o.VerboseVVVV { + return VerboseVVVVFlag, nil + } + + if o.VerboseVVV { + return VerboseVVVFlag, nil + } + + if o.VerboseVV { + return VerboseVVFlag, nil + } + + if o.VerboseV { + return VerboseVFlag, nil + } + + return "", nil +} + +// String returns a string representation of the ansible-galaxy role install options. +func (o *AnsibleGalaxyRoleInstallOptions) String() string { + str := "" + + if o.ApiKey != "" { + str = fmt.Sprintf("%s %s %s", str, ApiKeyFlag, o.ApiKey) + } + + if o.Force { + str = fmt.Sprintf("%s %s", str, ForceFlag) + } + + if o.ForceWithDeps { + str = fmt.Sprintf("%s %s", str, ForceWithDepsFlag) + } + + if o.IgnoreCerts { + str = fmt.Sprintf("%s %s", str, IgnoreCertsFlag) + } + + if o.IgnoreErrors { + str = fmt.Sprintf("%s %s", str, IgnoreErrorsFlag) + } + + if o.KeepSCMMeta { + str = fmt.Sprintf("%s %s", str, KeepSCMMetaFlag) + } + + if o.NoDeps { + str = fmt.Sprintf("%s %s", str, NoDepsFlag) + } + + if o.RoleFile != "" { + str = fmt.Sprintf("%s %s %s", str, RoleFileFlag, o.RoleFile) + } + + if o.RolesPath != "" { + str = fmt.Sprintf("%s %s %s", str, RolesPathFlag, o.RolesPath) + } + + if o.Server != "" { + str = fmt.Sprintf("%s %s %s", str, ServerFlag, o.Server) + } + + if o.Timeout != "" { + str = fmt.Sprintf("%s %s %s", str, TimeoutFlag, o.Timeout) + } + + if o.Token != "" { + str = fmt.Sprintf("%s %s %s", str, TokenFlag, o.Token) + } + + if o.Verbose { + str = fmt.Sprintf("%s %s", str, VerboseFlag) + } + + if o.VerboseV { + str = fmt.Sprintf("%s %s", str, VerboseVFlag) + } + + if o.VerboseVV { + str = fmt.Sprintf("%s %s", str, VerboseVVFlag) + } + + if o.VerboseVVV { + str = fmt.Sprintf("%s %s", str, VerboseVVVFlag) + } + + if o.VerboseVVVV { + str = fmt.Sprintf("%s %s", str, VerboseVVVVFlag) + } + + if o.Version { + str = fmt.Sprintf("%s %s", str, VersionFlag) + } + + return str +} diff --git a/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions_test.go b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions_test.go new file mode 100644 index 0000000..d09cbf2 --- /dev/null +++ b/pkg/galaxy/role/install/ansibleGalaxyRoleInstallOptions_test.go @@ -0,0 +1,210 @@ +package galaxyroleinstall + +import ( + "testing" + + errors "github.com/apenella/go-common-utils/error" + "github.com/stretchr/testify/assert" +) + +func TestAnsibleGalaxyRoleInstallOptionsGenerateCommandOptions(t *testing.T) { + + errContext := "(galaxy::GenerateCommandOptions)" + + tests := []struct { + desc string + options *AnsibleGalaxyRoleInstallOptions + err error + expect []string + }{ + { + desc: "Testing nil AnsibleGalaxyRoleInstallOptions definition", + options: nil, + err: errors.New(errContext, "AnsibleGalaxyRoleInstallOptions is nil"), + expect: []string{}, + }, + { + desc: "Testing an empty AnsibleGalaxyRoleInstallOptions definition", + options: &AnsibleGalaxyRoleInstallOptions{}, + err: nil, + expect: []string{}, + }, + { + desc: "Testing AnsibleGalaxyRoleInstallOptions with all flags", + options: &AnsibleGalaxyRoleInstallOptions{ + ApiKey: "apikey", + Force: true, + ForceWithDeps: true, + IgnoreCerts: true, + IgnoreErrors: true, + KeepSCMMeta: true, + NoDeps: true, + RoleFile: "rolefile", + RolesPath: "rolespath", + Server: "server", + Timeout: "timeout", + Token: "token", + Verbose: true, + VerboseV: true, + VerboseVV: true, + VerboseVVV: true, + VerboseVVVV: true, + Version: true, + }, + err: nil, + expect: []string{ + ApiKeyFlag, "apikey", + ForceFlag, + ForceWithDepsFlag, + IgnoreCertsFlag, + IgnoreErrorsFlag, + KeepSCMMetaFlag, + NoDepsFlag, + RoleFileFlag, "rolefile", + RolesPathFlag, "rolespath", + ServerFlag, "server", + TimeoutFlag, "timeout", + TokenFlag, "token", + VerboseVVVVFlag, + VersionFlag, + }, + }, + } + + for _, test := range tests { + + t.Run(test.desc, func(t *testing.T) { + t.Log(test.desc) + + options, err := test.options.GenerateCommandOptions() + + if err != nil && assert.Error(t, err) { + assert.Equal(t, test.err, err) + } else { + assert.Equal(t, test.expect, options, "Unexpected options value") + } + }) + } +} + +func TestAnsibleGalaxyRoleInstallOptionsGenerateVerbosityFlag(t *testing.T) { + tests := []struct { + desc string + options *AnsibleGalaxyRoleInstallOptions + res string + err error + }{ + { + desc: "Testing generate verbosity flag", + options: &AnsibleGalaxyRoleInstallOptions{ + Verbose: true, + }, + res: "-vvvv", + err: &errors.Error{}, + }, + { + desc: "Testing generate verbosity flag V", + options: &AnsibleGalaxyRoleInstallOptions{ + VerboseV: true, + }, + res: "-v", + err: &errors.Error{}, + }, + { + desc: "Testing generate verbosity flag VV", + options: &AnsibleGalaxyRoleInstallOptions{ + VerboseVV: true, + }, + res: "-vv", + err: &errors.Error{}, + }, + { + desc: "Testing generate verbosity flag VVV", + options: &AnsibleGalaxyRoleInstallOptions{ + VerboseVVV: true, + }, + res: "-vvv", + err: &errors.Error{}, + }, + { + desc: "Testing generate verbosity flag VVVV", + options: &AnsibleGalaxyRoleInstallOptions{ + VerboseVVVV: true, + }, + res: "-vvvv", + err: &errors.Error{}, + }, + { + desc: "Testing generate verbosity flag VV has precedence over V", + options: &AnsibleGalaxyRoleInstallOptions{ + VerboseVV: true, + VerboseV: true, + }, + res: "-vv", + err: &errors.Error{}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Log(test.desc) + + res, err := test.options.generateVerbosityFlag() + if err != nil && assert.Error(t, err) { + assert.Equal(t, test.err, err) + } else { + assert.Equal(t, test.res, res) + } + }) + } +} + +func TestAnsibleGalaxyRoleInstallOptionsString(t *testing.T) { + tests := []struct { + desc string + options *AnsibleGalaxyRoleInstallOptions + expect string + }{ + { + desc: "Testing generate string from an empty AnsibleGalaxyRoleInstallOptions", + options: &AnsibleGalaxyRoleInstallOptions{}, + expect: "", + }, + { + desc: "Testing generate string from an AnsibleGalaxyRoleInstallOptions with all flags", + options: &AnsibleGalaxyRoleInstallOptions{ + ApiKey: "apikey", + Force: true, + ForceWithDeps: true, + IgnoreCerts: true, + IgnoreErrors: true, + KeepSCMMeta: true, + NoDeps: true, + RoleFile: "rolefile", + RolesPath: "rolespath", + Server: "server", + Timeout: "timeout", + Token: "token", + Verbose: true, + VerboseV: true, + VerboseVV: true, + VerboseVVV: true, + VerboseVVVV: true, + Version: true, + }, + expect: " --api-key apikey --force --force-with-deps --ignore-certs --ignore-errors --keep-scm-meta --no-deps --role-file rolefile --roles-path rolespath --server server --timeout timeout --token token -vvvv -v -vv -vvv -vvvv --version", + }, + } + + for _, test := range tests { + + t.Run(test.desc, func(t *testing.T) { + t.Log(test.desc) + + res := test.options.String() + + assert.Equal(t, test.expect, res, "Unexpected options value for AnsibleGalaxyRoleInstallOptions.String()") + + }) + } +}