diff --git a/changelog/fragments/arbitraryArgs.yaml b/changelog/fragments/arbitraryArgs.yaml new file mode 100644 index 0000000000..5229567281 --- /dev/null +++ b/changelog/fragments/arbitraryArgs.yaml @@ -0,0 +1,10 @@ +entries: + - description: > + Added the "--ansible-args" command-line flag that allows users to specify arbitrary + CLI arguments for ansible-based operators that are passed through ansible-runner. + For example, passing --ansible-vault as an arbitrary argument allows the user to store + sensitive data in encrypted files. + + kind: "addition" + + breaking: false diff --git a/internal/ansible/flags/flag.go b/internal/ansible/flags/flag.go index 350bf31efd..8e8168d78a 100644 --- a/internal/ansible/flags/flag.go +++ b/internal/ansible/flags/flag.go @@ -34,6 +34,7 @@ type Flags struct { MetricsAddress string LeaderElectionID string LeaderElectionNamespace string + AnsibleArgs string } const AnsibleRolesPathEnvVar = "ANSIBLE_ROLES_PATH" @@ -99,4 +100,9 @@ func (f *Flags) AddTo(flagSet *pflag.FlagSet) { " holding the leader lock (required if running locally with leader"+ " election enabled).", ) + flagSet.StringVar(&f.AnsibleArgs, + "ansible-args", + "", + "Ansible args. Allows user to specify arbitrary arguments for ansible-based operators.", + ) } diff --git a/internal/ansible/runner/internal/inputdir/inputdir.go b/internal/ansible/runner/internal/inputdir/inputdir.go index 3bc4c96c6a..1a3ac5205e 100644 --- a/internal/ansible/runner/internal/inputdir/inputdir.go +++ b/internal/ansible/runner/internal/inputdir/inputdir.go @@ -36,6 +36,7 @@ type InputDir struct { Parameters map[string]interface{} EnvVars map[string]string Settings map[string]string + CmdLine string } // makeDirs creates the required directory structure. @@ -131,6 +132,19 @@ func (i *InputDir) Write() error { return err } + // Trimming off the first and last characters if the command is wrapped by single quotations + if strings.HasPrefix(i.CmdLine, string("'")) && i.CmdLine[0] == i.CmdLine[len(i.CmdLine)-1] { + i.CmdLine = i.CmdLine[1 : len(i.CmdLine)-1] + } + + cmdLineBytes := []byte(i.CmdLine) + if len(cmdLineBytes) > 0 { + err = i.addFile("env/cmdline", cmdLineBytes) + if err != nil { + return err + } + } + // ANSIBLE_INVENTORY takes precedence over our generated hosts file // so if the envvar is set we don't bother making it, we just copy // the inventory into our runner directory diff --git a/internal/ansible/runner/runner.go b/internal/ansible/runner/runner.go index 0d99c080f2..6b20c9eb64 100644 --- a/internal/ansible/runner/runner.go +++ b/internal/ansible/runner/runner.go @@ -89,17 +89,17 @@ func roleCmdFunc(path string) cmdFuncType { // check the verbosity since the exec.Command will fail if an arg as "" or " " be informed if verbosity > 0 { return exec.Command("ansible-runner", ansibleVerbosityString(verbosity), "--rotate-artifacts", - fmt.Sprintf("%v", maxArtifacts), "--role", roleName, "--roles-path", rolePath, "--hosts", - "localhost", "-i", ident, "run", inputDirPath) + fmt.Sprintf("%v", maxArtifacts), "--role", roleName, "--roles-path", rolePath, + "--hosts", "localhost", "-i", ident, "run", inputDirPath) } return exec.Command("ansible-runner", "--rotate-artifacts", - fmt.Sprintf("%v", maxArtifacts), "--role", roleName, "--roles-path", rolePath, "--hosts", - "localhost", "-i", ident, "run", inputDirPath) + fmt.Sprintf("%v", maxArtifacts), "--role", roleName, "--roles-path", rolePath, + "--hosts", "localhost", "-i", ident, "run", inputDirPath) } } // New - creates a Runner from a Watch struct -func New(watch watches.Watch) (Runner, error) { +func New(watch watches.Watch, runnerArgs string) (Runner, error) { var path string var cmdFunc, finalizerCmdFunc cmdFuncType @@ -139,6 +139,7 @@ func New(watch watches.Watch) (Runner, error) { GVK: watch.GroupVersionKind, maxRunnerArtifacts: watch.MaxRunnerArtifacts, ansibleVerbosity: watch.AnsibleVerbosity, + ansibleArgs: runnerArgs, snakeCaseParameters: watch.SnakeCaseParameters, }, nil } @@ -154,6 +155,7 @@ type runner struct { maxRunnerArtifacts int ansibleVerbosity int snakeCaseParameters bool + ansibleArgs string } func (r *runner) Run(ident string, u *unstructured.Unstructured, kubeconfig string) (RunResult, error) { @@ -188,6 +190,7 @@ func (r *runner) Run(ident string, u *unstructured.Unstructured, kubeconfig stri "runner_http_url": receiver.SocketPath, "runner_http_path": receiver.URLPath, }, + CmdLine: r.ansibleArgs, } // If Path is a dir, assume it is a role path. Otherwise assume it's a // playbook path diff --git a/internal/ansible/runner/runner_test.go b/internal/ansible/runner/runner_test.go index 0c369438ba..4133c1a835 100644 --- a/internal/ansible/runner/runner_test.go +++ b/internal/ansible/runner/runner_test.go @@ -159,7 +159,7 @@ func TestNew(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testWatch := watches.New(tc.gvk, tc.role, tc.playbook, tc.vars, tc.finalizer) - testRunner, err := New(*testWatch) + testRunner, err := New(*testWatch, "") if err != nil { t.Fatalf("Error occurred unexpectedly: %v", err) } diff --git a/internal/cmd/ansible-operator/run/cmd.go b/internal/cmd/ansible-operator/run/cmd.go index c3828aec44..b825c320a1 100644 --- a/internal/cmd/ansible-operator/run/cmd.go +++ b/internal/cmd/ansible-operator/run/cmd.go @@ -161,7 +161,7 @@ func run(cmd *cobra.Command, f *flags.Flags) { os.Exit(1) } for _, w := range watches { - runner, err := runner.New(w) + runner, err := runner.New(w, f.AnsibleArgs) if err != nil { log.Error(err, "Failed to create runner") os.Exit(1) diff --git a/test/ansible/build/Dockerfile b/test/ansible/build/Dockerfile index 9f9f4e9ce3..8b89901254 100644 --- a/test/ansible/build/Dockerfile +++ b/test/ansible/build/Dockerfile @@ -15,4 +15,6 @@ USER root RUN chmod -R ug+rwx /tmp/fixture_collection USER 1001 RUN ansible-galaxy collection build /tmp/fixture_collection/ --output-path /tmp/fixture_collection/ \ - && ansible-galaxy collection install /tmp/fixture_collection/operator_sdk-test_fixtures-0.0.0.tar.gz + && ansible-galaxy collection install /tmp/fixture_collection/operator_sdk-test_fixtures-0.0.0.tar.gz +RUN echo abc123 > /opt/ansible/pwd.yml \ + && ansible-vault encrypt_string --vault-password-file /opt/ansible/pwd.yml 'thisisatest' --name 'the_secret' > /opt/ansible/vars.yml \ No newline at end of file diff --git a/test/ansible/deploy/crds/test.example.com_argstest_crd.yaml b/test/ansible/deploy/crds/test.example.com_argstest_crd.yaml new file mode 100644 index 0000000000..8b3ebe8b21 --- /dev/null +++ b/test/ansible/deploy/crds/test.example.com_argstest_crd.yaml @@ -0,0 +1,22 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: argstests.test.example.com +spec: + group: test.example.com + names: + kind: ArgsTest + listKind: ArgsTestList + plural: argstests + singular: argstest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + served: true + storage: true + subresources: + status: {} diff --git a/test/ansible/molecule/cluster/tasks/args_test.yml b/test/ansible/molecule/cluster/tasks/args_test.yml new file mode 100644 index 0000000000..aa91e3b983 --- /dev/null +++ b/test/ansible/molecule/cluster/tasks/args_test.yml @@ -0,0 +1,26 @@ +--- +- name: Create the test.example.com/v1alpha1.ArgsTest + k8s: + state: present + definition: + apiVersion: test.example.com/v1alpha1 + kind: ArgsTest + metadata: + name: args-test + namespace: '{{ namespace }}' + spec: + field: value + wait: yes + wait_timeout: 300 + wait_condition: + type: Running + reason: Successful + status: "True" + register: args_test + +- name: Assert sentinel ConfigMap has been created for Molecule Test + assert: + that: cm.data.msg == "The decrypted value is thisisatest" + vars: + cm: "{{ q('k8s', api_version='v1', kind='ConfigMap', namespace=namespace, + resource_name='args-test').0 }}" diff --git a/test/ansible/molecule/templates/operator.yaml.j2 b/test/ansible/molecule/templates/operator.yaml.j2 index 8c72325a11..ac940a6030 100644 --- a/test/ansible/molecule/templates/operator.yaml.j2 +++ b/test/ansible/molecule/templates/operator.yaml.j2 @@ -48,6 +48,7 @@ spec: port: 6789 initialDelaySeconds: 5 periodSeconds: 3 + args: ["--ansible-args='--vault-password-file /opt/ansible/pwd.yml'"] volumes: - name: runner emptyDir: {} diff --git a/test/ansible/playbooks/args.yml b/test/ansible/playbooks/args.yml new file mode 100644 index 0000000000..c7a6670d12 --- /dev/null +++ b/test/ansible/playbooks/args.yml @@ -0,0 +1,20 @@ +--- +- hosts: localhost + gather_facts: no + collections: + - community.kubernetes + tasks: + - name: Get the decrypted message variable + include_vars: + file: /opt/ansible/vars.yml + name: the_secret + - name: Create configmap + k8s: + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: '{{ meta.name }}' + namespace: '{{ meta.namespace }}' + data: + msg: The decrypted value is {{the_secret.the_secret}} diff --git a/test/ansible/watches.yaml b/test/ansible/watches.yaml index 2a76caf314..a658f5447d 100644 --- a/test/ansible/watches.yaml +++ b/test/ansible/watches.yaml @@ -45,3 +45,10 @@ snakeCaseParameters: false vars: meta: '{{ ansible_operator_meta }}' + +- version: v1alpha1 + group: test.example.com + kind: ArgsTest + playbook: playbooks/args.yml + vars: + meta: '{{ ansible_operator_meta }}' diff --git a/website/content/en/docs/building-operators/ansible/reference/advanced_options.md b/website/content/en/docs/building-operators/ansible/reference/advanced_options.md index 3ec789c336..487c2c27d3 100644 --- a/website/content/en/docs/building-operators/ansible/reference/advanced_options.md +++ b/website/content/en/docs/building-operators/ansible/reference/advanced_options.md @@ -211,3 +211,51 @@ spec: served: true storage: true ``` + +## Passing Arbitrary Arguments to Ansible + +You are able to use the flag `--ansible-args` to pass an arbitrary argument to the Ansible-based Operator. With this option we can, for example, allow a playbook to run a specific part of the configuration without running the whole playbook: + +```shell +ansible-operator run --ansible-args='--tags "configuration,packages"' +``` +``` +ansible-operator run --ansible-args='--skip-tags "notification"' +``` +Ansible-runner will perform the task relevant to the command specified by the user in the ```---ansible-args``` flag. + + +## Using Ansible-Vault + +[Ansible Vault][ansible-vault-doc] allows you to keep sensitive data such as passwords or keys in encrypted files, rather than as plaintext in playbooks or roles. You can specify Ansible-Vault file via an arbitrary argument by using the `--ansible-args` flag. For example, let's assume that a playbook reads in a file `vars.yml` which contains an encrypted text and stores it in a variable `secret`: + +``` +--- +- name: Playbook to print debug messages + gather_facts: false + hosts: localhost + tasks: + - name: Get the decrypted message variable + include_vars: + file: vars.yml + name: secret + - debug: + msg: The decrypted value is {{secret.the_secret}} +``` + +Now, let's also assume that we have a password file, `pwd.yml`, that contains the password to decrypt the encrypted text. Then, by running the command `ansible-operator run --ansible-args='--vault-password-file pwd.yml'` the operator will read in the encrypted text from the file and perform decryption using the password stored in the `pwd.yml` file: + +``` +--------------------------- Ansible Task StdOut ------------------------------- + + TASK [debug] ******************************** +ok: [localhost] => { + "msg": "The decrypted value is DECRYPTED-TEST-VALUE" +} + +------------------------------------------------------------------------------- +``` +[ansible-vault-doc]: https://docs.ansible.com/ansible/latest/user_guide/vault.html + + +