diff --git a/roles/harbor/defaults/main.yml b/roles/harbor/defaults/main.yml index 10d56c0..b96ac4e 100644 --- a/roles/harbor/defaults/main.yml +++ b/roles/harbor/defaults/main.yml @@ -57,3 +57,68 @@ harbor_base_configuration: oidc_verify_cert: true oidc_auto_onboard: true oidc_admin_group: '/admin' + scan_all_policy: + parameter: + daily_time: 0 + +project_object_template: + project_attributes: + project_name: '{{ elem }}' + meta_data: + auto_scan: true + project_state: present + members: + - + group_name: '/{{ elem }}' + group_type: oidc + role: projectadmin + +harbor_projects_smardigo_default: + - awx + - sensw + - smardigo + +harbor_projects: [] + +harbor_robot_tokens: + - +# secret_refresh: True +# token_state: present + name: ansible + level: system + description: 'smardigo docker pull credentials' + secret: '{{ docker_registry_token }}' + disable: false + duration: -1 + editable: true + expires_at: -1 + permissions: + - access: + - action: push + resource: repository + - action: pull + resource: repository + - action: delete + resource: artifact + - action: read + resource: helm-chart + - action: create + resource: helm-chart-version + - action: delete + resource: helm-chart-version + - action: create + resource: tag + - action: delete + resource: tag + - action: create + resource: artifact-label + - action: create + resource: scan + kind: project + namespace: "*" + +harbor_scanall: + - + schedule: + cron: 0 0 1 * * * + type: Custom diff --git a/roles/harbor/tasks/configure.yml b/roles/harbor/tasks/configure.yml new file mode 100644 index 0000000..8341055 --- /dev/null +++ b/roles/harbor/tasks/configure.yml @@ -0,0 +1,80 @@ +--- + +- name: "harbor BASE settings" + block: + - name: "BLOCK: Login with keycloak-admin" + include_role: + name: keycloak + tasks_from: _authenticate + + - name: "GET available clients from <<{{ harbor_base_configuration.oidc_name }}>>-realm" + delegate_to: localhost + become: False + uri: + url: "{{ keycloak_server_url }}/auth/admin/realms/{{ harbor_base_configuration.oidc_name }}/clients" + method: GET + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ access_token }}" + status_code: [200] + register: realm_clients + + # available clients: get needed ID + - set_fact: + id_of_client: '{{ ( realm_clients.json | selectattr("clientId","equalto", harbor_base_configuration.oidc_client_id ) | first ).id }}' + + - name: "BLOCK: GET client-secret for client <<{{ harbor_base_configuration.oidc_client_id }}>> in realm <<{{ harbor_base_configuration.oidc_name }}>>" + delegate_to: localhost + become: False + uri: + url: "{{ keycloak_server_url }}/auth/admin/realms/{{ harbor_base_configuration.oidc_name }}/clients/{{ id_of_client }}/client-secret" + method: GET + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ access_token }}" + status_code: [200] + register: client_secret + + - set_fact: + dict: + oidc_client_secret: '{{ client_secret.json.value }}' + + - set_fact: + harbor_base_configuration_merged: '{{ harbor_base_configuration | combine( dict ,recursive=True ) }}' + + - name: "BLOCK: Configure harbor BASE settings" + include_tasks: configure_base_config.yml + vars: + base_configuration: '{{ harbor_base_configuration_merged }}' + args: + apply: + tags: + - harbor-configure-base +# end of block for base settings + +- name: "Create object of templated harbor projects" + set_fact: + projects_templated: "{{ ( projects_templated | default([]) ) + [ project_object_template ] }}" + loop: '{{ harbor_projects_smardigo_default }}' + loop_control: + loop_var: elem + when: + - harbor_projects_smardigo_default is defined + +- name: "CRUD - projects" + include_tasks: configure_projects.yml + loop: '{{ harbor_projects + projects_templated }}' + loop_control: + loop_var: project + +- name: "CRUD - robot tokens" + include_tasks: configure_robot_tokens.yml + loop: '{{ harbor_robot_tokens }}' + loop_control: + loop_var: robot_token + +- name: "CRUD - scanall schedule" + include_tasks: configure_scanall_schedule.yml + loop: '{{ harbor_scanall }}' + loop_control: + loop_var: scanschedule diff --git a/roles/harbor/tasks/configure_project_crud.yml b/roles/harbor/tasks/configure_project_crud.yml new file mode 100644 index 0000000..53799e5 --- /dev/null +++ b/roles/harbor/tasks/configure_project_crud.yml @@ -0,0 +1,85 @@ +--- + +- name: "Check if project <<{{ project.project_attributes.project_name }}>> exists" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project.project_attributes.project_name }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: GET + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,403] + register: project_exists + delay: 10 + retries: 3 + +- debug: + msg: 'found projects: {{ project_exists.json }}' + when: debug + +- name: "Create project: <<{{ project.project_attributes.project_name }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: POST + body_format: json + body: '{{ project.project_attributes | to_json }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: create_project + delay: 10 + retries: 3 + until: create_project.status in [200,201] + when: + - project_exists.status in [403] + +- name: "Update project: <<{{ project.project_attributes.project_name }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project.project_attributes.project_name }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PUT + body_format: json + body: '{{ project.project_attributes | to_json }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: update_project + delay: 10 + retries: 3 + until: update_project.status in [200,201] + when: + - project_exists.status in [200] + +- name: "Delete project: <<{{ project.project_attributes.project_name }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project.project_attributes.project_name }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: DELETE + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: create_project + delay: 10 + retries: 3 + until: create_project.status in [200] + when: + - project_exists.status in [200] + - project.project_state == 'absent' diff --git a/roles/harbor/tasks/configure_project_members_crud.yml b/roles/harbor/tasks/configure_project_members_crud.yml new file mode 100644 index 0000000..d0b7a0c --- /dev/null +++ b/roles/harbor/tasks/configure_project_members_crud.yml @@ -0,0 +1,137 @@ +--- + +- set_fact: + member_state: '{{ member.member_state | default("present") }}' + harbor_member_roles: + - + name: projectadmin + role_id: 1 + - + name: developer + role_id: 2 + - + name: guest + role_id: 3 + - + name: maintainer + role_id: 4 + harbor_member_grouptypes: + - + name: ldap + group_type: 1 + - + name: http + group_type: 2 + - + name: oidc + group_type: 3 + +- name: "Get all project members" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/members" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: GET + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: all_project_members + delay: 10 + retries: 3 + +- set_fact: + group_type: "{{ ( harbor_member_grouptypes | selectattr('name','==',( member.group_type | lower )) | list | first ).group_type }}" + role_id: "{{ ( harbor_member_roles | selectattr('name','==',( member.role| lower ) ) | list | first ).role_id | int }}" + +# creating body manual due to problems with IDs as integer - they will be converted to string in json +# => every API request will fail +# see also: +# https://stackoverflow.com/questions/69677986/converting-string-to-integer-in-ansible +- name: "Create membership" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/members" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: POST + body_format: json + body: >- + {{ + ( + { + "role_id": role_id | int, + "member_group": { + "group_name": member.group_name, + "group_type": group_type | int + } + } + ) | to_json }} + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: create_project_member + delay: 10 + retries: 3 + until: create_project_member.status in [200,201] + when: + - all_project_members.json | selectattr('entity_name','equalto',member.group_name) | list | length == 0 + - member_state == 'present' + +- name: "Update member: <<{{ member.group_name }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/members/{{ ( all_project_members.json | selectattr('entity_name','equalto',member.group_name) | list | first ).id }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PUT + body_format: json + body: >- + {{ + ( + { + "role_id": role_id | int, + "member_group": { + "group_name": member.group_name, + "group_type": group_type | int + } + } + ) | to_json }} + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: update_project_member + delay: 10 + retries: 3 + until: update_project_member.status in [200,201] + when: + - all_project_members.json | selectattr('entity_name','equalto',member.group_name) | list | length == 1 + - member_state == 'present' + +- name: "Delete member: <<{{ member.group_name }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/members/{{ ( all_project_members.json | selectattr('entity_name','equalto',member.group_name) | list | first ).id }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: DELETE + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: delete_project_member + delay: 10 + retries: 3 + until: delete_project_member.status in [200,201] + when: + - all_project_members.json | selectattr('entity_name','equalto',member.group_name) | list | length == 1 + - member_state == 'absent' diff --git a/roles/harbor/tasks/configure_project_metadata_crud.yml b/roles/harbor/tasks/configure_project_metadata_crud.yml new file mode 100644 index 0000000..7bd7ef8 --- /dev/null +++ b/roles/harbor/tasks/configure_project_metadata_crud.yml @@ -0,0 +1,65 @@ +--- + +- name: "Get all meta_data" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/metadatas/{{ meta_data_elem.key }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: GET + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: all_metadata + delay: 10 + retries: 3 + +- set_fact: + body_content: "{ \"{{ meta_data_elem.key }}\":\"{{ meta_data_elem.value }}\" }" + +- name: "Add meta_data: <<{{ meta_data_elem.key }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/metadatas" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: POST + body_format: json + body: '{{ body_content }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: create_metadata + delay: 10 + retries: 3 + until: create_metadata.status in [200,201] + when: + - meta_data_elem.key not in all_metadata.json + +- name: "Update meta_data: <<{{ meta_data_elem.key }}>>" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/projects/{{ project_name }}/metadatas/{{ meta_data_elem.key }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PUT + body_format: json + body: '{{ body_content }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: update_metadata + delay: 10 + retries: 3 + until: update_metadata.status in [200,201] + when: + - meta_data_elem.key in all_metadata.json + +# DELETION currently out-of-scope diff --git a/roles/harbor/tasks/configure_projects.yml b/roles/harbor/tasks/configure_projects.yml new file mode 100644 index 0000000..fb9c6a9 --- /dev/null +++ b/roles/harbor/tasks/configure_projects.yml @@ -0,0 +1,22 @@ +--- + +- name: "include CRUD for projects" + include_tasks: configure_project_crud.yml + +- name: "include CRUD for project meta-data" + include_tasks: configure_project_metadata_crud.yml + vars: + project_name: '{{ project.project_attributes.project_name }}' + loop: '{{ project.meta_data | dict2items }}' + loop_control: + loop_var: meta_data_elem + when: + - project.meta_data is defined + +- name: "include CRUD for project members" + include_tasks: configure_project_members_crud.yml + vars: + project_name: '{{ project.project_attributes.project_name }}' + loop: '{{ project.members }}' + loop_control: + loop_var: member diff --git a/roles/harbor/tasks/configure_robot_tokens.yml b/roles/harbor/tasks/configure_robot_tokens.yml new file mode 100644 index 0000000..db766f4 --- /dev/null +++ b/roles/harbor/tasks/configure_robot_tokens.yml @@ -0,0 +1,20 @@ +--- +- set_fact: + tok_obj: {} + +- debug: + msg: "DEBUGGING - robot_token: {{ robot_token }}" + when: + - debug + +- name: "Drop token_state from dict to avoid rejecting object by harbor API due to unknown field" + set_fact: + tok_obj: "{{ tok_obj |combine({item.key: item.value})}}" + when: item.key not in ['token_state'] + with_dict: "{{ robot_token }}" + +- name: + include_tasks: configure_robot_tokens_crud.yml + vars: + token_state: "{{ robot_token.token_state | default('present') }}" + token_object: "{{ tok_obj }}" diff --git a/roles/harbor/tasks/configure_robot_tokens_crud.yml b/roles/harbor/tasks/configure_robot_tokens_crud.yml new file mode 100644 index 0000000..09a872b --- /dev/null +++ b/roles/harbor/tasks/configure_robot_tokens_crud.yml @@ -0,0 +1,174 @@ +--- +- set_fact: + token_object_combined: {} + +- name: "Get all robot tokens" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/robots" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: GET + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: all_robot_tokens + delay: 10 + retries: 3 + +- name: "Create robot token" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/robots" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: POST + body_format: json + body: '{{ token_object | to_json }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200,201] + register: create + delay: 10 + retries: 3 + until: create.status in [200,201] + when: + - all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | length == 0 + - token_state == 'present' + +- set_fact: + robots_id: "{{ ( all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | first ).id }}" + remote_robot_token_object: "{{ all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | first }}" + token_object_combined: "{{ all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | first | combine(token_object, recursive=True) }}" + token_object_dropped: {} + when: + - all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | length == 1 + +- name: "Refresh the robot secret" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/robots/{{ robots_id }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PATCH + body_format: json + body: >- + {{ + ( + { + "secret": token_object.secret + } + ) + }} + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: update + delay: 10 + retries: 3 + until: update.status in [200] + when: + - all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | length == 1 + - token_state == 'present' + - token_object.secret_refresh is defined + - token_object.secret_refresh + +- name: "Block to Update robot token data" + block: + - debug: + msg: "DEBUGGING before dropping - combined token_object_combined: {{ token_object_combined }}" + when: + - debug + + # unknown param/key in object robot-token will result in errors with harbor API + # therefore we drop $keys from dict + - name: "Drop some keys from updated robot token object" + set_fact: + token_object_dropped: "{{ token_object_dropped | combine({item.key: item.value})}}" + with_dict: "{{ token_object_combined }}" + when: "{{ item.key not in ['secret','secret_refresh'] }}" + + # harbor API behaviour: + # in case of initial creation for robot token objects, harbor creates a name for this + # in form of << robot$OBJECT_NAME >> - plz be aware of the dollar sign! + # but only the OBJECT_NAME was defined in object declaration. + # In case of updating we have to make sure that the << robot$OBJECT_NAME >> is used in the + # updated object thrown against harbor API. + # + # so harbor API forces me to create this workaround to avoid such errors + # + # part 1: define name of object + - set_fact: + robot_token_name_cleaned: + name: 'robot${{ token_object_dropped.name }}' + # part 2: override name with new defined name of object + - set_fact: + token_object_finished: '{{ token_object_dropped | combine(robot_token_name_cleaned, recursive=True) }}' + + - debug: + msg: "DEBUGGING after dropping - combined token_object_finished: {{ token_object_finished }}" + when: + - debug + + # to update a robot token, the following conditions must be satisfied + # 1. ALL params of robot token object must be set + # 1.1. except the secret param - it must be removed/rejected from object - it will be updated with PATCH-method instead of PUT-method + # 2. the update (of parameter) itself + # + # there is no possibility to update if one of mentioned conditions is not statisfied. + # the API call will fail with one of the following errors: + # - HTTP 400 - "cannot update the level or name of robot" + # - HTTP 400 - "bad request error level input:" + # + - name: "Update robot token object" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/robots/{{ robots_id }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PUT + body_format: json + body: '{{ token_object_finished | to_json }}' + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: update + delay: 10 + retries: 3 + until: update.status in [200] +# when - part of BLOCK-statement + when: + - all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | length == 1 + - token_state == 'present' + +# end of BLOCK to Update robot token data + +- name: "Delete robot token" + delegate_to: 127.0.0.1 + become: false + uri: + url: "{{ harbor_external_url }}/api/v2.0/robots/{{ robots_id }}" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: DELETE + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + register: delete_project_member + delay: 10 + retries: 3 + until: delete_project_member.status in [200] + when: + - all_robot_tokens.json | selectattr('name','contains',token_object.name) | list | length == 1 + - token_state == 'absent' diff --git a/roles/harbor/tasks/configure_scanall_schedule.yml b/roles/harbor/tasks/configure_scanall_schedule.yml new file mode 100644 index 0000000..b3be9aa --- /dev/null +++ b/roles/harbor/tasks/configure_scanall_schedule.yml @@ -0,0 +1,30 @@ +--- +- name: "configure | configure scanall schedule | CREATE scanschedule" + uri: + url: "{{ harbor_external_url }}/api/v2.0/system/scanAll/schedule" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: POST + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + status_code: [200] + body: '{{ scanschedule |to_json }}' + status_code: [201,412] + register: create_scanschedule + +- name: "configure | configure scanall schedule | UPDATE scanschedule" + uri: + url: "{{ harbor_external_url }}/api/v2.0/system/scanAll/schedule" + user: '{{ harbor_admin_username }}' + password: '{{ harbor_admin_password }}' + method: PUT + body_format: json + force_basic_auth: yes + headers: + Content-Type: application/json + body: '{{ scanschedule |to_json }}' + status_code: [200] + when: + - create_scanschedule.status in [412] diff --git a/roles/harbor/tasks/main.yml b/roles/harbor/tasks/main.yml index c9a4c13..e51178c 100644 --- a/roles/harbor/tasks/main.yml +++ b/roles/harbor/tasks/main.yml @@ -7,54 +7,9 @@ tags: - harbor-install -- name: "harbor BASE settings" - block: - - name: "BLOCK: Login with keycloak-admin" - include_role: - name: keycloak - tasks_from: _authenticate - - - name: "GET available clients from <<{{ harbor_base_configuration.oidc_name }}>>-realm" - delegate_to: localhost - become: False - uri: - url: "{{ keycloak_server_url }}/auth/admin/realms/{{ harbor_base_configuration.oidc_name }}/clients" - method: GET - headers: - Content-Type: "application/json" - Authorization: "Bearer {{ access_token }}" - status_code: [200] - register: realm_clients - - # available clients: get needed ID - - set_fact: - id_of_client: '{{ ( realm_clients.json | selectattr("clientId","equalto", harbor_base_configuration.oidc_client_id ) | first ).id }}' - - - name: "BLOCK: GET client-secret for client <<{{ harbor_base_configuration.oidc_client_id }}>> in realm <<{{ harbor_base_configuration.oidc_name }}>>" - delegate_to: localhost - become: False - uri: - url: "{{ keycloak_server_url }}/auth/admin/realms/{{ harbor_base_configuration.oidc_name }}/clients/{{ id_of_client }}/client-secret" - method: GET - headers: - Content-Type: "application/json" - Authorization: "Bearer {{ access_token }}" - status_code: [200] - register: client_secret - - - set_fact: - dict: - oidc_client_secret: '{{ client_secret.json.value }}' - - - set_fact: - harbor_base_configuration_merged: '{{ harbor_base_configuration | combine( dict ,recursive=True ) }}' - - - name: "BLOCK: Configure harbor BASE settings" - include_tasks: configure_base_config.yml - vars: - base_configuration: '{{ harbor_base_configuration_merged }}' - args: - apply: - tags: - - harbor-configure-base -# end of block for base settings +- name: "Configure harbor" + include_tasks: configure.yml + args: + apply: + tags: + - harbor-configure