diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 36121a5743..be6a9d640b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4100,7 +4100,8 @@ class JobLaunchSerializer(BaseSerializer): errors.setdefault('credentials', []).append(_( 'Cannot assign multiple {} credentials.' ).format(cred.unique_hash(display=True))) - if cred.credential_type.kind not in ('ssh', 'vault', 'cloud', 'net'): + if cred.credential_type.kind not in ('ssh', 'vault', 'cloud', + 'net', 'kubernetes'): errors.setdefault('credentials', []).append(_( 'Cannot assign a Credential of kind `{}`' ).format(cred.credential_type.kind)) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index a950ff118f..77481a3917 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -2657,7 +2657,7 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): return {"error": _("Cannot assign multiple {credential_type} credentials.").format( credential_type=sub.unique_hash(display=True))} kind = sub.credential_type.kind - if kind not in ('ssh', 'vault', 'cloud', 'net'): + if kind not in ('ssh', 'vault', 'cloud', 'net', 'kubernetes'): return {'error': _('Cannot assign a Credential of kind `{}`.').format(kind)} return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 15b8229ea2..75d1f17bfe 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -101,3 +101,17 @@ def openstack(cred, env, private_data_dir): f.close() os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) env['OS_CLIENT_CONFIG_FILE'] = path + + +def kubernetes_bearer_token(cred, env, private_data_dir): + env['K8S_AUTH_HOST'] = cred.get_input('host', default='') + env['K8S_AUTH_API_KEY'] = cred.get_input('bearer_token', default='') + if cred.get_input('verify_ssl') and 'ssl_ca_cert' in cred.inputs: + env['K8S_AUTH_VERIFY_SSL'] = 'True' + handle, path = tempfile.mkstemp(dir=private_data_dir) + with os.fdopen(handle, 'w') as f: + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) + f.write(cred.get_input('ssl_ca_cert')) + env['K8S_AUTH_SSL_CA_CERT'] = path + else: + env['K8S_AUTH_VERIFY_SSL'] = 'False' diff --git a/awx/main/tests/functional/api/test_credential_type.py b/awx/main/tests/functional/api/test_credential_type.py index 45b5e79994..c8f87f0c57 100644 --- a/awx/main/tests/functional/api/test_credential_type.py +++ b/awx/main/tests/functional/api/test_credential_type.py @@ -220,7 +220,7 @@ def test_create_valid_kind(kind, get, post, admin): @pytest.mark.django_db -@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights']) +@pytest.mark.parametrize('kind', ['ssh', 'vault', 'scm', 'insights', 'kubernetes']) def test_create_invalid_kind(kind, get, post, admin): response = post(reverse('api:credential_type_list'), { 'kind': kind, diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index d792ec656d..e628623cf1 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -483,25 +483,26 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_ @pytest.mark.django_db -def test_job_launch_JT_with_credentials(machine_credential, credential, net_credential, deploy_jobtemplate): +def test_job_launch_JT_with_credentials(machine_credential, credential, net_credential, kube_credential, deploy_jobtemplate): deploy_jobtemplate.ask_credential_on_launch = True deploy_jobtemplate.save() - kv = dict(credentials=[credential.pk, net_credential.pk, machine_credential.pk]) + kv = dict(credentials=[credential.pk, net_credential.pk, machine_credential.pk, kube_credential.pk]) serializer = JobLaunchSerializer(data=kv, context={'template': deploy_jobtemplate}) validated = serializer.is_valid() assert validated, serializer.errors - kv['credentials'] = [credential, net_credential, machine_credential] # convert to internal value + kv['credentials'] = [credential, net_credential, machine_credential, kube_credential] # convert to internal value prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs( _exclude_errors=['required', 'prompts'], **kv) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) creds = job_obj.credentials.all() - assert len(creds) == 3 + assert len(creds) == 4 assert credential in creds assert net_credential in creds assert machine_credential in creds + assert kube_credential in creds @pytest.mark.django_db diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index f8c8094ac0..354b5a2db5 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1037,6 +1037,43 @@ class TestJobCredentials(TestJobExecution): assert '--vault-id dev@prompt' in ' '.join(args) assert '--vault-id prod@prompt' in ' '.join(args) + @pytest.mark.parametrize("verify", (True, False)) + def test_k8s_credential(self, job, private_data_dir, verify): + k8s = CredentialType.defaults['kubernetes_bearer_token']() + inputs = { + 'host': 'https://example.org/', + 'bearer_token': 'token123', + } + if verify: + inputs['verify_ssl'] = True + inputs['ssl_ca_cert'] = 'CERTDATA' + credential = Credential( + pk=1, + credential_type=k8s, + inputs = inputs, + ) + credential.inputs['bearer_token'] = encrypt_field(credential, 'bearer_token') + job.credentials.add(credential) + + env = {} + safe_env = {} + credential.credential_type.inject_credential( + credential, env, safe_env, [], private_data_dir + ) + + assert env['K8S_AUTH_HOST'] == 'https://example.org/' + assert env['K8S_AUTH_API_KEY'] == 'token123' + + if verify: + assert env['K8S_AUTH_VERIFY_SSL'] == 'True' + cert = open(env['K8S_AUTH_SSL_CA_CERT'], 'r').read() + assert cert == 'CERTDATA' + else: + assert env['K8S_AUTH_VERIFY_SSL'] == 'False' + assert 'K8S_AUTH_SSL_CA_CERT' not in env + + assert safe_env['K8S_AUTH_API_KEY'] == tasks.HIDDEN_PASSWORD + def test_aws_cloud_credential(self, job, private_data_dir): aws = CredentialType.defaults['aws']() credential = Credential( diff --git a/awx/ui/client/lib/components/tag/_index.less b/awx/ui/client/lib/components/tag/_index.less index d53a5e19e6..486b304337 100644 --- a/awx/ui/client/lib/components/tag/_index.less +++ b/awx/ui/client/lib/components/tag/_index.less @@ -67,6 +67,10 @@ &--external:before { content: '\f14c' } + + &--kubernetes:before, &--kubernetes_bearer_token:before { + content: '\f0c2'; + } } .TagComponent-button { diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js index 19e98d7ca3..fee9395c5c 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.directive.js @@ -111,7 +111,7 @@ function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) scope.credentialTypes.forEach((credentialType => { if(credentialType.kind - .match(/^(machine|cloud|net|ssh|vault)$/)) { + .match(/^(machine|cloud|net|ssh|vault|kubernetes)$/)) { scope.displayedCredentialTypes.push(credentialType); } })); diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html index 22c019d4a0..f40c9c55a4 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential-modal.partial.html @@ -29,6 +29,7 @@ +
diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html index 052c81f94f..054d48a29c 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.partial.html @@ -26,6 +26,7 @@ +
@@ -34,6 +35,7 @@ +
diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js index 53a8370a0c..f923c8018e 100644 --- a/awx/ui/client/src/templates/prompt/prompt.controller.js +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -55,7 +55,7 @@ export default [ 'ProcessErrors', 'CredentialTypeModel', 'TemplatesStrings', '$f vm.promptDataClone.prompts.credentials.credentialTypeOptions = []; response.data.results.forEach((credentialTypeRow => { vm.promptDataClone.prompts.credentials.credentialTypes[credentialTypeRow.id] = credentialTypeRow.kind; - if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault)$/)) { + if(credentialTypeRow.kind.match(/^(cloud|net|ssh|vault|kubernetes)$/)) { if(credentialTypeRow.kind === 'ssh') { vm.promptDataClone.prompts.credentials.credentialKind = credentialTypeRow.id.toString(); } diff --git a/awx/ui_next/src/api/models/CredentialTypes.js b/awx/ui_next/src/api/models/CredentialTypes.js index d2d993091c..dab1676231 100644 --- a/awx/ui_next/src/api/models/CredentialTypes.js +++ b/awx/ui_next/src/api/models/CredentialTypes.js @@ -7,7 +7,7 @@ class CredentialTypes extends Base { } async loadAllTypes( - acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault'] + acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault', 'kubernetes'] ) { const pageSize = 200; // The number of credential types a user can have is unlimited. In practice, it is unlikely for diff --git a/docs/licenses/ruamel.ordereddict.txt b/docs/licenses/ruamel.ordereddict.txt new file mode 100644 index 0000000000..0c12e55403 --- /dev/null +++ b/docs/licenses/ruamel.ordereddict.txt @@ -0,0 +1,23 @@ + + The MIT License (MIT) + + Copyright (c) 2007-2017 Anthon van der Neut/Ruamel BVBA + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 31a736e083..a493b4c863 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -32,8 +32,8 @@ django-oauth-toolkit==1.1.3 # via -r /awx_devel/requirements/requirements.in django-pglocks==1.0.4 # via -r /awx_devel/requirements/requirements.in django-polymorphic==2.1.2 # via -r /awx_devel/requirements/requirements.in django-qsstats-magic==1.1.0 # via -r /awx_devel/requirements/requirements.in -django-redis==4.5.0 django-radius==1.3.3 # via -r /awx_devel/requirements/requirements.in +django-redis==4.5.0 # via -r /awx_devel/requirements/requirements.in django-solo==1.1.3 # via -r /awx_devel/requirements/requirements.in django-split-settings==1.0.0 # via -r /awx_devel/requirements/requirements.in django-taggit==1.2.0 # via -r /awx_devel/requirements/requirements.in @@ -100,7 +100,7 @@ python3-openid==3.1.0 # via social-auth-core python3-saml==1.9.0 # via -r /awx_devel/requirements/requirements.in pytz==2019.3 # via django, irc, tempora, twilio pyyaml==5.3.1 # via -r /awx_devel/requirements/requirements.in, ansible-runner, djangorestframework-yaml, kubernetes -redis==3.4.1 # via -r /awx_devel/requirements/requirements.in +redis==3.4.1 # via -r /awx_devel/requirements/requirements.in, django-redis requests-oauthlib==1.3.0 # via kubernetes, msrest, social-auth-core requests==2.23.0 # via -r /awx_devel/requirements/requirements.in, adal, azure-keyvault, django-oauth-toolkit, kubernetes, msrest, requests-oauthlib, slackclient, social-auth-core, twilio rsa==4.0 # via google-auth diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index 6cc129180c..4eaf5f7c1d 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -62,5 +62,7 @@ requests requests-credssp==1.0.2 # For windows authentication awx/issues/1144 # OpenStack openstacksdk==0.37.0 +# Openshift/k8s +openshift>=0.11.0 # minimum version to pull in new pyyaml for CVE-2017-18342 pip==19.3.1 # see upgrade blockers -setuptools==41.6.0 # see upgrade blockers \ No newline at end of file +setuptools==41.6.0 # see upgrade blockers diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index b7d0d1d810..26667ea9a9 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -26,7 +26,7 @@ azure-mgmt-loganalytics==0.2.0 # via -r /awx_devel/requirements/requirements_an azure-mgmt-marketplaceordering==0.1.0 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-monitor==0.5.2 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-network==2.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -azure-mgmt-nspkg==2.0.0 # via -r /awx_devel/requirements/requirements_ansible.in, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web +azure-mgmt-nspkg==2.0.0; python_version < "3" # via -r /awx_devel/requirements/requirements_ansible.in, azure-mgmt-authorization, azure-mgmt-automation, azure-mgmt-batch, azure-mgmt-cdn, azure-mgmt-compute, azure-mgmt-containerinstance, azure-mgmt-containerregistry, azure-mgmt-containerservice, azure-mgmt-cosmosdb, azure-mgmt-devtestlabs, azure-mgmt-dns, azure-mgmt-hdinsight, azure-mgmt-iothub, azure-mgmt-keyvault, azure-mgmt-loganalytics, azure-mgmt-marketplaceordering, azure-mgmt-monitor, azure-mgmt-network, azure-mgmt-rdbms, azure-mgmt-redis, azure-mgmt-resource, azure-mgmt-servicebus, azure-mgmt-sql, azure-mgmt-storage, azure-mgmt-trafficmanager, azure-mgmt-web azure-mgmt-rdbms==1.4.1 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-redis==5.0.0 # via -r /awx_devel/requirements/requirements_ansible.in azure-mgmt-resource==2.1.0 # via -r /awx_devel/requirements/requirements_ansible.in @@ -43,7 +43,7 @@ boto3==1.9.223 # via -r /awx_devel/requirements/requirements_ansible. boto==2.47.0 # via -r /awx_devel/requirements/requirements_ansible.in botocore==1.12.253 # via boto3, s3transfer cachetools==3.1.1 # via google-auth -certifi==2019.11.28 # via msrest, requests +certifi==2019.11.28 # via kubernetes, msrest, requests cffi==1.13.2 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests colorama==0.4.3 # via azure-cli-core, knack @@ -53,18 +53,19 @@ docutils==0.15.2 # via botocore dogpile.cache==0.9.0 # via openstacksdk enum34==1.1.6; python_version < "3" # via cryptography, knack, msrest, ovirt-engine-sdk-python futures==3.3.0; python_version < "3" # via openstacksdk, s3transfer -google-auth==1.6.2 # via -r /awx_devel/requirements/requirements_ansible.in +google-auth==1.6.2 # via -r /awx_devel/requirements/requirements_ansible.in, kubernetes humanfriendly==4.18 # via azure-cli-core idna==2.8 # via requests -ipaddress==1.0.23; python_version < "3" # via cryptography, openstacksdk +ipaddress==1.0.23; python_version < "3" # via cryptography, kubernetes, openstacksdk iso8601==0.1.12 # via keystoneauth1, openstacksdk isodate==0.6.0 # via msrest -jinja2==2.10.1 # via -r /awx_devel/requirements/requirements_ansible.in +jinja2==2.10.1 # via -r /awx_devel/requirements/requirements_ansible.in, openshift jmespath==0.9.4 # via azure-cli-core, boto3, botocore, knack, openstacksdk jsonpatch==1.24 # via openstacksdk jsonpointer==2.0 # via jsonpatch keystoneauth1==3.18.0 # via openstacksdk knack==0.3.3 # via azure-cli-core +kubernetes==11.0.0 # via openshift lxml==4.4.2 # via ncclient markupsafe==1.1.1 # via jinja2 monotonic==1.5; python_version < "3" # via humanfriendly @@ -76,6 +77,7 @@ netaddr==0.7.19 # via -r /awx_devel/requirements/requirements_ansible. netifaces==0.10.9 # via openstacksdk ntlm-auth==1.4.0 # via requests-credssp, requests-ntlm oauthlib==3.1.0 # via requests-oauthlib +openshift==0.11.2 # via -r /awx_devel/requirements/requirements_ansible.in openstacksdk==0.37.0 # via -r /awx_devel/requirements/requirements_ansible.in os-service-types==1.7.0 # via keystoneauth1, openstacksdk ovirt-engine-sdk-python==4.3.0 # via -r /awx_devel/requirements/requirements_ansible.in @@ -93,27 +95,32 @@ pykerberos==1.2.1 # via requests-kerberos pynacl==1.3.0 # via paramiko pyopenssl==19.1.0 # via azure-cli-core, requests-credssp pyparsing==2.4.5 # via packaging -python-dateutil==2.8.1 # via adal, azure-storage, botocore +python-dateutil==2.8.1 # via adal, azure-storage, botocore, kubernetes +python-string-utils==0.6.0; python_version < "3" # via openshift pyvmomi==6.7.3 # via -r /awx_devel/requirements/requirements_ansible.in pywinrm[kerberos]==0.3.0 # via -r /awx_devel/requirements/requirements_ansible.in -pyyaml==5.2 # via azure-cli-core, knack, openstacksdk +pyyaml==5.2 # via azure-cli-core, knack, kubernetes, openstacksdk requests-credssp==1.0.2 # via -r /awx_devel/requirements/requirements_ansible.in requests-kerberos==0.12.0 # via pywinrm requests-ntlm==1.1.0 # via pywinrm -requests-oauthlib==1.3.0 # via msrest -requests==2.22.0 # via -r /awx_devel/requirements/requirements_ansible.in, adal, apache-libcloud, azure-cli-core, azure-keyvault, azure-storage, keystoneauth1, msrest, pyvmomi, pywinrm, requests-credssp, requests-kerberos, requests-ntlm, requests-oauthlib +requests-oauthlib==1.3.0 # via kubernetes, msrest +requests==2.22.0 # via -r /awx_devel/requirements/requirements_ansible.in, adal, apache-libcloud, azure-cli-core, azure-keyvault, azure-storage, keystoneauth1, kubernetes, msrest, pyvmomi, pywinrm, requests-credssp, requests-kerberos, requests-ntlm, requests-oauthlib requestsexceptions==1.4.0 # via openstacksdk rsa==4.0 # via google-auth +ruamel.ordereddict==0.4.14; python_version < "3" # via ruamel.yaml +ruamel.yaml.clib==0.2.0 # via ruamel.yaml +ruamel.yaml==0.16.10 # via openshift s3transfer==0.2.1 # via boto3 selectors2==2.0.1 # via ncclient -six==1.13.0 # via azure-cli-core, bcrypt, cryptography, google-auth, isodate, keystoneauth1, knack, munch, ncclient, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, pyvmomi, pywinrm, requests-credssp, stevedore +six==1.13.0 # via azure-cli-core, bcrypt, cryptography, google-auth, isodate, keystoneauth1, knack, kubernetes, munch, ncclient, openshift, openstacksdk, ovirt-engine-sdk-python, packaging, pynacl, pyopenssl, python-dateutil, pyvmomi, pywinrm, requests-credssp, stevedore, websocket-client stevedore==1.31.0 # via keystoneauth1 tabulate==0.8.2 # via azure-cli-core, knack typing==3.7.4.1; python_version < "3" # via msrest -urllib3==1.25.7 # via botocore, requests +urllib3==1.25.7 # via botocore, kubernetes, requests +websocket-client==0.57.0 # via kubernetes wheel==0.33.6 # via azure-cli-core (overriden, see upgrade blockers) xmltodict==0.12.0 # via pywinrm # The following packages are considered to be unsafe in a requirements file: pip==19.3.1 # via -r /awx_devel/requirements/requirements_ansible.in, azure-cli-core -setuptools==41.6.0 # via -r /awx_devel/requirements/requirements_ansible.in, ncclient +setuptools==41.6.0 # via -r /awx_devel/requirements/requirements_ansible.in, kubernetes, ncclient