Merge pull request #3780 from ywk253100/171213_merge
Merge master branch codes
36
.drone.ova.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Harbor OVA drone.
|
||||
---
|
||||
workspace:
|
||||
base: /drone
|
||||
path: src/github.com/vmware/harbor
|
||||
|
||||
pipeline:
|
||||
clone:
|
||||
image: plugins/git
|
||||
tags: true
|
||||
recursive: false
|
||||
|
||||
integration-test-on-pr:
|
||||
image: vmware/harbor-e2e-engine:1.39
|
||||
pull: true
|
||||
privileged: true
|
||||
environment:
|
||||
BIN: bin
|
||||
GOPATH: /drone
|
||||
SHELL: /bin/bash
|
||||
LOG_TEMP_DIR: install-logs
|
||||
HARBOR_ADMIN: ${HARBOR_ADMIN}
|
||||
HARBOR_PASSWORD: ${HARBOR_PASSWORD}
|
||||
DHCP: ${DHCP}
|
||||
PROTOCOL: ${PROTOCOL}
|
||||
USER: ${USER}
|
||||
PASSWORD: ${PASSWORD}
|
||||
HOST: ${HOST}
|
||||
DATASTORE: ${DATASTORE}
|
||||
CLUSTER: ${CLUSTER}
|
||||
DATACENTER: ${DATACERTER}
|
||||
commands:
|
||||
- tests/integration_ova.sh
|
||||
when:
|
||||
status: success
|
||||
|
27
.drone.yml
|
@ -11,7 +11,7 @@ pipeline:
|
|||
recursive: false
|
||||
|
||||
integration-test-on-pr:
|
||||
image: vmware/harbor-e2e-engine:1.37
|
||||
image: vmware/harbor-e2e-engine:1.38
|
||||
pull: true
|
||||
privileged: true
|
||||
environment:
|
||||
|
@ -29,13 +29,15 @@ pipeline:
|
|||
GS_PRIVATE_KEY: ${GS_PRIVATE_KEY}
|
||||
DOMAIN: ${CI_DOMAIN}
|
||||
MAIL_PWD: ${MAIL_PWD}
|
||||
NPM_USERNAME: ${NPM_USERNAME}
|
||||
NPM_PASSWORD: ${NPM_PASSWORD}
|
||||
commands:
|
||||
- tests/integration.sh
|
||||
when:
|
||||
status: success
|
||||
|
||||
bundle:
|
||||
image: vmware/harbor-e2e-engine:1.37
|
||||
image: vmware/harbor-e2e-engine:1.38
|
||||
pull: true
|
||||
privileged: true
|
||||
environment:
|
||||
|
@ -46,12 +48,17 @@ pipeline:
|
|||
commands:
|
||||
- du -ks harbor-offline-installer-*.tgz | awk '{print $1 / 1024}' | { read x; echo $x MB; }
|
||||
- mkdir -p bundle
|
||||
- mkdir -p pks-bundle
|
||||
- echo $(git describe --tags) > pks-bundle/version
|
||||
- cp harbor-offline-installer-*.tgz bundle
|
||||
- if [ ${DRONE_BRANCH} = "master" ]; then cp harbor-offline-installer-*.tgz pks-bundle/harbor-offline-installer-latest-master.tgz; fi
|
||||
- if ( echo ${DRONE_BRANCH} | grep "pks*" ); then cp harbor-offline-installer-*.tgz pks-bundle/harbor-offline-installer-latest-pks.tgz; fi
|
||||
- ls -la bundle
|
||||
- ls -la pks-bundle
|
||||
when:
|
||||
repo: vmware/harbor
|
||||
event: [ push, tag ]
|
||||
branch: [ master, release-*, refs/tags/* ]
|
||||
branch: [ master, release-*, pks-*, refs/tags/* ]
|
||||
status: success
|
||||
|
||||
notify-slack:
|
||||
|
@ -93,6 +100,20 @@ pipeline:
|
|||
branch: [ release-*, refs/tags/* ]
|
||||
status: success
|
||||
|
||||
publish-gcs-pks-builds:
|
||||
image: maplain/drone-gcs:latest
|
||||
pull: true
|
||||
source: pks-bundle
|
||||
target: harbor-ci-pipeline-store/latest
|
||||
acl:
|
||||
- allUsers:READER
|
||||
cache_control: public,max-age=3600
|
||||
when:
|
||||
repo: vmware/harbor
|
||||
event: [ push, tag ]
|
||||
branch: [ master, pks-*, refs/tags/* ]
|
||||
status: success
|
||||
|
||||
trigger:
|
||||
image: plugins/downstream
|
||||
server: https://ci.vcna.io
|
||||
|
|
|
@ -1 +1 @@
|
|||
eyJhbGciOiJIUzI1NiJ9.IyBIYXJib3IgZHJvbmUuCi0tLQp3b3Jrc3BhY2U6CiAgYmFzZTogL2Ryb25lCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vdm13YXJlL2hhcmJvcgoKcGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogZmFsc2UKCiAgaW50ZWdyYXRpb24tdGVzdC1vbi1wcjoKICAgIGltYWdlOiB2bXdhcmUvaGFyYm9yLWUyZS1lbmdpbmU6MS4zNwogICAgcHVsbDogdHJ1ZQogICAgcHJpdmlsZWdlZDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIEJJTjogYmluCiAgICAgIEdPUEFUSDogL2Ryb25lCiAgICAgIFNIRUxMOiAvYmluL2Jhc2gKICAgICAgTE9HX1RFTVBfRElSOiBpbnN0YWxsLWxvZ3MKICAgICAgR0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWTogICR7R0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWX0KICAgICAgRFJPTkVfU0VSVkVSOiAgJHtEUk9ORV9TRVJWRVJ9CiAgICAgIERST05FX1RPS0VOOiAgJHtEUk9ORV9UT0tFTl9JTlRFfQogICAgICBIQVJCT1JfQURNSU46ICR7SEFSQk9SX0FETUlOfQogICAgICBIQVJCT1JfUEFTU1dPUkQ6ICR7SEFSQk9SX1BBU1NXT1JEfQogICAgICBHU19QUk9KRUNUX0lEOiAke0dTX1BST0pFQ1RfSUR9CiAgICAgIEdTX0NMSUVOVF9FTUFJTDogJHtHU19DTElFTlRfRU1BSUx9CiAgICAgIEdTX1BSSVZBVEVfS0VZOiAke0dTX1BSSVZBVEVfS0VZfQogICAgICBET01BSU46ICR7Q0lfRE9NQUlOfQogICAgICBNQUlMX1BXRDogJHtNQUlMX1BXRH0KICAgIGNvbW1hbmRzOgogICAgICAtIHRlc3RzL2ludGVncmF0aW9uLnNoCiAgICB3aGVuOgogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgYnVuZGxlOgogICAgaW1hZ2U6IHZtd2FyZS9oYXJib3ItZTJlLWVuZ2luZToxLjM3CiAgICBwdWxsOiB0cnVlCiAgICBwcml2aWxlZ2VkOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQklOOiBiaW4KICAgICAgR09QQVRIOiAvZHJvbmUKICAgICAgU0hFTEw6IC9iaW4vYmFzaAogICAgICBCVUlMRF9OVU1CRVI6ICR7RFJPTkVfQlVJTERfTlVNQkVSfQogICAgY29tbWFuZHM6CiAgICAgIC0gZHUgLWtzIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiB8IGF3ayAne3ByaW50ICQxIC8gMTAyNH0nIHwgeyByZWFkIHg7IGVjaG8gJHggTUI7IH0KICAgICAgLSBta2RpciAtcCBidW5kbGUKICAgICAgLSBjcCBoYXJib3Itb2ZmbGluZS1pbnN0YWxsZXItKi50Z3ogYnVuZGxlCiAgICAgIC0gbHMgLWxhIGJ1bmRsZQogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLSosIHJlZnMvdGFncy8qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCgogIG5vdGlmeS1zbGFjazoKICAgIGltYWdlOiBwbHVnaW5zL3NsYWNrCiAgICB3ZWJob29rOiAke1NMQUNLX1VSTH0KICAgIHVzZXJuYW1lOiBkcm9uZQogICAgdGVtcGxhdGU6ID4KICAgICAgYnVpbGQgaHR0cHM6Ly9jaS52Y25hLmlvL3Ztd2FyZS9oYXJib3Ive3sgYnVpbGQubnVtYmVyIH19IGZpbmlzaGVkIHdpdGggYSB7eyBidWlsZC5zdGF0dXMgfX0gc3RhdHVzLiBQbGVhc2UgZmluZCBsb2dzIGF0IGh0dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9oYXJib3ItY2ktbG9ncy9pbnRlZ3JhdGlvbl9sb2dzX3t7IGJ1aWxkLm51bWJlciB9fV97eyBidWlsZC5jb21taXQgfX0udGFyLmd6CiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIsIHJlbGVhc2UtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IFsgZmFpbHVyZSwgc3VjY2VzcyBdCgogIHB1Ymxpc2gtZ2NzLWJ1aWxkczoKICAgIGltYWdlOiBtYXBsYWluL2Ryb25lLWdjczpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIHNvdXJjZTogYnVuZGxlCiAgICB0YXJnZXQ6IGhhcmJvci1idWlsZHMKICAgIGFjbDoKICAgICAgLSBhbGxVc2VyczpSRUFERVIKICAgIGNhY2hlX2NvbnRyb2w6IHB1YmxpYyxtYXgtYWdlPTM2MDAKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCgogIHB1Ymxpc2gtZ2NzLXJlbGVhc2VzOgogICAgaW1hZ2U6IG1hcGxhaW4vZHJvbmUtZ2NzOmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgc291cmNlOiBidW5kbGUKICAgIHRhcmdldDogaGFyYm9yLXJlbGVhc2VzCiAgICBhY2w6CiAgICAgIC0gYWxsVXNlcnM6UkVBREVSCiAgICBjYWNoZV9jb250cm9sOiBwdWJsaWMsbWF4LWFnZT0zNjAwCiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLSosIHJlZnMvdGFncy8qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCgogIHRyaWdnZXI6CiAgICBpbWFnZTogcGx1Z2lucy9kb3duc3RyZWFtCiAgICBzZXJ2ZXI6IGh0dHBzOi8vY2kudmNuYS5pbwogICAgdG9rZW46ICR7RE9XTlNUUkVBTV9UT0tFTn0KICAgIGZvcms6IHRydWUKICAgIHJlcG9zaXRvcmllczoKICAgICAgIC0gdm13YXJlL3ZpYy1wcm9kdWN0CiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIsIHJlbGVhc2UtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MK.pc3_vwqOx7Nf2Yz9LnFrIzik0K6o0f5Dpv2Aludl3B8
|
||||
eyJhbGciOiJIUzI1NiJ9.IyBIYXJib3IgZHJvbmUuCi0tLQp3b3Jrc3BhY2U6CiAgYmFzZTogL2Ryb25lCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vdm13YXJlL2hhcmJvcgoKcGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogZmFsc2UKCiAgaW50ZWdyYXRpb24tdGVzdC1vbi1wcjoKICAgIGltYWdlOiB2bXdhcmUvaGFyYm9yLWUyZS1lbmdpbmU6MS4zOAogICAgcHVsbDogdHJ1ZQogICAgcHJpdmlsZWdlZDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIEJJTjogYmluCiAgICAgIEdPUEFUSDogL2Ryb25lCiAgICAgIFNIRUxMOiAvYmluL2Jhc2gKICAgICAgTE9HX1RFTVBfRElSOiBpbnN0YWxsLWxvZ3MKICAgICAgR0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWTogICR7R0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWX0KICAgICAgRFJPTkVfU0VSVkVSOiAgJHtEUk9ORV9TRVJWRVJ9CiAgICAgIERST05FX1RPS0VOOiAgJHtEUk9ORV9UT0tFTl9JTlRFfQogICAgICBIQVJCT1JfQURNSU46ICR7SEFSQk9SX0FETUlOfQogICAgICBIQVJCT1JfUEFTU1dPUkQ6ICR7SEFSQk9SX1BBU1NXT1JEfQogICAgICBHU19QUk9KRUNUX0lEOiAke0dTX1BST0pFQ1RfSUR9CiAgICAgIEdTX0NMSUVOVF9FTUFJTDogJHtHU19DTElFTlRfRU1BSUx9CiAgICAgIEdTX1BSSVZBVEVfS0VZOiAke0dTX1BSSVZBVEVfS0VZfQogICAgICBET01BSU46ICR7Q0lfRE9NQUlOfQogICAgICBNQUlMX1BXRDogJHtNQUlMX1BXRH0KICAgICAgTlBNX1VTRVJOQU1FOiAke05QTV9VU0VSTkFNRX0KICAgICAgTlBNX1BBU1NXT1JEOiAke05QTV9QQVNTV09SRH0KICAgIGNvbW1hbmRzOgogICAgICAtIHRlc3RzL2ludGVncmF0aW9uLnNoCiAgICB3aGVuOgogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgYnVuZGxlOgogICAgaW1hZ2U6IHZtd2FyZS9oYXJib3ItZTJlLWVuZ2luZToxLjM4CiAgICBwdWxsOiB0cnVlCiAgICBwcml2aWxlZ2VkOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQklOOiBiaW4KICAgICAgR09QQVRIOiAvZHJvbmUKICAgICAgU0hFTEw6IC9iaW4vYmFzaAogICAgICBCVUlMRF9OVU1CRVI6ICR7RFJPTkVfQlVJTERfTlVNQkVSfQogICAgY29tbWFuZHM6CiAgICAgIC0gZHUgLWtzIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiB8IGF3ayAne3ByaW50ICQxIC8gMTAyNH0nIHwgeyByZWFkIHg7IGVjaG8gJHggTUI7IH0KICAgICAgLSBta2RpciAtcCBidW5kbGUKICAgICAgLSBta2RpciAtcCBwa3MtYnVuZGxlCiAgICAgIC0gZWNobyAkKGdpdCBkZXNjcmliZSAtLXRhZ3MpID4gcGtzLWJ1bmRsZS92ZXJzaW9uIAogICAgICAtIGNwIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiBidW5kbGUKICAgICAgLSBpZiBbICR7RFJPTkVfQlJBTkNIfSA9ICJtYXN0ZXIiIF07IHRoZW4gY3AgaGFyYm9yLW9mZmxpbmUtaW5zdGFsbGVyLSoudGd6IHBrcy1idW5kbGUvaGFyYm9yLW9mZmxpbmUtaW5zdGFsbGVyLWxhdGVzdC1tYXN0ZXIudGd6OyBmaQogICAgICAtIGlmICggZWNobyAke0RST05FX0JSQU5DSH0gfCBncmVwICJwa3MqIiApOyB0aGVuIGNwIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiBwa3MtYnVuZGxlL2hhcmJvci1vZmZsaW5lLWluc3RhbGxlci1sYXRlc3QtcGtzLnRnejsgZmkKICAgICAgLSBscyAtbGEgYnVuZGxlCiAgICAgIC0gbHMgLWxhIHBrcy1idW5kbGUKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCBwa3MtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgbm90aWZ5LXNsYWNrOgogICAgaW1hZ2U6IHBsdWdpbnMvc2xhY2sKICAgIHdlYmhvb2s6ICR7U0xBQ0tfVVJMfQogICAgdXNlcm5hbWU6IGRyb25lCiAgICB0ZW1wbGF0ZTogPgogICAgICBidWlsZCBodHRwczovL2NpLnZjbmEuaW8vdm13YXJlL2hhcmJvci97eyBidWlsZC5udW1iZXIgfX0gZmluaXNoZWQgd2l0aCBhIHt7IGJ1aWxkLnN0YXR1cyB9fSBzdGF0dXMuIFBsZWFzZSBmaW5kIGxvZ3MgYXQgaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL2hhcmJvci1jaS1sb2dzL2ludGVncmF0aW9uX2xvZ3Nfe3sgYnVpbGQubnVtYmVyIH19X3t7IGJ1aWxkLmNvbW1pdCB9fS50YXIuZ3oKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCByZWZzL3RhZ3MvKiBdCiAgICAgIHN0YXR1czogWyBmYWlsdXJlLCBzdWNjZXNzIF0KCiAgcHVibGlzaC1nY3MtYnVpbGRzOgogICAgaW1hZ2U6IG1hcGxhaW4vZHJvbmUtZ2NzOmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgc291cmNlOiBidW5kbGUKICAgIHRhcmdldDogaGFyYm9yLWJ1aWxkcwogICAgYWNsOgogICAgICAtIGFsbFVzZXJzOlJFQURFUgogICAgY2FjaGVfY29udHJvbDogcHVibGljLG1heC1hZ2U9MzYwMAogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLSogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgcHVibGlzaC1nY3MtcmVsZWFzZXM6CiAgICBpbWFnZTogbWFwbGFpbi9kcm9uZS1nY3M6bGF0ZXN0CiAgICBwdWxsOiB0cnVlCiAgICBzb3VyY2U6IGJ1bmRsZQogICAgdGFyZ2V0OiBoYXJib3ItcmVsZWFzZXMKICAgIGFjbDoKICAgICAgLSBhbGxVc2VyczpSRUFERVIKICAgIGNhY2hlX2NvbnRyb2w6IHB1YmxpYyxtYXgtYWdlPTM2MDAKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlbGVhc2UtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgcHVibGlzaC1nY3MtcGtzLWJ1aWxkczoKICAgIGltYWdlOiBtYXBsYWluL2Ryb25lLWdjczpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIHNvdXJjZTogcGtzLWJ1bmRsZQogICAgdGFyZ2V0OiBoYXJib3ItY2ktcGlwZWxpbmUtc3RvcmUvbGF0ZXN0CiAgICBhY2w6CiAgICAgIC0gYWxsVXNlcnM6UkVBREVSCiAgICBjYWNoZV9jb250cm9sOiBwdWJsaWMsbWF4LWFnZT0zNjAwCiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIsIHBrcy0qLCByZWZzL3RhZ3MvKiBdCiAgICAgIHN0YXR1czogc3VjY2VzcwoKICB0cmlnZ2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG93bnN0cmVhbQogICAgc2VydmVyOiBodHRwczovL2NpLnZjbmEuaW8KICAgIHRva2VuOiAke0RPV05TVFJFQU1fVE9LRU59CiAgICBmb3JrOiB0cnVlCiAgICByZXBvc2l0b3JpZXM6CiAgICAgICAtIHZtd2FyZS92aWMtcHJvZHVjdAogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLSosIHJlZnMvdGFncy8qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCg.TRzg0jvokGI8PBccqkW4foVBX_1uGzFUhTRaPzMFaeY
|
|
@ -19,7 +19,7 @@ env:
|
|||
MYSQL_PWD: root123
|
||||
MYSQL_DATABASE: registry
|
||||
SQLITE_FILE: /tmp/registry.db
|
||||
ADMIN_SERVER_URL: http://127.0.0.1:8888
|
||||
ADMINSERVER_URL: http://127.0.0.1:8888
|
||||
DOCKER_COMPOSE_VERSION: 1.7.1
|
||||
HARBOR_ADMIN: admin
|
||||
HARBOR_ADMIN_PASSWD: Harbor12345
|
||||
|
@ -98,6 +98,7 @@ script:
|
|||
- goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
- docker-compose -f make/docker-compose.test.yml down
|
||||
- sudo rm -rf /data/config/*
|
||||
- sudo rm -rf /data/database/*
|
||||
- ls /data/cert
|
||||
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 NOTARYFLAG=true CLAIRFLAG=true
|
||||
- sleep 10
|
||||
|
|
25
Makefile
|
@ -84,9 +84,9 @@ NOTARYFLAG=false
|
|||
REGISTRYVERSION=2.6.2-photon
|
||||
NGINXVERSION=1.11.13
|
||||
PHOTONVERSION=1.0
|
||||
NOTARYVERSION=server-0.5.0
|
||||
NOTARYSIGNERVERSION=signer-0.5.0
|
||||
MARIADBVERSION=10.2.8
|
||||
NOTARYVERSION=server-0.5.1
|
||||
NOTARYSIGNERVERSION=signer-0.5.1
|
||||
MARIADBVERSION=10.2.10
|
||||
HTTPPROXY=
|
||||
REBUILDCLARITYFLAG=false
|
||||
NEWCLARITYVERSION=
|
||||
|
@ -214,7 +214,7 @@ REGISTRYUSER=user
|
|||
REGISTRYPASSWORD=default
|
||||
|
||||
# migrator
|
||||
MIGRATORVERSION=1.2
|
||||
MIGRATORVERSION=1.3
|
||||
MIGRATORFLAG=false
|
||||
|
||||
# cmds
|
||||
|
@ -224,19 +224,20 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
|
|||
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
|
||||
vmware/nginx-photon:$(NGINXVERSION) vmware/registry:$(REGISTRYVERSION) \
|
||||
photon:$(PHOTONVERSION)
|
||||
vmware/photon:$(PHOTONVERSION)
|
||||
PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(GITTAGVERSION).tgz \
|
||||
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz \
|
||||
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
|
||||
$(HARBORPKG)/upgrade $(HARBORPKG)/harbor_1_1_0_template \
|
||||
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME)
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
|
||||
$(HARBORPKG)/ha
|
||||
PACKAGE_ONLINE_PARA=-zcvf harbor-online-installer-$(GITTAGVERSION).tgz \
|
||||
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
|
||||
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
|
||||
$(HARBORPKG)/upgrade $(HARBORPKG)/harbor_1_1_0_template \
|
||||
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
|
||||
$(HARBORPKG)/harbor.cfg
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/ha
|
||||
DOCKERCOMPOSE_LIST=-f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
|
||||
|
||||
ifeq ($(NOTARYFLAG), true)
|
||||
|
@ -313,7 +314,7 @@ prepare:
|
|||
|
||||
build_common: version
|
||||
@echo "buildging db container for photon..."
|
||||
@cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
|
||||
@cd $(DOCKERFILEPATH_DB) && $(DOCKERBUILD) --pull -f $(DOCKERFILENAME_DB) -t $(DOCKERIMAGENAME_DB):$(VERSIONTAG) .
|
||||
@echo "Done."
|
||||
|
||||
build_photon: build_common
|
||||
|
@ -327,7 +328,9 @@ build: build_$(BASEIMAGE)
|
|||
modify_composefile:
|
||||
@echo "preparing docker-compose file..."
|
||||
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSETPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
|
||||
@cp $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSETPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSEFILENAME)
|
||||
@$(SEDCMD) -i 's/__version__/$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME)
|
||||
@$(SEDCMD) -i 's/__version__/$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSEFILENAME)
|
||||
|
||||
modify_sourcefiles:
|
||||
@echo "change mode of source files."
|
||||
|
@ -345,6 +348,8 @@ package_online: modify_composefile
|
|||
@if [ -n "$(REGISTRYSERVER)" ] ; then \
|
||||
$(SEDCMD) -i 's/image\: vmware/image\: $(REGISTRYSERVER)\/$(REGISTRYPROJECTNAME)/' \
|
||||
$(HARBORPKG)/docker-compose.yml ; \
|
||||
$(SEDCMD) -i 's/image\: vmware/image\: $(REGISTRYSERVER)\/$(REGISTRYPROJECTNAME)/' \
|
||||
$(HARBORPKG)/ha/docker-compose.yml ; \
|
||||
fi
|
||||
@cp LICENSE $(HARBORPKG)/LICENSE
|
||||
@cp NOTICE $(HARBORPKG)/NOTICE
|
||||
|
@ -363,6 +368,7 @@ package_offline: compile build modify_sourcefiles modify_composefile
|
|||
@cp NOTICE $(HARBORPKG)/NOTICE
|
||||
@cp tools/migration/migration_cfg/upgrade $(HARBORPKG)/upgrade
|
||||
@cp tools/migration/migration_cfg/harbor_1_1_0_template $(HARBORPKG)/harbor_1_1_0_template
|
||||
@cp $(HARBORPKG)/common/db/registry.sql $(HARBORPKG)/ha/
|
||||
|
||||
@echo "pulling nginx and registry..."
|
||||
@$(DOCKERPULL) vmware/registry:$(REGISTRYVERSION)
|
||||
|
@ -384,7 +390,8 @@ package_offline: compile build modify_sourcefiles modify_composefile
|
|||
fi
|
||||
|
||||
@echo "saving harbor docker image"
|
||||
@$(DOCKERSAVE) $(DOCKERSAVE_PARA) | gzip > $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz
|
||||
@$(DOCKERSAVE) $(DOCKERSAVE_PARA) > $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar
|
||||
@gzip $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar
|
||||
|
||||
@$(TARCMD) $(PACKAGE_OFFLINE_PARA)
|
||||
@rm -rf $(HARBORPKG)
|
||||
|
|
|
@ -33,8 +33,11 @@ Download binaries of **[Harbor release ](https://github.com/vmware/harbor/releas
|
|||
Refer to **[User Guide](docs/user_guide.md)** for more details on how to use Harbor.
|
||||
|
||||
### Community
|
||||
**Twitter:** @project_harbor
|
||||
**User Group:** Join Harbor user email group: harbor-users@googlegroups.com to get update of Harbor's news, features, releases, or to provide suggestion and feedback. To subscribe, send an email to harbor-users+subscribe@googlegroups.com .
|
||||
**Developer Group:** Join Harbor developer group: harbor-dev@googlegroups.com for discussion on Harbor development and contribution.To subscribe, send an email to harbor-dev+subscribe@googlegroups.com .
|
||||
**Slack:** Join Harbor's community for discussion and ask questions: [VMware {code}](https://code.vmware.com/join/), Channel: #harbor.
|
||||
**Email:** harbor@vmware.com .
|
||||
|
||||
More info on [partners and users](partners.md).
|
||||
|
||||
### Contribution
|
||||
|
@ -56,7 +59,7 @@ This project uses open source components which have additional licensing terms.
|
|||
* MySQL 5.6: [docker image](https://hub.docker.com/_/mysql/), [license](https://github.com/docker-library/mysql/blob/master/LICENSE)
|
||||
|
||||
### Commercial Support
|
||||
If you need commercial support of Harbor, please contact us for more information: harbor@vmware.com .
|
||||
If you need commercial support of Harbor, please contact us for more information: harbor at vmware.com .
|
||||
|
||||
|
||||
|
||||
|
|
10
contrib/harbor-cli/Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
FROM python:2
|
||||
MAINTAINER int32bit krystism@gmail.com
|
||||
|
||||
ADD . /opt/harborclient
|
||||
RUN pip install -r /opt/harborclient/requirements.txt
|
||||
RUN set -ex \
|
||||
&& cd /opt/harborclient \
|
||||
&& python setup.py install \
|
||||
&& rm -rf /opt/harborclient
|
||||
CMD ["harbor"]
|
574
contrib/harbor-cli/README.md
Normal file
|
@ -0,0 +1,574 @@
|
|||
## About This Project
|
||||
|
||||
Project Harbor is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security. Having a registry closer to the build and run environment improves the image transfer efficiency. Harbor supports the setup of multiple registries and has images replicated between them. With Harbor, the images are stored within the private registry, keeping the bits and intellectual properties behind the company firewall. In addition, Harbor offers advanced security features, such as user management, access control and activity auditing.
|
||||
|
||||
This project provides a great native command-line experience for managing Harbor resources like user, project, etc. It can be used on macOS, Linux, and Docker.
|
||||
|
||||
## Install Harbor CLI
|
||||
|
||||
Harbor CLI can be installed by one of two approaches:
|
||||
|
||||
* Option 1: Build as a Docker image(easy, recommended)
|
||||
* Option 2: Native Installation from Source
|
||||
* Option 3: Install from pypi
|
||||
|
||||
### Option 1: Build as a Docker image(easy, recommended)
|
||||
|
||||
We maintain a Docker prebuilt image with Harbor CLI. Install the CLI using `docker run`.
|
||||
|
||||
```sh
|
||||
docker run -t -i krystism/harborclient harbor help
|
||||
```
|
||||
|
||||
We strongly suggest you build image from code manually, because our prebuilt image may be not latest version.
|
||||
|
||||
```sh
|
||||
docker build -t yourname/harborclient .
|
||||
```
|
||||
|
||||
Run Harbor CLI as follows:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-e HARBOR_USERNAME="admin" \
|
||||
-e HARBOR_PASSWORD="Harbor12345" \
|
||||
-e HARBOR_PROJECT=1 \
|
||||
-e HARBOR_URL="http://localhost" \
|
||||
yourname/harborclient harbor info
|
||||
|
||||
+------------------------------+---------------------+
|
||||
| Property | Value |
|
||||
+------------------------------+---------------------+
|
||||
| admiral_endpoint | NA |
|
||||
| auth_mode | db_auth |
|
||||
| disk_free | 4993355776 |
|
||||
| disk_total | 18381979648 |
|
||||
| harbor_version | v1.2.2 |
|
||||
| has_ca_root | False |
|
||||
| next_scan_all | 0 |
|
||||
| project_creation_restriction | everyone |
|
||||
| registry_url | localhost |
|
||||
| self_registration | True |
|
||||
| with_admiral | False |
|
||||
| with_clair | False |
|
||||
| with_notary | False |
|
||||
+------------------------------+---------------------+
|
||||
```
|
||||
|
||||
Create an alias:
|
||||
|
||||
```bash
|
||||
alias harbor='docker run \
|
||||
-e HARBOR_USERNAME="admin" \
|
||||
-e HARBOR_PASSWORD="Harbor12345" \
|
||||
-e HARBOR_URL="http://localhost" \
|
||||
--rm krystism/harborclient harbor'
|
||||
```
|
||||
|
||||
Then you can run Harbor CLI like:
|
||||
|
||||
```
|
||||
$ harbor user-list
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
| user_id | username | is_admin | email | realname | comment |
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
| 1 | admin | 1 | admin@example.com | system admin | admin user |
|
||||
| 2 | int32bit | 0 | int32bit@example.com | int32bit | int32bit |
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
```
|
||||
|
||||
### Option 2: Native Installation from Source
|
||||
|
||||
The installation steps boil down to the following:
|
||||
|
||||
#### Install requirements
|
||||
|
||||
```
|
||||
sudo pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Install Harbor CLI.
|
||||
|
||||
```sh
|
||||
sudo python setup.py install
|
||||
```
|
||||
|
||||
Or
|
||||
|
||||
```sh
|
||||
sudo pip install .
|
||||
```
|
||||
|
||||
### Option 3: Install from pypi
|
||||
|
||||
```
|
||||
sudo pip install python-harborclient
|
||||
```
|
||||
|
||||
### Verify operation
|
||||
|
||||
As the `admin` user, do a `info` request:
|
||||
|
||||
```
|
||||
$ harbor --os-baseurl https://localhost --os-username admin --os-project 1 info
|
||||
password: *****
|
||||
+------------------------------+---------------------+
|
||||
| Property | Value |
|
||||
+------------------------------+---------------------+
|
||||
| admiral_endpoint | NA |
|
||||
| auth_mode | db_auth |
|
||||
| disk_free | 4992696320 |
|
||||
| disk_total | 18381979648 |
|
||||
| harbor_version | v1.2.2 |
|
||||
| has_ca_root | False |
|
||||
| next_scan_all | 0 |
|
||||
| project_creation_restriction | everyone |
|
||||
| registry_url | localhost |
|
||||
| self_registration | True |
|
||||
| with_admiral | False |
|
||||
| with_clair | False |
|
||||
| with_notary | False |
|
||||
+------------------------------+---------------------+
|
||||
```
|
||||
|
||||
### Create harbor client environment scripts
|
||||
|
||||
To increase efficiency of client operations, Harbor CLI supports simple client environment scrips also known as `harborrc` file.
|
||||
These scripts typically contain common options for all client, but also support unique options.
|
||||
|
||||
Create client environment scripts for `admin` user:
|
||||
|
||||
```bash
|
||||
cat >admin-harborrc <<EOF
|
||||
export HARBOR_USERNAME=admin
|
||||
export HARBOR_PASSWORD=Harbor12345
|
||||
export HARBOR_URL=http://localhost
|
||||
export HARBOR_PROJECT=1
|
||||
EOF
|
||||
```
|
||||
|
||||
Replace `HARBOR_PASSWORD` with your password.
|
||||
|
||||
To run clients as a specific project and user, you can simply load the associated client environment script prior to running them.
|
||||
|
||||
```bash
|
||||
source admin-harborrc
|
||||
```
|
||||
|
||||
List images:
|
||||
|
||||
```bash
|
||||
$ harbor list
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
| name | project_id | size | tags_count | star_count | pull_count | update_time |
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
| int32bit/busybox | 2 | 715181 | 1 | 0 | 0 | 2017-11-01T07:06:36Z |
|
||||
| int32bit/golang:1.7.3 | 2 | 257883053 | 2 | 0 | 0 | 2017-11-01T12:59:05Z |
|
||||
| int32bit/hello-world | 2 | 974 | 1 | 0 | 0 | 2017-11-01T13:22:46Z |
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
```
|
||||
|
||||
### Setup bash completion
|
||||
|
||||
```bash
|
||||
$ complete -W $(harbor bash-completion) harbor
|
||||
$ harbor us<tab><tab>
|
||||
usage user-create user-delete user-list user-show user-update
|
||||
```
|
||||
|
||||
## User Guide
|
||||
|
||||
This guide walks you through the fundamentals of using Harbor CLI. You'll learn how to use Harbor CLI to:
|
||||
|
||||
* Manage your projects.
|
||||
* Manage members of a project.
|
||||
* Search projects and repositories.
|
||||
* Manage users.
|
||||
* Manage replication policies.
|
||||
* Manage configuration.
|
||||
* Delete repositories and images.
|
||||
* Show logs.
|
||||
* Get statistics data.
|
||||
* ...
|
||||
|
||||
Once you install Harbor CLI, you can run `harbor help` to get usage:
|
||||
|
||||
```bash
|
||||
$ harbor help
|
||||
usage: harbor [--debug] [--timings] [--version] [--os-username <username>]
|
||||
[--os-password <password>] [--os-project <project>]
|
||||
[--timeout <timeout>] [--os-baseurl <baseurl>] [--insecure]
|
||||
[--os-cacert <ca-certificate>] [--os-api-version <api-version>]
|
||||
<subcommand> ...
|
||||
```
|
||||
|
||||
Run "harbor help COMMAND" for help on a specific command.
|
||||
|
||||
```bash
|
||||
$ harbor help user-create
|
||||
usage: harbor user-create --username <username> --password <password> --email
|
||||
<email> [--realname <realname>]
|
||||
[--comment <comment>]
|
||||
|
||||
Create a new User.
|
||||
|
||||
Optional arguments:
|
||||
--username <username> Unique name of the new user.
|
||||
--password <password> Password of the new user.
|
||||
--email <email> Email of the new user.
|
||||
--realname <realname> Realname of the new user.
|
||||
--comment <comment> Comment of the new user.
|
||||
```
|
||||
|
||||
Show details about API requests using `--debug` option:
|
||||
|
||||
```bash
|
||||
$ harbor --debug --insecure project-list
|
||||
DEBUG (connectionpool:824) Starting new HTTPS connection (1): devstack
|
||||
DEBUG (connectionpool:396) https://devstack:443 "POST /login HTTP/1.1" 200 0
|
||||
DEBUG (client:274) Successfully login, session id: 2642a18db2cb0fb207bd721899da9f8b
|
||||
REQ: curl -g -i --insecure 'https://devstack/api/projects' -X GET -H "Accept: application/json" -H "Harbor-API-Version: v2" -H "User-Agent: python-harborclient" -b "beegosessionID: 2642a18db2cb0fb207bd721899da9f8b"
|
||||
DEBUG (connectionpool:824) Starting new HTTPS connection (1): devstack
|
||||
DEBUG (connectionpool:396) https://devstack:443 "GET /api/projects HTTP/1.1" 200 316
|
||||
RESP: [200] {'Content-Length': '316', 'Content-Encoding': 'gzip', 'X-Total-Count': '2', 'Server': 'nginx/1.11.13', 'Connection': 'keep-alive', 'Date': 'Mon, 06 Nov 2017 12:24:53 GMT', 'Content-Type': 'application/json; charset=utf-8'}
|
||||
RESP BODY: [{"creation_time_str": "", "enable_content_trust": false, "Togglable": true, "owner_name": "", "name": "int32bit", "deleted": 0, "repo_count": 3, "creation_time": "2017-11-01T06:56:07Z", "update_time": "2017-11-01T06:56:07Z", "prevent_vulnerable_images_from_running": false, "current_user_role_id": 1, "project_id": 2, "automatically_scan_images_on_push": false, "public": 1, "prevent_vulnerable_images_from_running_severity": "", "owner_id": 1}, {"creation_time_str": "", "enable_content_trust": false, "Togglable": true, "owner_name": "", "name": "library", "deleted": 0, "repo_count": 0, "creation_time": "2017-11-01T06:08:43Z", "update_time": "2017-11-01T06:08:43Z", "prevent_vulnerable_images_from_running": false, "current_user_role_id": 1, "project_id": 1, "automatically_scan_images_on_push": false, "public": 1, "prevent_vulnerable_images_from_running_severity": "", "owner_id": 1}]
|
||||
|
||||
+------------+----------+----------+----------------------+------------+----------------------+--------+
|
||||
| project_id | name | owner_id | current_user_role_id | repo_count | creation_time | public |
|
||||
+------------+----------+----------+----------------------+------------+----------------------+--------+
|
||||
| 1 | library | 1 | 1 | 0 | 2017-11-01T06:08:43Z | 1 |
|
||||
| 2 | int32bit | 1 | 1 | 3 | 2017-11-01T06:56:07Z | 1 |
|
||||
+------------+----------+----------+----------------------+------------+----------------------+--------+
|
||||
```
|
||||
|
||||
Print call timing info with `--timings` option:
|
||||
|
||||
```
|
||||
$ harbor --insecure --timings user-list
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
| user_id | username | is_admin | email | realname | comment |
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
| 1 | admin | 1 | admin@example.com | system admin | admin user |
|
||||
| 3 | int32bit | 0 | int32bit@example.com | int32bit | test |
|
||||
+---------+----------+----------+----------------------+--------------+-------------+
|
||||
+--------------+-----------------+
|
||||
| url | seconds |
|
||||
+--------------+-----------------+
|
||||
| GET /users | 0.0146510601044 |
|
||||
| GET /users/1 | 0.0146780014038 |
|
||||
| Total | 0.0293290615082 |
|
||||
+--------------+-----------------+
|
||||
Total: 0.0293290615082 seconds
|
||||
```
|
||||
|
||||
All SSL connections are attempted to be made secure by using the CA certificate bundle installed by default. This makes all connections considered "insecure" fail unless `--insecure` is used.
|
||||
|
||||
```
|
||||
$ harbor info
|
||||
Traceback (most recent call last):
|
||||
File "/usr/local/bin/harbor", line 10, in <module>
|
||||
sys.exit(main())
|
||||
File "/usr/local/lib/python2.7/dist-packages/harborclient/shell.py", line 404, in main
|
||||
HarborShell().main(argv)
|
||||
File "/usr/local/lib/python2.7/dist-packages/harborclient/shell.py", line 330, in main
|
||||
self.cs.authenticate()
|
||||
File "/usr/local/lib/python2.7/dist-packages/harborclient/v2/client.py", line 83, in authenticate
|
||||
self.client.authenticate()
|
||||
File "/usr/local/lib/python2.7/dist-packages/harborclient/client.py", line 270, in authenticate
|
||||
verify=self.verify_cert)
|
||||
File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 112, in post
|
||||
return request('post', url, data=data, json=json, **kwargs)
|
||||
File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 58, in request
|
||||
return session.request(method=method, url=url, **kwargs)
|
||||
File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 508, in request
|
||||
resp = self.send(prep, **send_kwargs)
|
||||
File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 618, in send
|
||||
r = adapter.send(request, **kwargs)
|
||||
File "/usr/local/lib/python2.7/dist-packages/requests/adapters.py", line 506, in send
|
||||
raise SSLError(e, request=request)
|
||||
requests.exceptions.SSLError: HTTPSConnectionPool(host='devstack', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLError("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",),))
|
||||
$ harbor --insecure info
|
||||
+------------------------------+---------------------+
|
||||
| Property | Value |
|
||||
+------------------------------+---------------------+
|
||||
| admiral_endpoint | NA |
|
||||
| auth_mode | db_auth |
|
||||
| disk_free | 4991021056 |
|
||||
| disk_total | 18381979648 |
|
||||
| harbor_version | v1.2.2 |
|
||||
| has_ca_root | False |
|
||||
| next_scan_all | 0 |
|
||||
| project_creation_restriction | everyone |
|
||||
| registry_url | 192.168.99.101:8888 |
|
||||
| self_registration | True |
|
||||
| with_admiral | False |
|
||||
| with_clair | False |
|
||||
| with_notary | False |
|
||||
+------------------------------+---------------------+
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Create a new user
|
||||
|
||||
```
|
||||
$ harbor --insecure user-create \
|
||||
--username new-user \
|
||||
--password 1q2w3e4r \
|
||||
--email new_user@example.com \
|
||||
--realname newuser \
|
||||
--comment "I am a new user"
|
||||
Create user 'new-user' successfully.
|
||||
```
|
||||
|
||||
### Delete a user
|
||||
|
||||
```
|
||||
$ harbor --insecure user-delete new-user
|
||||
Delete user 'new-user' sucessfully.
|
||||
```
|
||||
|
||||
### List repositories and images
|
||||
|
||||
```
|
||||
$ harbor list
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
| name | project_id | size | tags_count | star_count | pull_count | update_time |
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
| int32bit/busybox | 2 | 715181 | 1 | 0 | 0 | 2017-11-01T07:06:36Z |
|
||||
| int32bit/golang:1.7.3 | 2 | 257883053 | 2 | 0 | 0 | 2017-11-01T12:59:05Z |
|
||||
| int32bit/hello-world | 2 | 974 | 1 | 0 | 0 | 2017-11-01T13:22:46Z |
|
||||
+-----------------------+------------+-----------+------------+------------+------------+----------------------+
|
||||
```
|
||||
|
||||
### Show details about image
|
||||
|
||||
```
|
||||
$ harbor show int32bit/golang:1.7.3
|
||||
+--------------------+-------------------------------------------------------------------------+
|
||||
| Property | Value |
|
||||
+--------------------+-------------------------------------------------------------------------+
|
||||
| creation_time | 2017-11-01T12:59:05Z |
|
||||
| description | |
|
||||
| id | 2 |
|
||||
| name | int32bit/golang |
|
||||
| project_id | 2 |
|
||||
| pull_count | 0 |
|
||||
| star_count | 0 |
|
||||
| tag_architecture | amd64 |
|
||||
| tag_author | |
|
||||
| tag_created | 2016-11-08T19:32:39.908048617Z |
|
||||
| tag_digest | sha256:37d263ccd240e113a752c46306ad004e36532ce118eb3131d9f76f43cc606d5d |
|
||||
| tag_docker_version | 1.12.3 |
|
||||
| tag_name | 1.7.3 |
|
||||
| tag_os | linux |
|
||||
| tag_signature | - |
|
||||
| tags_count | 2 |
|
||||
| update_time | 2017-11-01T12:59:05Z |
|
||||
+--------------------+-------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Get top accessed repositories
|
||||
|
||||
```
|
||||
$ harbor top
|
||||
+----------------------+------------+------------+
|
||||
| name | pull_count | star_count |
|
||||
+----------------------+------------+------------+
|
||||
| int32bit/busybox | 10 | 0 |
|
||||
| int32bit/golang | 8 | 0 |
|
||||
| int32bit/hello-world | 1 | 0 |
|
||||
+----------------------+------------+------------+
|
||||
```
|
||||
|
||||
### Lists members of a project.
|
||||
|
||||
```
|
||||
$ harbor member-list
|
||||
+----------+--------------+---------+---------+
|
||||
| username | role_name | user_id | role_id |
|
||||
+----------+--------------+---------+---------+
|
||||
| admin | projectAdmin | 1 | 1 |
|
||||
| foo | developer | 5 | 2 |
|
||||
| test | guest | 6 | 3 |
|
||||
+----------+--------------+---------+---------+
|
||||
```
|
||||
|
||||
### Show logs
|
||||
|
||||
```
|
||||
$ harbor logs
|
||||
+--------+----------------------+----------+------------+-----------+-----------------------------+
|
||||
| log_id | op_time | username | project_id | operation | repository |
|
||||
+--------+----------------------+----------+------------+-----------+-----------------------------+
|
||||
| 1 | 2017-11-01T06:56:07Z | admin | 2 | create | int32bit/ |
|
||||
| 2 | 2017-11-01T07:06:36Z | admin | 2 | push | int32bit/busybox:latest |
|
||||
| 3 | 2017-11-01T12:59:05Z | admin | 2 | push | int32bit/golang:1.7.3 |
|
||||
| 4 | 2017-11-01T13:22:46Z | admin | 2 | push | int32bit/hello-world:latest |
|
||||
| 5 | 2017-11-01T14:21:49Z | admin | 2 | push | int32bit/golang:latest |
|
||||
| 6 | 2017-11-03T20:39:04Z | admin | 3 | create | test/ |
|
||||
| 7 | 2017-11-03T20:39:22Z | admin | 3 | delete | test/ |
|
||||
| 8 | 2017-11-03T20:39:38Z | admin | 4 | create | test/ |
|
||||
| 9 | 2017-11-03T20:49:33Z | admin | 4 | delete | test/ |
|
||||
+--------+----------------------+----------+------------+-----------+-----------------------------+
|
||||
```
|
||||
|
||||
### Search projects and repositories.
|
||||
|
||||
```
|
||||
$ harbor search int32bit
|
||||
Find 1 Projects:
|
||||
+------------+----------+--------+------------+----------------------+
|
||||
| project_id | name | public | repo_count | creation_time |
|
||||
+------------+----------+--------+------------+----------------------+
|
||||
| 2 | int32bit | 1 | 3 | 2017-11-01T06:56:07Z |
|
||||
+------------+----------+--------+------------+----------------------+
|
||||
|
||||
Find 3 Repositories:
|
||||
+----------------------+--------------+------------+----------------+
|
||||
| repository_name | project_name | project_id | project_public |
|
||||
+----------------------+--------------+------------+----------------+
|
||||
| int32bit/busybox | int32bit | 2 | 1 |
|
||||
| int32bit/golang | int32bit | 2 | 1 |
|
||||
| int32bit/hello-world | int32bit | 2 | 1 |
|
||||
+----------------------+--------------+------------+----------------+
|
||||
```
|
||||
|
||||
### Lists targets
|
||||
|
||||
```
|
||||
$ harbor target-list
|
||||
+----+----------------------+-------------------------------------+----------+----------+----------------------+
|
||||
| id | name | endpoint | username | password | creation_time |
|
||||
+----+----------------------+-------------------------------------+----------+----------+----------------------+
|
||||
| 1 | test-target | http://192.168.99.101:8888 | admin | - | 2017-11-02T01:35:30Z |
|
||||
| 2 | test-target-2 | http://192.168.99.101:9999 | admin | - | 2017-11-02T13:43:07Z |
|
||||
| 3 | int32bit-test-target | http://192.168.99.101:8888/int32bit | admin | - | 2017-11-02T14:28:54Z |
|
||||
+----+----------------------+-------------------------------------+----------+----------+----------------------+
|
||||
```
|
||||
|
||||
### Ping a target
|
||||
|
||||
```
|
||||
$ harbor target-ping 1
|
||||
OK
|
||||
```
|
||||
|
||||
### Lists replication job
|
||||
|
||||
```
|
||||
$ harbor job-list 1
|
||||
+----+----------------------+-----------+----------+----------------------+
|
||||
| id | repository | operation | status | update_time |
|
||||
+----+----------------------+-----------+----------+----------------------+
|
||||
| 1 | int32bit/busybox | transfer | finished | 2017-11-02T01:35:31Z |
|
||||
| 2 | int32bit/golang | transfer | finished | 2017-11-02T01:35:31Z |
|
||||
| 3 | int32bit/hello-world | transfer | finished | 2017-11-02T01:35:31Z |
|
||||
+----+----------------------+-----------+----------+----------------------+
|
||||
```
|
||||
|
||||
### Show job logs:
|
||||
|
||||
```
|
||||
$ harbor job-log 1
|
||||
2017-11-02T01:35:30Z [INFO] initializing: repository: int32bit/busybox, tags: [], source URL: http://registry:5000, destination URL: http://192.168.99.101:8888, insecure: false, destination user: admin
|
||||
2017-11-02T01:35:30Z [INFO] initialization completed: project: int32bit, repository: int32bit/busybox, tags: [latest], source URL: http://registry:5000, destination URL: http://192.168.99.101:8888, insecure: false, destination user: admin
|
||||
2017-11-02T01:35:30Z [WARNING] the status code is 409 when creating project int32bit on http://192.168.99.101:8888 with user admin, try to do next step
|
||||
2017-11-02T01:35:30Z [INFO] manifest of int32bit/busybox:latest pulled successfully from http://registry:5000: sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af
|
||||
2017-11-02T01:35:30Z [INFO] all blobs of int32bit/busybox:latest from http://registry:5000: [sha256:54511612f1c4d97e93430fc3d5dc2f05dfbe8fb7e6259b7351deeca95eaf2971 sha256:03b1be98f3f9b05cb57782a3a71a44aaf6ec695de5f4f8e6c1058cd42f04953e]
|
||||
2017-11-02T01:35:31Z [INFO] blob sha256:54511612f1c4d97e93430fc3d5dc2f05dfbe8fb7e6259b7351deeca95eaf2971 of int32bit/busybox:latest already exists in http://192.168.99.101:8888
|
||||
2017-11-02T01:35:31Z [INFO] blob sha256:03b1be98f3f9b05cb57782a3a71a44aaf6ec695de5f4f8e6c1058cd42f04953e of int32bit/busybox:latest already exists in http://192.168.99.101:8888
|
||||
2017-11-02T01:35:31Z [INFO] blobs of int32bit/busybox:latest need to be transferred to http://192.168.99.101:8888: []
|
||||
2017-11-02T01:35:31Z [INFO] manifest of int32bit/busybox:latest exists on source registry http://registry:5000, continue manifest pushing
|
||||
2017-11-02T01:35:31Z [INFO] manifest of int32bit/busybox:latest exists on destination registry http://192.168.99.101:8888, skip manifest pushing
|
||||
2017-11-02T01:35:31Z [INFO] no tag needs to be replicated, next state is "finished"
|
||||
```
|
||||
|
||||
### Show usage
|
||||
|
||||
```
|
||||
$ harbor usage
|
||||
+-----------------------+-------+
|
||||
| Property | Value |
|
||||
+-----------------------+-------+
|
||||
| private_project_count | 0 |
|
||||
| private_repo_count | 0 |
|
||||
| public_project_count | 2 |
|
||||
| public_repo_count | 3 |
|
||||
| total_project_count | 2 |
|
||||
| total_repo_count | 3 |
|
||||
+-----------------------+-------+
|
||||
```
|
||||
|
||||
### Show Harbor info
|
||||
|
||||
```
|
||||
$ harbor info
|
||||
+------------------------------+---------------------+
|
||||
| Property | Value |
|
||||
+------------------------------+---------------------+
|
||||
| admiral_endpoint | NA |
|
||||
| auth_mode | db_auth |
|
||||
| disk_free | 4989370368 |
|
||||
| disk_total | 18381979648 |
|
||||
| harbor_version | v1.2.2 |
|
||||
| has_ca_root | False |
|
||||
| next_scan_all | 0 |
|
||||
| project_creation_restriction | everyone |
|
||||
| registry_url | 192.168.99.101:8888 |
|
||||
| self_registration | True |
|
||||
| with_admiral | False |
|
||||
| with_clair | False |
|
||||
| with_notary | False |
|
||||
+------------------------------+---------------------+
|
||||
```
|
||||
|
||||
### Get configrations
|
||||
|
||||
```
|
||||
$ harbor get-conf
|
||||
+------------------------------+-------------------------------------------------------+----------+
|
||||
| name | value | editable |
|
||||
+------------------------------+-------------------------------------------------------+----------+
|
||||
| auth_mode | db_auth | False |
|
||||
| email_from | admin <sample_admin@mydomain.com> | True |
|
||||
| email_host | smtp.mydomain.com | True |
|
||||
| email_identity | - | True |
|
||||
| email_port | 25 | True |
|
||||
| email_ssl | False | True |
|
||||
| email_username | sample_admin@mydomain.com | True |
|
||||
| ldap_base_dn | ou=people,dc=mydomain,dc=com | True |
|
||||
| ldap_filter | - | True |
|
||||
| ldap_scope | 3 | True |
|
||||
| ldap_search_dn | - | True |
|
||||
| ldap_timeout | 5 | True |
|
||||
| ldap_uid | uid | True |
|
||||
| ldap_url | ldaps://ldap.mydomain.com | True |
|
||||
| project_creation_restriction | everyone | True |
|
||||
| scan_all_policy | {u'parameter': {u'daily_time': 0}, u'type': u'daily'} | True |
|
||||
| self_registration | True | True |
|
||||
| token_expiration | 30 | True |
|
||||
| verify_remote_cert | True | True |
|
||||
+------------------------------+-------------------------------------------------------+----------+
|
||||
```
|
||||
|
||||
### Update user password
|
||||
|
||||
```
|
||||
$ harbor change-password int32bit
|
||||
Old password: *****
|
||||
New Password: *****
|
||||
Retype new Password: *****
|
||||
Update password successfully.
|
||||
```
|
||||
|
||||
### Promote a user to administrator
|
||||
|
||||
```
|
||||
$ harbor promote int32bit
|
||||
Promote user 'int32bit' as administrator successfully.
|
||||
```
|
||||
|
||||
## Licensing
|
||||
|
||||
HarborClient is licensed under the MIT License, Version 2.0. See [LICENSE](./LICENSE) for the full license text.
|
13
contrib/harbor-cli/harborclient/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import pbr.version
|
||||
|
||||
from harborclient import api_versions
|
||||
|
||||
__version__ = pbr.version.VersionInfo('python-harborclient').version_string()
|
||||
|
||||
API_MIN_VERSION = api_versions.APIVersion("2.0")
|
||||
# The max version should be the latest version that is supported in the client,
|
||||
# not necessarily the latest that the server can provide. This is only bumped
|
||||
# when client supported the max version, and bumped sequentially, otherwise
|
||||
# the client may break due to server side new version may include some
|
||||
# backward incompatible change.
|
||||
API_MAX_VERSION = api_versions.APIVersion("2.0")
|
274
contrib/harbor-cli/harborclient/api_versions.py
Normal file
|
@ -0,0 +1,274 @@
|
|||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import re
|
||||
|
||||
import harborclient
|
||||
from harborclient import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_type_error_msg = "'%(other)s' should be an instance of '%(cls)s'"
|
||||
|
||||
|
||||
if not LOG.handlers:
|
||||
LOG.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
class APIVersion(object):
|
||||
"""This class represents an API Version Request.
|
||||
|
||||
This class provides convenience methods for manipulation
|
||||
and comparison of version numbers that we need to do to
|
||||
implement microversions.
|
||||
"""
|
||||
|
||||
def __init__(self, version_str=None):
|
||||
"""Create an API version object.
|
||||
|
||||
:param version_str: String representation of APIVersionRequest.
|
||||
Correct format is 'X.Y', where 'X' and 'Y'
|
||||
are int values. None value should be used
|
||||
to create Null APIVersionRequest, which is
|
||||
equal to 0.0
|
||||
"""
|
||||
self.ver_major = 0
|
||||
self.ver_minor = 0
|
||||
|
||||
if version_str is not None:
|
||||
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str)
|
||||
if match:
|
||||
self.ver_major = int(match.group(1))
|
||||
if match.group(2) == "latest":
|
||||
# NOTE(andreykurilin): Infinity allows to easily determine
|
||||
# latest version and doesn't require any additional checks
|
||||
# in comparison methods.
|
||||
self.ver_minor = float("inf")
|
||||
else:
|
||||
self.ver_minor = int(match.group(2))
|
||||
else:
|
||||
msg = ("Invalid format of client version '%s'. "
|
||||
"Expected format 'X.Y', where X is a major part and Y "
|
||||
"is a minor part of version.") % version_str
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
def __str__(self):
|
||||
"""Debug/Logging representation of object."""
|
||||
if self.is_latest():
|
||||
return "Latest API Version Major: %s" % self.ver_major
|
||||
return ("API Version Major: %s, Minor: %s" % (self.ver_major,
|
||||
self.ver_minor))
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_null():
|
||||
return "<APIVersion: null>"
|
||||
else:
|
||||
return "<APIVersion: %s>" % self.get_string()
|
||||
|
||||
def is_null(self):
|
||||
return self.ver_major == 0 and self.ver_minor == 0
|
||||
|
||||
def is_latest(self):
|
||||
return self.ver_minor == float("inf")
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, APIVersion):
|
||||
raise TypeError(
|
||||
_type_error_msg % {"other": other,
|
||||
"cls": self.__class__})
|
||||
|
||||
return ((self.ver_major, self.ver_minor) <
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, APIVersion):
|
||||
raise TypeError(
|
||||
_type_error_msg % {"other": other,
|
||||
"cls": self.__class__})
|
||||
|
||||
return ((self.ver_major, self.ver_minor) == (other.ver_major,
|
||||
other.ver_minor))
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, APIVersion):
|
||||
raise TypeError(
|
||||
_type_error_msg % {"other": other,
|
||||
"cls": self.__class__})
|
||||
|
||||
return ((self.ver_major, self.ver_minor) >
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __le__(self, other):
|
||||
return self < other or self == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self > other or self == other
|
||||
|
||||
def matches(self, min_version, max_version):
|
||||
"""Matches the version object.
|
||||
|
||||
Returns whether the version object represents a version
|
||||
greater than or equal to the minimum version and less than
|
||||
or equal to the maximum version.
|
||||
|
||||
:param min_version: Minimum acceptable version.
|
||||
:param max_version: Maximum acceptable version.
|
||||
:returns: boolean
|
||||
|
||||
If min_version is null then there is no minimum limit.
|
||||
If max_version is null then there is no maximum limit.
|
||||
If self is null then raise ValueError
|
||||
"""
|
||||
|
||||
if self.is_null():
|
||||
raise ValueError("Null APIVersion doesn't support 'matches'.")
|
||||
if max_version.is_null() and min_version.is_null():
|
||||
return True
|
||||
elif max_version.is_null():
|
||||
return min_version <= self
|
||||
elif min_version.is_null():
|
||||
return self <= max_version
|
||||
else:
|
||||
return min_version <= self <= max_version
|
||||
|
||||
def get_string(self):
|
||||
"""Version string representation.
|
||||
|
||||
Converts object to string representation which if used to create
|
||||
an APIVersion object results in the same version.
|
||||
"""
|
||||
if self.is_null():
|
||||
raise ValueError("Null APIVersion cannot be converted to string.")
|
||||
elif self.is_latest():
|
||||
return "%s.%s" % (self.ver_major, "latest")
|
||||
return "%s.%s" % (self.ver_major, self.ver_minor)
|
||||
|
||||
|
||||
class VersionedMethod(object):
|
||||
def __init__(self, name, start_version, end_version, func):
|
||||
"""Versioning information for a single method
|
||||
|
||||
:param name: Name of the method
|
||||
:param start_version: Minimum acceptable version
|
||||
:param end_version: Maximum acceptable_version
|
||||
:param func: Method to call
|
||||
|
||||
Minimum and maximums are inclusive
|
||||
"""
|
||||
self.name = name
|
||||
self.start_version = start_version
|
||||
self.end_version = end_version
|
||||
self.func = func
|
||||
|
||||
def __str__(self):
|
||||
return ("Version Method %s: min: %s, max: %s" %
|
||||
(self.name, self.start_version, self.end_version))
|
||||
|
||||
def __repr__(self):
|
||||
return "<VersionedMethod %s>" % self.name
|
||||
|
||||
|
||||
def get_available_major_versions():
|
||||
# NOTE(andreykurilin): available clients version should not be
|
||||
# hardcoded, so let's discover them.
|
||||
matcher = re.compile(r"v[0-9]*$")
|
||||
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
|
||||
available_versions = [
|
||||
name[1:] for loader, name, ispkg in submodules if matcher.search(name)
|
||||
]
|
||||
|
||||
return available_versions
|
||||
|
||||
|
||||
def check_major_version(api_version):
|
||||
"""Checks major part of ``APIVersion`` obj is supported.
|
||||
|
||||
:raises harborclient.exceptions.UnsupportedVersion: if major part is not
|
||||
supported
|
||||
"""
|
||||
available_versions = get_available_major_versions()
|
||||
if (not api_version.is_null() and
|
||||
str(api_version.ver_major) not in available_versions):
|
||||
if len(available_versions) == 1:
|
||||
msg = ("Invalid client version '%(version)s'. "
|
||||
"Major part should be '%(major)s'") % {
|
||||
"version": api_version.get_string(),
|
||||
"major": available_versions[0]}
|
||||
else:
|
||||
msg = ("Invalid client version '%(version)s'. "
|
||||
"Major part must be one of: '%(major)s'") % {
|
||||
"version": api_version.get_string(),
|
||||
"major": ", ".join(available_versions)}
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
|
||||
def get_api_version(version_string):
|
||||
"""Returns checked APIVersion object"""
|
||||
version_string = str(version_string)
|
||||
api_version = APIVersion(version_string)
|
||||
check_major_version(api_version)
|
||||
return api_version
|
||||
|
||||
|
||||
def _get_server_version_range(client):
|
||||
version = client.versions.get_current()
|
||||
|
||||
if not hasattr(version, 'version') or not version.version:
|
||||
return APIVersion(), APIVersion()
|
||||
|
||||
return APIVersion(version.min_version), APIVersion(version.version)
|
||||
|
||||
|
||||
def discover_version(client, requested_version):
|
||||
"""Discover most recent version supported by API and client.
|
||||
|
||||
Checks ``requested_version`` and returns the most recent version
|
||||
supported by both the API and the client.
|
||||
|
||||
:param client: client object
|
||||
:param requested_version: requested version represented by APIVersion obj
|
||||
:returns: APIVersion
|
||||
"""
|
||||
server_start_version, server_end_version = _get_server_version_range(
|
||||
client)
|
||||
|
||||
if (not requested_version.is_latest() and
|
||||
requested_version != APIVersion('2.0')):
|
||||
if server_start_version.is_null() and server_end_version.is_null():
|
||||
raise exceptions.UnsupportedVersion(
|
||||
("Server doesn't support microversions"))
|
||||
if not requested_version.matches(server_start_version,
|
||||
server_end_version):
|
||||
raise exceptions.UnsupportedVersion(
|
||||
("The specified version isn't supported by server. The valid "
|
||||
"version range is '%(min)s' to '%(max)s'") % {
|
||||
"min": server_start_version.get_string(),
|
||||
"max": server_end_version.get_string()})
|
||||
return requested_version
|
||||
|
||||
if server_start_version.is_null() and server_end_version.is_null():
|
||||
return APIVersion('2.0')
|
||||
elif harborclient.API_MIN_VERSION > server_end_version:
|
||||
raise exceptions.UnsupportedVersion(
|
||||
("Server version is too old. The client valid version range is "
|
||||
"'%(client_min)s' to '%(client_max)s'. The server valid version "
|
||||
"range is '%(server_min)s' to '%(server_max)s'.") % {
|
||||
'client_min': harborclient.API_MIN_VERSION.get_string(),
|
||||
'client_max': harborclient.API_MAX_VERSION.get_string(),
|
||||
'server_min': server_start_version.get_string(),
|
||||
'server_max': server_end_version.get_string()})
|
||||
elif harborclient.API_MAX_VERSION < server_start_version:
|
||||
raise exceptions.UnsupportedVersion(
|
||||
("Server version is too new. The client valid version range is "
|
||||
"'%(client_min)s' to '%(client_max)s'. The server valid version "
|
||||
"range is '%(server_min)s' to '%(server_max)s'.") % {
|
||||
'client_min': harborclient.API_MIN_VERSION.get_string(),
|
||||
'client_max': harborclient.API_MAX_VERSION.get_string(),
|
||||
'server_min': server_start_version.get_string(),
|
||||
'server_max': server_end_version.get_string()})
|
||||
elif harborclient.API_MAX_VERSION <= server_end_version:
|
||||
return harborclient.API_MAX_VERSION
|
||||
elif server_end_version < harborclient.API_MAX_VERSION:
|
||||
return server_end_version
|
41
contrib/harbor-cli/harborclient/base.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""Manager for API service.
|
||||
|
||||
Managers interact with a particular type of API (projects, users,
|
||||
reposiries,etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self.api.client
|
||||
|
||||
@property
|
||||
def api_version(self):
|
||||
return self.api.api_version
|
||||
|
||||
def _list(self, url, body=None):
|
||||
if body:
|
||||
data = self.api.client.post(url, body=body)
|
||||
else:
|
||||
data = self.api.client.get(url)
|
||||
return data
|
||||
|
||||
def _get(self, url):
|
||||
return self.api.client.get(url)
|
||||
|
||||
def _create(self, url, body=None, **kwargs):
|
||||
return self.api.client.post(url, body=body)
|
||||
|
||||
def _delete(self, url):
|
||||
return self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body, **kwargs):
|
||||
return self.api.client.put(url, body=body)
|
374
contrib/harbor-cli/harborclient/client.py
Normal file
|
@ -0,0 +1,374 @@
|
|||
"""
|
||||
Harbor Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import logging
|
||||
from urlparse import urlparse
|
||||
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from harborclient import api_versions
|
||||
from harborclient import exceptions
|
||||
from harborclient import utils
|
||||
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
USER_AGENT = 'python-harborclient'
|
||||
|
||||
def __init__(self,
|
||||
username,
|
||||
password,
|
||||
project,
|
||||
baseurl,
|
||||
timeout=None,
|
||||
timings=False,
|
||||
http_log_debug=False,
|
||||
cacert=None,
|
||||
insecure=False,
|
||||
api_version=None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.project = project
|
||||
self.baseurl = baseurl
|
||||
self.api_version = api_version or api_versions.APIVersion()
|
||||
self.timings = timings
|
||||
self.http_log_debug = http_log_debug
|
||||
# Has no protocol, use http
|
||||
if not urlparse(baseurl).scheme:
|
||||
self.baseurl = 'http://' + baseurl
|
||||
parsed_url = urlparse(self.baseurl)
|
||||
self.protocol = parsed_url.scheme
|
||||
self.host = parsed_url.hostname
|
||||
self.port = parsed_url.port
|
||||
if timeout is not None:
|
||||
self.timeout = float(timeout)
|
||||
else:
|
||||
self.timeout = None
|
||||
# https
|
||||
if insecure:
|
||||
self.verify_cert = False
|
||||
else:
|
||||
if cacert:
|
||||
self.verify_cert = cacert
|
||||
else:
|
||||
self.verify_cert = True
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self.session_id = None
|
||||
|
||||
if self.http_log_debug and not self._logger.handlers:
|
||||
# Logging level is already set on the root logger
|
||||
ch = logging.StreamHandler()
|
||||
self._logger.addHandler(ch)
|
||||
self._logger.propagate = False
|
||||
if hasattr(requests, 'logging'):
|
||||
rql = requests.logging.getLogger(requests.__name__)
|
||||
rql.addHandler(ch)
|
||||
# Since we have already setup the root logger on debug, we
|
||||
# have to set it up here on WARNING (its original level)
|
||||
# otherwise we will get all the requests logging messages
|
||||
rql.setLevel(logging.WARNING)
|
||||
|
||||
def unauthenticate(self):
|
||||
"""Forget all of our authentication information."""
|
||||
requests.get(
|
||||
'%s://%s/logout' % (self.protocol, self.host),
|
||||
cookies={'beegosessionID': self.session_id},
|
||||
verify=self.verify_cert)
|
||||
logging.debug("Successfully logout")
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def reset_timings(self):
|
||||
self.times = []
|
||||
|
||||
def _redact(self, target, path, text=None):
|
||||
"""Replace the value of a key in `target`.
|
||||
|
||||
The key can be at the top level by specifying a list with a single
|
||||
key as the path. Nested dictionaries are also supported by passing a
|
||||
list of keys to be navigated to find the one that should be replaced.
|
||||
In this case the last one is the one that will be replaced.
|
||||
|
||||
:param dict target: the dictionary that may have a key to be redacted;
|
||||
modified in place
|
||||
:param list path: a list representing the nested structure in `target`
|
||||
that should be redacted; modified in place
|
||||
:param string text: optional text to use as a replacement for the
|
||||
redacted key. if text is not specified, the
|
||||
default text will be sha1 hash of the value being
|
||||
redacted
|
||||
"""
|
||||
|
||||
key = path.pop()
|
||||
|
||||
# move to the most nested dict
|
||||
for p in path:
|
||||
try:
|
||||
target = target[p]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if key in target:
|
||||
if text:
|
||||
target[key] = text
|
||||
elif target[key] is not None:
|
||||
# because in python3 byte string handling is ... ug
|
||||
value = target[key].encode('utf-8')
|
||||
sha1sum = hashlib.sha1(value)
|
||||
target[key] = "{SHA1}%s" % sha1sum.hexdigest()
|
||||
|
||||
def http_log_req(self, method, url, kwargs):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
string_parts = ['curl -g -i']
|
||||
|
||||
if self.verify_cert is not None:
|
||||
if not self.verify_cert:
|
||||
string_parts.append(' --insecure')
|
||||
|
||||
string_parts.append(" '%s'" % url)
|
||||
string_parts.append(' -X %s' % method)
|
||||
|
||||
headers = copy.deepcopy(kwargs['headers'])
|
||||
# because dict ordering changes from 2 to 3
|
||||
keys = sorted(headers.keys())
|
||||
for name in keys:
|
||||
value = headers[name]
|
||||
header = ' -H "%s: %s"' % (name, value)
|
||||
string_parts.append(header)
|
||||
cookies = kwargs['cookies']
|
||||
for name in sorted(cookies.keys()):
|
||||
value = cookies[name]
|
||||
cookie = header = ' -b "%s: %s"' % (name, value)
|
||||
string_parts.append(cookie)
|
||||
if 'data' in kwargs:
|
||||
data = json.loads(kwargs['data'])
|
||||
string_parts.append(" -d '%s'" % json.dumps(data))
|
||||
self._logger.debug("REQ: %s" % "".join(string_parts))
|
||||
|
||||
def http_log_resp(self, resp):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
if resp.text and resp.status_code != 400:
|
||||
try:
|
||||
body = json.loads(resp.text)
|
||||
except ValueError:
|
||||
body = None
|
||||
else:
|
||||
body = None
|
||||
|
||||
self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: "
|
||||
"%(text)s\n", {
|
||||
'status': resp.status_code,
|
||||
'headers': resp.headers,
|
||||
'text': json.dumps(body)
|
||||
})
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
url = self.baseurl + "/api" + url
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['body'])
|
||||
del kwargs['body']
|
||||
kwargs["headers"]['Harbor-API-Version'] = "v2"
|
||||
if self.timeout is not None:
|
||||
kwargs.setdefault('timeout', self.timeout)
|
||||
|
||||
self.http_log_req(method, url, kwargs)
|
||||
|
||||
resp = requests.request(method, url, verify=self.verify_cert, **kwargs)
|
||||
self.http_log_resp(resp)
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, resp.text, url, method)
|
||||
try:
|
||||
body = json.loads(resp.text)
|
||||
except ValueError:
|
||||
body = resp.text
|
||||
return body
|
||||
|
||||
def _time_request(self, url, method, **kwargs):
|
||||
with utils.record_time(self.times, self.timings, method, url):
|
||||
body = self.request(url, method, **kwargs)
|
||||
return body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.session_id:
|
||||
self.authenticate()
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
body = self._time_request(
|
||||
url,
|
||||
method,
|
||||
cookies={'beegosessionID': self.session_id},
|
||||
**kwargs)
|
||||
return body
|
||||
except exceptions.Unauthorized as e:
|
||||
try:
|
||||
# first discard auth token, to avoid the possibly expired
|
||||
# token being re-used in the re-authentication attempt
|
||||
self.unauthenticate()
|
||||
# overwrite bad token
|
||||
self.authenticate()
|
||||
body = self._time_request(url, method, **kwargs)
|
||||
return body
|
||||
except exceptions.Unauthorized:
|
||||
raise e
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def authenticate(self):
|
||||
if not self.baseurl:
|
||||
msg = ("Authentication requires 'baseurl', which should be "
|
||||
"specified in '%s'") % self.__class__.__name__
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
|
||||
if not self.username:
|
||||
msg = ("Authentication requires 'username', which should be "
|
||||
"specified in '%s'") % self.__class__.__name__
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
|
||||
if not self.password:
|
||||
msg = ("Authentication requires 'password', which should be "
|
||||
"specified in '%s'") % self.__class__.__name__
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.baseurl + "/login",
|
||||
data={'principal': self.username,
|
||||
'password': self.password},
|
||||
verify=self.verify_cert)
|
||||
except requests.exceptions.SSLError:
|
||||
msg = ("Certificate verify failed, please use '--os-cacert' option"
|
||||
" to specify a CA bundle file to use in verifying a TLS"
|
||||
" (https) server certificate or use '--insecure' option"
|
||||
" to explicitly allow client to perform insecure"
|
||||
" TLS (https) requests.")
|
||||
raise exceptions.AuthorizationFailure(msg)
|
||||
if resp.status_code == 200:
|
||||
self.session_id = resp.cookies.get('beegosessionID')
|
||||
logging.debug(
|
||||
"Successfully login, session id: %s" % self.session_id)
|
||||
if resp.status_code >= 400:
|
||||
msg = resp.text or ("The request you have made requires "
|
||||
"authentication. (HTTP 401)")
|
||||
reason = '{"reason": "%s", "message": "%s"}' % (resp.reason, msg)
|
||||
raise exceptions.AuthorizationFailure(reason)
|
||||
|
||||
|
||||
def _construct_http_client(username=None,
|
||||
password=None,
|
||||
project=None,
|
||||
baseurl=None,
|
||||
timeout=None,
|
||||
extensions=None,
|
||||
timings=False,
|
||||
http_log_debug=False,
|
||||
user_agent='python-harborclient',
|
||||
api_version=None,
|
||||
insecure=False,
|
||||
cacert=None,
|
||||
**kwargs):
|
||||
return HTTPClient(
|
||||
username,
|
||||
password,
|
||||
project,
|
||||
baseurl,
|
||||
timeout=timeout,
|
||||
timings=timings,
|
||||
http_log_debug=http_log_debug,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
api_version=api_version)
|
||||
|
||||
|
||||
def _get_client_class_and_version(version):
|
||||
if not isinstance(version, api_versions.APIVersion):
|
||||
version = api_versions.get_api_version(version)
|
||||
else:
|
||||
api_versions.check_major_version(version)
|
||||
if version.is_latest():
|
||||
raise exceptions.UnsupportedVersion(("The version should be explicit, "
|
||||
"not latest."))
|
||||
return version, importutils.import_class(
|
||||
"harborclient.v%s.client.Client" % version.ver_major)
|
||||
|
||||
|
||||
def get_client_class(version):
|
||||
"""Returns Client class based on given version."""
|
||||
_api_version, client_class = _get_client_class_and_version(version)
|
||||
return client_class
|
||||
|
||||
|
||||
def Client(version,
|
||||
username=None,
|
||||
password=None,
|
||||
project=None,
|
||||
baseurl=None,
|
||||
insecure=False,
|
||||
cacert=None,
|
||||
*args,
|
||||
**kwargs):
|
||||
"""Initialize client object based on given version.
|
||||
|
||||
HOW-TO:
|
||||
The simplest way to create a client instance is initialization with your
|
||||
credentials::
|
||||
|
||||
>>> from harborclient import client
|
||||
>>> harbor = client.Client(VERSION, USERNAME, PASSWORD,
|
||||
... PROJECT, HARBOR_URL)
|
||||
|
||||
Here ``VERSION`` can be a string or
|
||||
``harborclient.api_versions.APIVersion`` obj. If you prefer string value,
|
||||
you can use ``1.1`` (deprecated now), ``2`` or ``2.X``
|
||||
(where X is a microversion).
|
||||
|
||||
|
||||
Alternatively, you can create a client instance using the keystoneauth
|
||||
session API. See "The harborclient Python API" page at
|
||||
python-harborclient's doc.
|
||||
"""
|
||||
api_version, client_class = _get_client_class_and_version(version)
|
||||
kwargs.pop("direct_use", None)
|
||||
return client_class(
|
||||
username=username,
|
||||
password=password,
|
||||
project=project,
|
||||
baseurl=baseurl,
|
||||
api_version=api_version,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
*args,
|
||||
**kwargs)
|
194
contrib/harbor-cli/harborclient/exceptions.py
Normal file
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
|
||||
class UnsupportedVersion(Exception):
|
||||
"""Unsupport API version.
|
||||
|
||||
Indicates that the user is trying to use an unsupported version of the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedAttribute(AttributeError):
|
||||
"""Unsupport attribute
|
||||
|
||||
Indicates that the user is trying to transmit the argument to a method,
|
||||
which is not supported by selected version.
|
||||
"""
|
||||
|
||||
def __init__(self, argument_name, start_version, end_version=None):
|
||||
if end_version:
|
||||
self.message = (
|
||||
"'%(name)s' argument is only allowed for microversions "
|
||||
"%(start)s - %(end)s." % {
|
||||
"name": argument_name,
|
||||
"start": start_version,
|
||||
"end": end_version
|
||||
})
|
||||
else:
|
||||
self.message = (
|
||||
"'%(name)s' argument is only allowed since microversion "
|
||||
"%(start)s." % {
|
||||
"name": argument_name,
|
||||
"start": start_version
|
||||
})
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises."""
|
||||
message = 'Unknown Error'
|
||||
|
||||
def __init__(self,
|
||||
code,
|
||||
message=None,
|
||||
details=None,
|
||||
request_id=None,
|
||||
url=None,
|
||||
method=None):
|
||||
self.code = code
|
||||
self.message = message or self.__class__.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.url = url
|
||||
self.method = method
|
||||
|
||||
def __str__(self):
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.code)
|
||||
if self.request_id:
|
||||
formatted_string += " (Request-ID: %s)" % self.request_id
|
||||
|
||||
return formatted_string
|
||||
|
||||
|
||||
class RetryAfterException(ClientException):
|
||||
"""Retry exception
|
||||
|
||||
The base exception class for ClientExceptions that use Retry-After header.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RetryAfterException, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BadRequest(ClientException):
|
||||
"""HTTP 400 - Bad request: you sent some malformed data."""
|
||||
http_status = 400
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(ClientException):
|
||||
"""HTTP 401 - Unauthorized: bad credentials."""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(ClientException):
|
||||
"""HTTP 403 - Forbidden
|
||||
|
||||
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||
resource.
|
||||
"""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(ClientException):
|
||||
"""HTTP 404 - Not found"""
|
||||
http_status = 404
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class MethodNotAllowed(ClientException):
|
||||
"""HTTP 405 - Method Not Allowed"""
|
||||
http_status = 405
|
||||
message = "Method Not Allowed"
|
||||
|
||||
|
||||
class NotAcceptable(ClientException):
|
||||
"""HTTP 406 - Not Acceptable"""
|
||||
http_status = 406
|
||||
message = "Not Acceptable"
|
||||
|
||||
|
||||
class Conflict(ClientException):
|
||||
"""HTTP 409 - Conflict"""
|
||||
http_status = 409
|
||||
message = "Conflict"
|
||||
|
||||
|
||||
class OverLimit(RetryAfterException):
|
||||
"""HTTP 413 - Over limit
|
||||
|
||||
You're over the API limits for this time period.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Over limit"
|
||||
|
||||
|
||||
class RateLimit(RetryAfterException):
|
||||
"""HTTP 429 - Rate limit
|
||||
|
||||
you've sent too many requests for this time period.
|
||||
"""
|
||||
http_status = 429
|
||||
message = "Rate limit"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(ClientException):
|
||||
"""HTTP 501 - Not Implemented
|
||||
|
||||
the server does not support this operation.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in ClientException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_error_classes = [
|
||||
BadRequest, Unauthorized, Forbidden, NotFound, MethodNotAllowed,
|
||||
NotAcceptable, Conflict, OverLimit, RateLimit, HTTPNotImplemented
|
||||
]
|
||||
_code_map = dict((c.http_status, c) for c in _error_classes)
|
||||
|
||||
|
||||
def from_response(response, body, url, method=None):
|
||||
"""Extract exception from response
|
||||
|
||||
Return an instance of an ClientException or subclass baseda
|
||||
on a requests response.
|
||||
|
||||
Usage::
|
||||
|
||||
resp, body = requests.request(...)
|
||||
if resp.status_code != 200:
|
||||
raise exception_from_response(resp, rest.text)
|
||||
"""
|
||||
cls = _code_map.get(response.status_code, ClientException)
|
||||
kwargs = {
|
||||
'code': response.status_code,
|
||||
'method': method,
|
||||
'url': url,
|
||||
'message': body.strip(),
|
||||
}
|
||||
return cls(**kwargs)
|
414
contrib/harbor-cli/harborclient/shell.py
Normal file
|
@ -0,0 +1,414 @@
|
|||
"""
|
||||
Command-line interface to the Harbor API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
|
||||
import harborclient
|
||||
from harborclient import api_versions
|
||||
from harborclient import client
|
||||
from harborclient import exceptions as exc
|
||||
from harborclient import utils
|
||||
|
||||
DEFAULT_API_VERSION = "2.0"
|
||||
DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HarborClientArgumentParser(argparse.ArgumentParser):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HarborClientArgumentParser, self).__init__(*args, **kwargs)
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
"""
|
||||
self.print_usage(sys.stderr)
|
||||
# FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
||||
choose_from = ' (choose from'
|
||||
progparts = self.prog.partition(' ')
|
||||
self.exit(2,
|
||||
("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
||||
" for more information.\n") % {
|
||||
'errmsg': message.split(choose_from)[0],
|
||||
'mainp': progparts[0],
|
||||
'subp': progparts[2]})
|
||||
|
||||
def _get_option_tuples(self, option_string):
|
||||
"""returns (action, option, value) candidates for an option prefix
|
||||
|
||||
Returns [first candidate] if all candidates refers to current and
|
||||
deprecated forms of the same options parsing succeed.
|
||||
"""
|
||||
option_tuples = (super(HarborClientArgumentParser, self)
|
||||
._get_option_tuples(option_string))
|
||||
if len(option_tuples) > 1:
|
||||
normalizeds = [
|
||||
option.replace('_', '-')
|
||||
for action, option, value in option_tuples
|
||||
]
|
||||
if len(set(normalizeds)) == 1:
|
||||
return option_tuples[:1]
|
||||
return option_tuples
|
||||
|
||||
|
||||
class HarborShell(object):
|
||||
times = []
|
||||
|
||||
def _append_global_identity_args(self, parser, argv):
|
||||
# Register the CLI arguments that have moved to the session object.
|
||||
parser.set_defaults(os_username=utils.env('HARBOR_USERNAME'))
|
||||
parser.set_defaults(os_password=utils.env('HARBOR_PASSWORD'))
|
||||
parser.set_defaults(os_project=utils.env('HARBOR_PROJECT'))
|
||||
parser.set_defaults(os_baseurl=utils.env('HARBOR_URL'))
|
||||
|
||||
def get_base_parser(self, argv):
|
||||
parser = HarborClientArgumentParser(
|
||||
prog='harbor',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "harbor help COMMAND" '
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=HarborHelpFormatter, )
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument(
|
||||
'-h',
|
||||
'--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS, )
|
||||
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Print debugging output.")
|
||||
|
||||
parser.add_argument(
|
||||
'--timings',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Print call timing info.")
|
||||
|
||||
parser.add_argument(
|
||||
'--version', action='version', version=harborclient.__version__)
|
||||
|
||||
parser.add_argument(
|
||||
'--os-username',
|
||||
dest='os_username',
|
||||
metavar='<username>',
|
||||
help='Username')
|
||||
|
||||
parser.add_argument(
|
||||
'--os-password',
|
||||
dest='os_password',
|
||||
metavar='<password>',
|
||||
help="User's password")
|
||||
|
||||
parser.add_argument(
|
||||
'--os-project',
|
||||
dest='os_project',
|
||||
metavar='<project>',
|
||||
help="Project Id")
|
||||
|
||||
parser.add_argument(
|
||||
'--timeout',
|
||||
metavar='<timeout>',
|
||||
help="Set request timeout (in seconds).")
|
||||
|
||||
parser.add_argument(
|
||||
'--os-baseurl',
|
||||
metavar='<baseurl>',
|
||||
help='API base url')
|
||||
|
||||
parser.add_argument(
|
||||
'--insecure',
|
||||
default=False,
|
||||
action='store_true',
|
||||
dest='insecure',
|
||||
help='Explicitly allow client to perform '
|
||||
'"insecure" TLS (https) requests. The '
|
||||
'server\'s certificate will not be verified '
|
||||
'against any certificate authorities. This '
|
||||
'option should be used with caution.')
|
||||
|
||||
parser.add_argument(
|
||||
'--os-cacert',
|
||||
dest='os_cacert',
|
||||
metavar='<ca-certificate>',
|
||||
default=os.environ.get('OS_CACERT'),
|
||||
help='Specify a CA bundle file to use in '
|
||||
'verifying a TLS (https) server certificate. '
|
||||
'Defaults to env[OS_CACERT].')
|
||||
|
||||
parser.add_argument(
|
||||
'--os-api-version',
|
||||
metavar='<api-version>',
|
||||
default=utils.env(
|
||||
'HARBOR_API_VERSION', default=DEFAULT_API_VERSION),
|
||||
help=('Accepts X, X.Y (where X is major and Y is minor part) or '
|
||||
'"X.latest", defaults to env[HARBOR_API_VERSION].'))
|
||||
|
||||
self._append_global_identity_args(parser, argv)
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version, do_help=False, argv=None):
|
||||
parser = self.get_base_parser(argv)
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
actions_module = importutils.import_module(
|
||||
"harborclient.v%s.shell" % version.ver_major)
|
||||
|
||||
self._find_actions(subparsers, actions_module, version, do_help)
|
||||
self._find_actions(subparsers, self, version, do_help)
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = subparsers.add_parser(
|
||||
'bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=HarborHelpFormatter)
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _find_actions(self, subparsers, actions_module, version, do_help):
|
||||
msg = " (Supported by API versions '%(start)s' - '%(end)s')"
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hyphen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
if hasattr(callback, "versioned"):
|
||||
additional_msg = ""
|
||||
subs = api_versions.get_substitutions(
|
||||
utils.get_function_name(callback))
|
||||
if do_help:
|
||||
additional_msg = msg % {
|
||||
'start': subs[0].start_version.get_string(),
|
||||
'end': subs[-1].end_version.get_string()
|
||||
}
|
||||
subs = [
|
||||
versioned_method for versioned_method in subs
|
||||
if version.matches(versioned_method.start_version,
|
||||
versioned_method.end_version)
|
||||
]
|
||||
if subs:
|
||||
# use the "latest" substitution
|
||||
callback = subs[-1].func
|
||||
else:
|
||||
# there is no proper versioned method
|
||||
continue
|
||||
desc = callback.__doc__ or desc
|
||||
desc += additional_msg
|
||||
|
||||
action_help = desc.strip()
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
command,
|
||||
help=action_help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HarborHelpFormatter)
|
||||
subparser.add_argument(
|
||||
'-h',
|
||||
'--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS, )
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
start_version = kwargs.get("start_version", None)
|
||||
if start_version:
|
||||
start_version = api_versions.APIVersion(start_version)
|
||||
end_version = kwargs.get("end_version", None)
|
||||
if end_version:
|
||||
end_version = api_versions.APIVersion(end_version)
|
||||
else:
|
||||
end_version = api_versions.APIVersion(
|
||||
"%s.latest" % start_version.ver_major)
|
||||
if do_help:
|
||||
kwargs["help"] = kwargs.get("help", "") + (
|
||||
msg % {
|
||||
"start": start_version.get_string(),
|
||||
"end": end_version.get_string()
|
||||
})
|
||||
if not version.matches(start_version, end_version):
|
||||
continue
|
||||
kw = kwargs.copy()
|
||||
kw.pop("start_version", None)
|
||||
kw.pop("end_version", None)
|
||||
subparser.add_argument(*args, **kw)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def setup_debugging(self, debug):
|
||||
if not debug:
|
||||
return
|
||||
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
|
||||
logging.basicConfig(level=logging.DEBUG, format=streamformat)
|
||||
logging.getLogger('iso8601').setLevel(logging.WARNING)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version and debug settings
|
||||
parser = self.get_base_parser(argv)
|
||||
(args, args_list) = parser.parse_known_args(argv)
|
||||
self.setup_debugging(args.debug)
|
||||
do_help = ('help' in argv) or ('--help' in argv) or (
|
||||
'-h' in argv) or not argv
|
||||
|
||||
# bash-completion should not require authentication
|
||||
if not args.os_api_version:
|
||||
api_version = api_versions.get_api_version(
|
||||
DEFAULT_MAJOR_OS_COMPUTE_API_VERSION)
|
||||
else:
|
||||
api_version = api_versions.get_api_version(args.os_api_version)
|
||||
|
||||
os_username = args.os_username
|
||||
os_password = args.os_password
|
||||
os_project = args.os_project
|
||||
os_baseurl = args.os_baseurl
|
||||
subcommand_parser = self.get_subcommand_parser(
|
||||
api_version, do_help=do_help, argv=argv)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
if args.help or not argv:
|
||||
subcommand_parser.print_help()
|
||||
return 0
|
||||
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
insecure = args.insecure
|
||||
cacert = args.os_cacert
|
||||
if not os_baseurl:
|
||||
print(("ERROR (CommandError): You must provide harbor url via "
|
||||
"either --os-baseurl or env[HARBOR_URL]."))
|
||||
return 1
|
||||
if not os_username:
|
||||
print(("ERROR (CommandError): You must provide username via "
|
||||
"either --os-username or env[HARBOR_USERNAME]."))
|
||||
return 1
|
||||
if not os_project:
|
||||
print(("ERROR (CommandError): You must provide project via "
|
||||
"either --os-project or env[HARBOR_PROJECT]."))
|
||||
return 1
|
||||
while not os_password:
|
||||
os_password = getpass.getpass("password: ")
|
||||
self.cs = client.Client(
|
||||
api_version,
|
||||
os_username,
|
||||
os_password,
|
||||
os_project,
|
||||
os_baseurl,
|
||||
timings=args.timings,
|
||||
http_log_debug=args.debug,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
timeout=args.timeout)
|
||||
try:
|
||||
self.cs.authenticate()
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError("Invalid Harbor credentials.")
|
||||
except exc.AuthorizationFailure as e:
|
||||
raise exc.CommandError("Unable to authorize user '%s': %s"
|
||||
% (os_username, e))
|
||||
args.func(self.cs, args)
|
||||
if args.timings:
|
||||
self._dump_timings(self.times + self.cs.get_timings())
|
||||
|
||||
def _dump_timings(self, timings):
|
||||
results = [{
|
||||
"url": url,
|
||||
"seconds": end - start
|
||||
} for url, start, end in timings]
|
||||
total = 0.0
|
||||
for tyme in results:
|
||||
total += tyme['seconds']
|
||||
results.append({"url": "Total", "seconds": total})
|
||||
utils.print_list(results, ["url", "seconds"], align='l')
|
||||
print("Total: %s seconds" % total)
|
||||
|
||||
def do_bash_completion(self, _args):
|
||||
"""Print bash completion
|
||||
|
||||
Prints all of the commands and options to stdout so that the
|
||||
harbor.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = list()
|
||||
options = list()
|
||||
for sc_str, sc in self.subcommands.items():
|
||||
commands.append(sc_str)
|
||||
for option in sc._optionals._option_string_actions.keys():
|
||||
options.append(option)
|
||||
|
||||
options.extend(self.parser._option_string_actions.keys())
|
||||
print(' '.join(set(commands + options)))
|
||||
|
||||
@utils.arg(
|
||||
'command',
|
||||
metavar='<subcommand>',
|
||||
nargs='?',
|
||||
help='Display help for <subcommand>.')
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
if args.command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError(
|
||||
("'%s' is not a valid subcommand") % args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class HarborHelpFormatter(argparse.HelpFormatter):
|
||||
def __init__(self,
|
||||
prog,
|
||||
indent_increment=2,
|
||||
max_help_position=32,
|
||||
width=None):
|
||||
super(HarborHelpFormatter, self).__init__(prog, indent_increment,
|
||||
max_help_position, width)
|
||||
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(HarborHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]]
|
||||
HarborShell().main(argv)
|
||||
except KeyboardInterrupt:
|
||||
print("... terminating harbor client", file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except exc.CommandError as e:
|
||||
print("CommandError: %s" % e)
|
||||
sys.exit(127)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
165
contrib/harbor-cli/harborclient/utils.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
import contextlib
|
||||
import os
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
import prettytable
|
||||
import six
|
||||
|
||||
|
||||
def env(*args, **kwargs):
|
||||
"""Returns the first environment variable set.
|
||||
|
||||
If all are empty, defaults to '' or keyword arg `default`.
|
||||
"""
|
||||
for arg in args:
|
||||
value = os.environ.get(arg)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args.
|
||||
|
||||
Example:
|
||||
|
||||
>>> @arg("name", help="Name of the new entity")
|
||||
... def entity_create(args):
|
||||
... pass
|
||||
"""
|
||||
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def add_arg(func, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(func, 'arguments'):
|
||||
func.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in func.arguments:
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def pretty_choice_dict(d):
|
||||
"""Returns a formatted dict as 'key=value'."""
|
||||
return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())])
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}, sortby=None, align='c'):
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.align = align
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
if callable(formatters[field]):
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
row.append(o.get(formatters[field], None))
|
||||
else:
|
||||
data = o.get(field, None)
|
||||
if data is None or data == "":
|
||||
data = '-'
|
||||
data = six.text_type(data).replace("\r", "")
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
if sortby is not None and sortby in fields:
|
||||
result = encodeutils.safe_encode(pt.get_string(sortby=sortby))
|
||||
else:
|
||||
result = encodeutils.safe_encode(pt.get_string())
|
||||
|
||||
if six.PY3:
|
||||
result = result.decode()
|
||||
|
||||
print(result)
|
||||
|
||||
|
||||
def print_dict(d, dict_property="Property", dict_value="Value", wrap=0):
|
||||
pt = prettytable.PrettyTable([dict_property, dict_value], caching=False)
|
||||
pt.align = 'l'
|
||||
for k, v in sorted(d.items()):
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, (dict, list)):
|
||||
v = jsonutils.dumps(v)
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(six.text_type(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v):
|
||||
# '\r' would break the table, so remove it.
|
||||
if '\r' in v:
|
||||
v = v.replace('\r', '')
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
else:
|
||||
if v is None:
|
||||
v = '-'
|
||||
pt.add_row([k, v])
|
||||
|
||||
result = encodeutils.safe_encode(pt.get_string())
|
||||
|
||||
if six.PY3:
|
||||
result = result.decode()
|
||||
|
||||
print(result)
|
||||
|
||||
|
||||
def safe_issubclass(*args):
|
||||
"""Like issubclass, but will just return False if not a class."""
|
||||
|
||||
try:
|
||||
if issubclass(*args):
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def record_time(times, enabled, *args):
|
||||
"""Record the time of a specific action.
|
||||
|
||||
:param times: A list of tuples holds time data.
|
||||
:type times: list
|
||||
:param enabled: Whether timing is enabled.
|
||||
:type enabled: bool
|
||||
:param *args: Other data to be stored besides time data, these args
|
||||
will be joined to a string.
|
||||
"""
|
||||
if not enabled:
|
||||
yield
|
||||
else:
|
||||
start = time.time()
|
||||
yield
|
||||
end = time.time()
|
||||
times.append((' '.join(args), start, end))
|
||||
|
||||
|
||||
def get_function_name(func):
|
||||
if six.PY2:
|
||||
if hasattr(func, "im_class"):
|
||||
return "%s.%s" % (func.im_class, func.__name__)
|
||||
else:
|
||||
return "%s.%s" % (func.__module__, func.__name__)
|
||||
else:
|
||||
return "%s.%s" % (func.__module__, func.__qualname__)
|
1
contrib/harbor-cli/harborclient/v2/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from harborclient.v2.client import Client # noqa
|
73
contrib/harbor-cli/harborclient/v2/client.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from harborclient import client
|
||||
from harborclient.v2 import configurations
|
||||
from harborclient.v2 import jobs
|
||||
from harborclient.v2 import logs
|
||||
from harborclient.v2 import projects
|
||||
from harborclient.v2 import repositories
|
||||
from harborclient.v2 import searcher
|
||||
from harborclient.v2 import statistics
|
||||
from harborclient.v2 import systeminfo
|
||||
from harborclient.v2 import targets
|
||||
from harborclient.v2 import users
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Top-level object to access the Harbor API.
|
||||
|
||||
.. warning:: All scripts and projects should not initialize this class
|
||||
directly. It should be done via `harborclient.client.Client` interface.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
username=None,
|
||||
password=None,
|
||||
project=None,
|
||||
baseurl=None,
|
||||
insecure=False,
|
||||
cacert=None,
|
||||
api_version=None,
|
||||
*argv,
|
||||
**kwargs):
|
||||
"""Initialization of Client object.
|
||||
|
||||
:param str username: Username
|
||||
:param str password: Password
|
||||
:param str project: Project
|
||||
"""
|
||||
self.baseurl = baseurl
|
||||
self.users = users.UserManager(self)
|
||||
self.projects = projects.ProjectManager(self)
|
||||
self.jobs = jobs.JobManager(self)
|
||||
self.repositories = repositories.RepositoryManager(self)
|
||||
self.searcher = searcher.SearchManager(self)
|
||||
self.statistics = statistics.StatisticsManager(self)
|
||||
self.logs = logs.LogManager(self)
|
||||
self.targets = targets.TargetManager(self)
|
||||
self.systeminfo = systeminfo.SystemInfoManager(self)
|
||||
self.configurations = configurations.ConfigurationManager(self)
|
||||
self.client = client._construct_http_client(
|
||||
username=username,
|
||||
password=password,
|
||||
project=project,
|
||||
baseurl=baseurl,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
api_version=api_version,
|
||||
**kwargs)
|
||||
|
||||
def get_timings(self):
|
||||
return self.client.get_timings()
|
||||
|
||||
def reset_timings(self):
|
||||
self.client.reset_timings()
|
||||
|
||||
def authenticate(self):
|
||||
"""Authenticate against the server.
|
||||
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
7
contrib/harbor-cli/harborclient/v2/configurations.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class ConfigurationManager(base.Manager):
|
||||
def get(self):
|
||||
"""Get system configurations."""
|
||||
return self._get("/configurations")
|
11
contrib/harbor-cli/harborclient/v2/jobs.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class JobManager(base.Manager):
|
||||
def list(self, policy_id=None):
|
||||
"""List filters jobs according to the policy and repository."""
|
||||
return self._list("/jobs/replication?policy_id=%s" % policy_id)
|
||||
|
||||
def get_log(self, job_id):
|
||||
"""Get job logs."""
|
||||
return self._get("/jobs/replication/%s/log" % job_id)
|
7
contrib/harbor-cli/harborclient/v2/logs.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class LogManager(base.Manager):
|
||||
def list(self):
|
||||
"""Get recent logs of the projects which the user is a member of."""
|
||||
return self._list("/logs")
|
44
contrib/harbor-cli/harborclient/v2/projects.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from harborclient import base
|
||||
from harborclient import exceptions as exp
|
||||
|
||||
|
||||
class ProjectManager(base.Manager):
|
||||
def is_id(self, key):
|
||||
return key.isdigit()
|
||||
|
||||
def get(self, id):
|
||||
"""Return specific project detail infomation."""
|
||||
return self._get("/projects/%s" % id)
|
||||
|
||||
def list(self):
|
||||
"""List projects."""
|
||||
return self._list("/projects")
|
||||
|
||||
def get_id_by_name(self, name):
|
||||
"""Return specific project detail infomation by name."""
|
||||
projects = self.list()
|
||||
for p in projects:
|
||||
if p['name'] == name:
|
||||
return p['project_id']
|
||||
raise exp.NotFound("Project '%s' not Found." % name)
|
||||
|
||||
def get_name_by_id(self, id):
|
||||
"""Return specific project detail infomation by id."""
|
||||
projects = self.list()
|
||||
for p in projects:
|
||||
if p['project_id'] == id:
|
||||
return p['name']
|
||||
raise exp.NotFound("Project '%s' not Found." % id)
|
||||
|
||||
def create(self, name, public=True):
|
||||
"""Create a new project."""
|
||||
project = {"project_name": name, "public": 1 if public else 0}
|
||||
return self._create("/projects", project)
|
||||
|
||||
def delete(self, id):
|
||||
"""Delete project by id."""
|
||||
return self._delete("/projects/%s" % id)
|
||||
|
||||
def get_members(self, id):
|
||||
"""Return a project's relevant role members."""
|
||||
return self._list("/projects/%s/members/" % id)
|
27
contrib/harbor-cli/harborclient/v2/repositories.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class RepositoryManager(base.Manager):
|
||||
def get(self, id):
|
||||
"""Get a Repository."""
|
||||
return self._get("/repositories/%s" % id)
|
||||
|
||||
def list(self, project):
|
||||
"""Get repositories accompany with relevant project and repo name."""
|
||||
repositories = self._list("/repositories?project_id=%s" % project)
|
||||
return repositories
|
||||
|
||||
def list_tags(self, repo_name):
|
||||
"""Get the tag of the repository."""
|
||||
return self.api.client.get(
|
||||
"/repositories/%s/tags" % repo_name)
|
||||
|
||||
def get_manifests(self, repo_name, tag):
|
||||
"""Get manifests of a relevant repository."""
|
||||
return self.api.client.get(
|
||||
"/repositories/%(repo_name)s/tags/%(tag)s/manifest"
|
||||
% {"repo_name": repo_name, "tag": tag})
|
||||
|
||||
def get_top(self, count):
|
||||
"""Get public repositories which are accessed most."""
|
||||
return self._list("/repositories/top?count=%s" % count)
|
7
contrib/harbor-cli/harborclient/v2/searcher.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class SearchManager(base.Manager):
|
||||
def search(self, query):
|
||||
"""Search for projects and repositories."""
|
||||
return self.api.client.get("/search?q=%s" % query)
|
607
contrib/harbor-cli/harborclient/v2/shell.py
Normal file
|
@ -0,0 +1,607 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
import logging
|
||||
|
||||
from oslo_utils import strutils
|
||||
|
||||
from harborclient import exceptions as exp
|
||||
from harborclient import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_id(obj):
|
||||
try:
|
||||
int(obj)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--sortby',
|
||||
metavar='<sortby>',
|
||||
dest="sortby",
|
||||
default="user_id",
|
||||
help='Sort key.')
|
||||
def do_user_list(cs, args):
|
||||
"""Get registered users of Harbor."""
|
||||
try:
|
||||
users = cs.users.list()
|
||||
except exp.Forbidden as e:
|
||||
raise exp.CommandError(e.message)
|
||||
# Get admin user
|
||||
try:
|
||||
admin = cs.users.get(1)
|
||||
users.append(admin)
|
||||
except Exception:
|
||||
pass
|
||||
fields = ['user_id', 'username', 'is_admin',
|
||||
'email', 'realname', 'comment']
|
||||
formatters = {"is_admin": 'has_admin_role'}
|
||||
utils.print_list(users, fields, formatters=formatters, sortby=args.sortby)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'user',
|
||||
metavar='<user>',
|
||||
help='User name or id')
|
||||
def do_set_admin(cs, args):
|
||||
"""Update a registered user to change to be an administrator of Harbor."""
|
||||
try:
|
||||
user = cs.users.find(args.user)
|
||||
except exp.NotFound:
|
||||
print("User '%s' not found." % args.user)
|
||||
cs.users.set_admin(user['user_id'], True)
|
||||
print("Set user '%s' as administrator successfully." % args.user)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'user',
|
||||
metavar='<user>',
|
||||
help='User name or id')
|
||||
def do_revoke_admin(cs, args):
|
||||
"""Update a registered user to be a non-admin of Harbor."""
|
||||
try:
|
||||
user = cs.users.find(args.user)
|
||||
except exp.NotFound:
|
||||
print("User '%s' not found." % args.user)
|
||||
cs.users.set_admin(user['user_id'], False)
|
||||
print("Revoke admin privilege from user '%s' successfully." % args.user)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'user',
|
||||
metavar='<user>',
|
||||
help='User name or id')
|
||||
@utils.arg(
|
||||
'--email',
|
||||
metavar='<email>',
|
||||
dest='email',
|
||||
help='Email of the user')
|
||||
@utils.arg(
|
||||
'--realname',
|
||||
metavar='<realname>',
|
||||
dest='realname',
|
||||
help='Email of the user')
|
||||
@utils.arg(
|
||||
'--comment',
|
||||
metavar='<comment>',
|
||||
dest='comment',
|
||||
help='Comment of the user')
|
||||
def do_user_update(cs, args):
|
||||
"""Update a registered user to change his profile."""
|
||||
try:
|
||||
user = cs.users.find(args.user)
|
||||
except exp.NotFound:
|
||||
print("User '%s' not found." % args.user)
|
||||
realname = args.realname or user['realname']
|
||||
email = args.email or user['email']
|
||||
comment = args.comment or user['comment']
|
||||
cs.users.update(user['user_id'], realname, email, comment)
|
||||
user = cs.users.get(user['user_id'])
|
||||
utils.print_dict(user)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'user',
|
||||
metavar='<user>',
|
||||
help='User name or id')
|
||||
def do_change_password(cs, args):
|
||||
"""Change the password on a user that already exists."""
|
||||
try:
|
||||
user = cs.users.find(args.user)
|
||||
except exp.NotFound:
|
||||
print("User '%s' not found." % args.user)
|
||||
old_password = getpass.getpass('Old password: ')
|
||||
new_password = getpass.getpass('New Password: ')
|
||||
try:
|
||||
cs.users.change_password(user['user_id'], old_password, new_password)
|
||||
print("Update password successfully.")
|
||||
except exp.Forbidden as e:
|
||||
print(e.message.replace("_", ' '))
|
||||
return 1
|
||||
|
||||
|
||||
@utils.arg('user', metavar='<user>', help='ID or name of user.')
|
||||
def do_user_show(cs, args):
|
||||
"""Get a user's profile."""
|
||||
key = args.user
|
||||
if cs.users.is_id(key):
|
||||
id = key
|
||||
else:
|
||||
id = cs.users.get_id_by_name(key)
|
||||
user = cs.users.get(id)
|
||||
utils.print_dict(user)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--detail',
|
||||
'-d',
|
||||
dest="detail",
|
||||
action="store_true",
|
||||
help='show detail info.')
|
||||
def do_whoami(cs, args):
|
||||
"""Get current user info."""
|
||||
user = cs.users.current()
|
||||
if args.detail:
|
||||
utils.print_dict(user)
|
||||
else:
|
||||
print(user['username'])
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--username',
|
||||
metavar='<username>',
|
||||
dest='username',
|
||||
required=True,
|
||||
help='Unique name of the new user')
|
||||
@utils.arg(
|
||||
'--password',
|
||||
metavar='<password>',
|
||||
dest='password',
|
||||
required=True,
|
||||
help='Password of the new user')
|
||||
@utils.arg(
|
||||
'--email',
|
||||
metavar='<email>',
|
||||
dest='email',
|
||||
required=True,
|
||||
help='Email of the new user')
|
||||
@utils.arg(
|
||||
'--realname',
|
||||
metavar='<realname>',
|
||||
dest='realname',
|
||||
default=None,
|
||||
help='Email of the new user')
|
||||
@utils.arg(
|
||||
'--comment',
|
||||
metavar='<comment>',
|
||||
dest='comment',
|
||||
default=None,
|
||||
help='Comment of the new user')
|
||||
def do_user_create(cs, args):
|
||||
"""Creates a new user account."""
|
||||
cs.users.create(args.username, args.password,
|
||||
args.email, args.realname,
|
||||
args.comment)
|
||||
print("Create user '%s' successfully." % args.username)
|
||||
|
||||
|
||||
@utils.arg('user', metavar='<user>', help='ID or name of user.')
|
||||
def do_user_delete(cs, args):
|
||||
"""Mark a registered user as be removed."""
|
||||
key = args.user
|
||||
if cs.users.is_id(key):
|
||||
id = key
|
||||
else:
|
||||
id = cs.users.get_id_by_name(key)
|
||||
cs.users.delete(id)
|
||||
print("Delete user '%s' sucessfully." % key)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--sortby',
|
||||
metavar='<sortby>',
|
||||
dest="sortby",
|
||||
default="project_id",
|
||||
help='Sort key.')
|
||||
def do_project_list(cs, args):
|
||||
"""List projects."""
|
||||
projects = cs.projects.list()
|
||||
fields = [
|
||||
'project_id',
|
||||
'name',
|
||||
'owner_id',
|
||||
'current_user_role_id',
|
||||
'repo_count',
|
||||
'creation_time',
|
||||
'public',
|
||||
]
|
||||
utils.print_list(projects, fields, formatters={}, sortby=args.sortby)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--project-id',
|
||||
'-p',
|
||||
dest='project_id',
|
||||
metavar='<project_id>',
|
||||
default=None,
|
||||
help='ID of project.')
|
||||
def do_member_list(cs, args):
|
||||
"""List a project's relevant role members."""
|
||||
project = args.project_id
|
||||
if not project:
|
||||
project = cs.client.project
|
||||
members = cs.projects.get_members(project)
|
||||
fields = [
|
||||
'username',
|
||||
'role_name',
|
||||
'user_id',
|
||||
'role_id',
|
||||
]
|
||||
utils.print_list(members, fields, formatters={}, sortby='user_id')
|
||||
|
||||
|
||||
@utils.arg('project', metavar='<project>', help='ID or name of project.')
|
||||
def do_project_show(cs, args):
|
||||
"""Show specific project detail infomation."""
|
||||
key = args.project
|
||||
if cs.projects.is_id(key):
|
||||
project_id = key
|
||||
else:
|
||||
project_id = cs.projects.get_id_by_name(key)
|
||||
projects = cs.projects.list()
|
||||
for project in projects:
|
||||
if str(project['project_id']) == str(project_id):
|
||||
utils.print_dict(project)
|
||||
return
|
||||
raise exp.NotFound("Project '%s' not found" % args.project)
|
||||
|
||||
|
||||
@utils.arg('project', metavar='<project>', help='ID or name of project.')
|
||||
def do_project_delete(cs, args):
|
||||
"""Delete project by Id or name."""
|
||||
key = args.project
|
||||
if cs.projects.is_id(key):
|
||||
id = key
|
||||
else:
|
||||
try:
|
||||
id = cs.projects.get_id_by_name(key)
|
||||
except exp.NotFound:
|
||||
print("Project '%s' not found." % args.project)
|
||||
return 1
|
||||
try:
|
||||
cs.projects.delete(id)
|
||||
print("Delete Project '%s' successfully." % key)
|
||||
return 0
|
||||
except exp.NotFound:
|
||||
print("Project '%s' not Found." % args.project)
|
||||
return 1
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'name',
|
||||
metavar='<name>',
|
||||
help='Name of new project.')
|
||||
@utils.arg(
|
||||
'--is-public',
|
||||
metavar='<is-public>',
|
||||
default=True,
|
||||
help='Make project accessible to the public (default true).')
|
||||
def do_project_create(cs, args):
|
||||
"""Create a new project."""
|
||||
is_public = strutils.bool_from_string(args.is_public, strict=True)
|
||||
try:
|
||||
cs.projects.create(args.name, is_public)
|
||||
print("Create project '%s' successfully." % args.name)
|
||||
except exp.Conflict:
|
||||
print("Project name '%s' already exists." % args.name)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--project-id',
|
||||
'-p',
|
||||
dest='project_id',
|
||||
metavar='<project_id>',
|
||||
default=None,
|
||||
help='ID of project.')
|
||||
@utils.arg(
|
||||
'--sortby',
|
||||
dest='sortby',
|
||||
metavar='<sortby>',
|
||||
default='Id',
|
||||
help='Sort key.')
|
||||
def do_list(cs, args):
|
||||
"""Get repositories accompany with relevant project and repo name."""
|
||||
project_id = args.project_id
|
||||
if not project_id:
|
||||
project_id = cs.client.project
|
||||
repositories = cs.repositories.list(project_id)
|
||||
data = []
|
||||
for repo in repositories:
|
||||
tags = cs.repositories.list_tags(repo['name'])
|
||||
for tag in tags:
|
||||
item = repo.copy()
|
||||
manifest = cs.repositories.get_manifests(item['name'],
|
||||
tag['name'])
|
||||
size = 0
|
||||
for layer in manifest['manifest']['layers']:
|
||||
size += layer['size']
|
||||
item['size'] = size
|
||||
if tag['name'] != 'latest':
|
||||
item['name'] = repo['name'] + ":" + tag['name']
|
||||
data.append(item)
|
||||
fields = [
|
||||
"name", 'project_id', 'size',
|
||||
"tags_count", "star_count", "pull_count",
|
||||
"update_time"
|
||||
]
|
||||
utils.print_list(data, fields, sortby=args.sortby)
|
||||
|
||||
|
||||
@utils.arg('repository', metavar='<repository>', help='Name of repository.')
|
||||
def do_list_tags(cs, args):
|
||||
"""Get tags of a relevant repository."""
|
||||
tags = cs.repositories.list_tags(args.repository)
|
||||
fields = ["name", 'author', 'architecture',
|
||||
'os', 'docker_version', 'created']
|
||||
utils.print_list(tags, fields, sortby="name")
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--project-id',
|
||||
'-p',
|
||||
dest='project_id',
|
||||
metavar='<project_id>',
|
||||
default=None,
|
||||
help='ID of project.')
|
||||
@utils.arg(
|
||||
'repository',
|
||||
metavar='<repository>',
|
||||
help="Repository name, for example: int32bit/ubuntu:14.04.")
|
||||
def do_show(cs, args):
|
||||
"""Show specific repository detail infomation."""
|
||||
project = args.project_id
|
||||
if not project:
|
||||
project = cs.client.project
|
||||
repo = args.repository
|
||||
tag_index = repo.find(':')
|
||||
if tag_index != -1:
|
||||
tag = repo[tag_index + 1:]
|
||||
repo = repo[:tag_index]
|
||||
else:
|
||||
tag = "latest"
|
||||
if repo.find('/') == -1:
|
||||
repo = "library/" + repo
|
||||
repos = cs.repositories.list(project)
|
||||
found_repo = None
|
||||
for r in repos:
|
||||
if r['name'] == repo:
|
||||
found_repo = r
|
||||
break
|
||||
if not found_repo:
|
||||
print("Image '%s' not found." % repo)
|
||||
return
|
||||
tags = cs.repositories.list_tags(found_repo['name'])
|
||||
found_tag = None
|
||||
for t in tags:
|
||||
if t['name'] == tag:
|
||||
found_tag = t
|
||||
break
|
||||
if not found_tag:
|
||||
print("Image '%s' with tag '%s' not found." % (repo, tag))
|
||||
return
|
||||
for key in found_tag:
|
||||
found_repo['tag_' + key] = found_tag[key]
|
||||
utils.print_dict(found_repo)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--count',
|
||||
'-c',
|
||||
metavar='<count>',
|
||||
dest='count',
|
||||
default=5,
|
||||
help='Count.')
|
||||
def do_top(cs, args):
|
||||
"""Get public repositories which are accessed most."""
|
||||
try:
|
||||
count = int(args.count)
|
||||
except ValueError:
|
||||
print("'%s' is not a valid number." % args.count)
|
||||
return 1
|
||||
if count < 1:
|
||||
print("invalid count %s, count must > 0." % args.count)
|
||||
return 1
|
||||
data = cs.repositories.get_top(count)
|
||||
utils.print_list(data,
|
||||
['name', 'pull_count', 'star_count'],
|
||||
sortby='pull_count')
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'query',
|
||||
metavar='<query>',
|
||||
help='Search parameter for project and repository name.')
|
||||
def do_search(cs, args):
|
||||
"""Search for projects and repositories."""
|
||||
data = cs.searcher.search(args.query)
|
||||
project_fields = ['project_id', 'name', 'public',
|
||||
'repo_count', 'creation_time']
|
||||
print("Find %d Projects: " % len(data['project']))
|
||||
utils.print_list(
|
||||
data['project'], project_fields, formatters={}, sortby='id')
|
||||
repository_fields = [
|
||||
'repository_name', 'project_name', 'project_id', 'project_public'
|
||||
]
|
||||
print("\n")
|
||||
print("Find %d Repositories: " % len(data['repository']))
|
||||
utils.print_list(
|
||||
data['repository'],
|
||||
repository_fields,
|
||||
formatters={},
|
||||
sortby='repository_name')
|
||||
|
||||
|
||||
def do_usage(cs, args):
|
||||
"""Get projects number and repositories number relevant to the user."""
|
||||
data = cs.statistics.list()
|
||||
utils.print_dict(data)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'--sortby',
|
||||
dest='sortby',
|
||||
metavar='<sortby>',
|
||||
default='op_time',
|
||||
help='Sort key.')
|
||||
def do_logs(cs, args):
|
||||
"""Get recent logs of the projects which the user is a member of."""
|
||||
logs = cs.logs.list() or []
|
||||
for log in logs:
|
||||
repo = log['repo_name']
|
||||
tag = None
|
||||
if log['repo_tag'] != 'N/A':
|
||||
tag = log['repo_tag']
|
||||
if tag:
|
||||
repo += ":%s" % tag
|
||||
log['repository'] = repo
|
||||
fields = ['log_id', 'op_time', 'username',
|
||||
'project_id', 'operation', 'repository']
|
||||
utils.print_list(logs, fields, sortby=args.sortby)
|
||||
|
||||
|
||||
def do_info(cs, args):
|
||||
"""Get general system info."""
|
||||
info = cs.systeminfo.get()
|
||||
try:
|
||||
volumes = cs.systeminfo.get_volumes()
|
||||
info['disk_total'] = volumes['storage']['total']
|
||||
info['disk_free'] = volumes['storage']['free']
|
||||
except exp.Forbidden:
|
||||
# Only admin can get volumes
|
||||
pass
|
||||
utils.print_dict(info)
|
||||
|
||||
|
||||
def do_get_cert(cs, args):
|
||||
"""Get default root cert under OVA deployment."""
|
||||
try:
|
||||
certs = cs.systeminfo.get_cert()
|
||||
print(certs)
|
||||
except exp.NotFound:
|
||||
print("No certificate found")
|
||||
except exp.Forbidden:
|
||||
print("Only admin can perform this operation.")
|
||||
|
||||
|
||||
def do_version(cs, args):
|
||||
"""Get harbor version."""
|
||||
info = cs.systeminfo.get()
|
||||
print(info['harbor_version'])
|
||||
|
||||
|
||||
def do_get_conf(cs, args):
|
||||
"""Get system configurations."""
|
||||
try:
|
||||
configurations = cs.configurations.get()
|
||||
except exp.Forbidden:
|
||||
raise exp.CommandError("Only admin can perform this operation.")
|
||||
data = []
|
||||
for key in configurations:
|
||||
item = {}
|
||||
item['name'] = key
|
||||
item['value'] = configurations[key]['value']
|
||||
item['editable'] = configurations[key]['editable']
|
||||
data.append(item)
|
||||
utils.print_list(data, ['name', 'value', 'editable'], sortby='name')
|
||||
|
||||
|
||||
def do_target_list(cs, args):
|
||||
"""List filters targets."""
|
||||
targets = cs.targets.list()
|
||||
fields = ['id', 'name', 'endpoint',
|
||||
'username', 'password', 'creation_time']
|
||||
utils.print_list(targets, fields)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'target',
|
||||
metavar='<target>',
|
||||
help="The target name or id.")
|
||||
def do_target_ping(cs, args):
|
||||
"""Ping validates target."""
|
||||
target = None
|
||||
if is_id(args.target):
|
||||
target = args.target
|
||||
else:
|
||||
targets = cs.targets.list()
|
||||
for t in targets:
|
||||
if t['name'] == args.target:
|
||||
target = t['id']
|
||||
break
|
||||
if not target:
|
||||
print("target '%s' not found!" % args.target)
|
||||
return 1
|
||||
try:
|
||||
cs.targets.ping(target)
|
||||
print("OK")
|
||||
except Exception as e:
|
||||
print("Can not ping target: %s" % e)
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'target',
|
||||
metavar='<target>',
|
||||
help="The target name or id.")
|
||||
def do_policy_list(cs, args):
|
||||
"""List filters policies by name and project_id."""
|
||||
target = None
|
||||
if is_id(args.target):
|
||||
target = args.target
|
||||
else:
|
||||
targets = cs.targets.list()
|
||||
for t in targets:
|
||||
if t['name'] == args.target:
|
||||
target = t['id']
|
||||
break
|
||||
if not target:
|
||||
print("target '%s' not found!" % args.target)
|
||||
return 1
|
||||
try:
|
||||
policies = cs.targets.list_policies(target)
|
||||
except exp.NotFound:
|
||||
print("target '%s' not found!" % args.target)
|
||||
return 1
|
||||
if not policies:
|
||||
policies = []
|
||||
fields = ["id", "name", "description",
|
||||
"enabled", "start_time", "cron_str",
|
||||
"creation_time"]
|
||||
utils.print_list(policies, fields, sortby='id')
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'policy_id',
|
||||
metavar='<policy_id>',
|
||||
help="The policy id.")
|
||||
def do_job_list(cs, args):
|
||||
"""List filters jobs according to the policy and repository."""
|
||||
jobs = cs.jobs.list(args.policy_id)
|
||||
for job in jobs:
|
||||
if job['tags']:
|
||||
job['name'] += ":" + job['tags']
|
||||
fields = ['id', 'repository', 'operation', 'status', 'update_time']
|
||||
utils.print_list(jobs, fields, sortby='id')
|
||||
|
||||
|
||||
@utils.arg(
|
||||
'job_id',
|
||||
metavar='<job_id>',
|
||||
help="The job id.")
|
||||
def do_job_log(cs, args):
|
||||
"""Get job logs."""
|
||||
log = cs.jobs.get_log(args.job_id)
|
||||
print(log)
|
7
contrib/harbor-cli/harborclient/v2/statistics.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class StatisticsManager(base.Manager):
|
||||
def list(self):
|
||||
"""Get projects number and repositories number relevant to the user."""
|
||||
return self._list("/statistics")
|
15
contrib/harbor-cli/harborclient/v2/systeminfo.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class SystemInfoManager(base.Manager):
|
||||
def get(self):
|
||||
"""Get general system info."""
|
||||
return self._get("/systeminfo")
|
||||
|
||||
def get_volumes(self):
|
||||
"""Get system volume info (total/free size)."""
|
||||
return self._get("/systeminfo/volumes")
|
||||
|
||||
def get_cert(self):
|
||||
"""Get default root certificate under OVA deployment."""
|
||||
return self._get("/systeminfo/getcert")
|
18
contrib/harbor-cli/harborclient/v2/targets.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from harborclient import base
|
||||
|
||||
|
||||
class TargetManager(base.Manager):
|
||||
|
||||
def list(self, name=None):
|
||||
"""List filters targets by name."""
|
||||
if name:
|
||||
return self._list("/targets?name=%s" % name)
|
||||
return self._list("/targets")
|
||||
|
||||
def ping(self, id):
|
||||
"""Ping validates target."""
|
||||
return self._create("/targets/%s/ping" % id)
|
||||
|
||||
def list_policies(self, id):
|
||||
"""List the target relevant policies."""
|
||||
return self._list("/targets/%s/policies" % id)
|
72
contrib/harbor-cli/harborclient/v2/users.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from harborclient import base
|
||||
from harborclient import exceptions as exp
|
||||
|
||||
|
||||
class UserManager(base.Manager):
|
||||
def is_id(self, key):
|
||||
return key.isdigit()
|
||||
|
||||
def get(self, id):
|
||||
"""Get a user's profile."""
|
||||
return self._get("/users/%s" % id)
|
||||
|
||||
def current(self):
|
||||
"""Get current user info."""
|
||||
return self._get("/users/current")
|
||||
|
||||
def list(self):
|
||||
"""Get registered users of Harbor."""
|
||||
return self._list("/users")
|
||||
|
||||
def get_id_by_name(self, name):
|
||||
users = self.list()
|
||||
for u in users:
|
||||
if u['username'] == name:
|
||||
return u['user_id']
|
||||
raise exp.NotFound("User '%s' Not Found!" % name)
|
||||
|
||||
def find(self, key):
|
||||
if self.is_id(key):
|
||||
return self.get(key)
|
||||
else:
|
||||
users = self.list()
|
||||
for user in users:
|
||||
if user['username'] == key:
|
||||
return user
|
||||
raise exp.NotFound("User '%s' Not Found!" % key)
|
||||
|
||||
def create(self, username, password, email, realname=None, comment=None):
|
||||
"""Creates a new user account."""
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email,
|
||||
"realname": realname or username,
|
||||
"comment": comment or "",
|
||||
}
|
||||
return self._create("/users", data)
|
||||
|
||||
def update(self, id, realname, email, comment):
|
||||
"""Update a registered user to change his profile."""
|
||||
profile = {"realname": realname,
|
||||
"email": email,
|
||||
"comment": comment}
|
||||
return self._update("/users/%s" % id, profile)
|
||||
|
||||
def delete(self, id):
|
||||
"""Mark a registered user as be removed."""
|
||||
return self._delete("/users/%s" % id)
|
||||
|
||||
def change_password(self, id, old_password, new_password):
|
||||
"""Change the password on a user that already exists."""
|
||||
profile = {"old_password": old_password,
|
||||
"new_password": new_password}
|
||||
return self._update("/users/%s/password" % id, profile)
|
||||
|
||||
def set_admin(self, id, is_admin):
|
||||
"""Update a registered user to change to be an admin of Harbor."""
|
||||
if is_admin:
|
||||
profile = {"has_admin_role": 1}
|
||||
else:
|
||||
profile = {"has_admin_role": 0}
|
||||
return self._update("/users/%s/sysadmin" % id, profile)
|
5
contrib/harbor-cli/harborrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
export HARBOR_USERNAME=admin
|
||||
export HARBOR_PASSWORD=Harbor12345
|
||||
export HARBOR_URL=https://localhost
|
||||
export HARBOR_PROJECT=2
|
||||
complete -W "$(harbor bash-completion)" harbor
|
10
contrib/harbor-cli/requirements.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr>=1.6 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
oslo.utils>=3.11.0 # Apache-2.0
|
||||
PrettyTable<0.8,>=0.7 # BSD
|
||||
requests>=2.10.0 # Apache-2.0
|
||||
simplejson>=2.2.0 # MIT
|
||||
six>=1.9.0 # MIT
|
1
contrib/harbor-cli/set_bash_completion.sh
Executable file
|
@ -0,0 +1 @@
|
|||
complete -W $(harbor bash-completion) harbor
|
35
contrib/harbor-cli/setup.cfg
Normal file
|
@ -0,0 +1,35 @@
|
|||
[metadata]
|
||||
name = python-harborclient
|
||||
summary = A CLI tool for the Docker Registry Harbor
|
||||
description = A CLI tool for the Docker Registry Harbor
|
||||
license = Apache License, Version 2.0
|
||||
author = int32bit
|
||||
author-email = krystism@gmail.com
|
||||
maintainer = int32bit
|
||||
maintainer-email = krystism@gmail.com
|
||||
home-page = https://github.com/int32bit/python-harborclient
|
||||
url = 'https://github.com/int32bit/python-harborclient'
|
||||
version = 1.2.2
|
||||
keywords = 'docker registry distribution harbor python sdk'
|
||||
install_requires = ['requests>2.2.0', 'oslo.serialization>=1.10.0', 'oslo.utils>=3.11.0', 'PrettyTable', 'simplejson', 'six'],
|
||||
classifier =
|
||||
Development Status :: 3 - Alpha
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
[files]
|
||||
packages =
|
||||
harborclient
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
harbor = harborclient.shell:main
|
||||
[wheel]
|
||||
universal = 1
|
||||
[pbr]
|
||||
warnerrors = true
|
8
contrib/harbor-cli/setup.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import setuptools
|
||||
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(setup_requires=['pbr>=1.8'], pbr=True)
|
3
contrib/harbor-cli/test-requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
16
contrib/harbor-cli/tools/pretty_tox.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -o pipefail
|
||||
|
||||
TESTRARGS=$1
|
||||
|
||||
# --until-failure is not compatible with --subunit see:
|
||||
#
|
||||
# https://bugs.launchpad.net/testrepository/+bug/1411804
|
||||
#
|
||||
# this work around exists until that is addressed
|
||||
if [[ "$TESTARGS" =~ "until-failure" ]]; then
|
||||
python setup.py testr --slowest --testr-args="$TESTRARGS"
|
||||
else
|
||||
python setup.py testr --slowest --testr-args="--subunit $TESTRARGS" | subunit-trace -f
|
||||
fi
|
23
contrib/harbor-cli/tox.ini
Normal file
|
@ -0,0 +1,23 @@
|
|||
[tox]
|
||||
envlist = pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
# tox is silly... these need to be separated by a newline....
|
||||
whitelist_externals = find
|
||||
bash
|
||||
install_command = pip install -U {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
find . -type f -name "*.pyc" -delete
|
||||
bash tools/pretty_tox.sh '{posargs}'
|
||||
# there is also secret magic in pretty_tox.sh which lets you run in a fail only
|
||||
# mode. To do this define the TRACE_FAILONLY environmental variable.
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8 {posargs}
|
|
@ -57,7 +57,7 @@ ui:
|
|||
```
|
||||
* Recreate Harbor containers
|
||||
```docker
|
||||
docker-compose down -v & docker-compose up -d
|
||||
docker-compose down -v && docker-compose up -d
|
||||
```
|
||||
|
||||
* Because a session ID is usually required by Harbor API, **you should log in first from a browser.**
|
||||
|
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 94 KiB |
BIN
docs/img/log_search_advanced.png
Normal file
After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 66 KiB |
BIN
docs/img/project_configuration.png
Normal file
After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 66 KiB |
|
@ -67,9 +67,11 @@ The parameters are described below - note that at the very least, you will need
|
|||
* **ssl_cert**: The path of SSL certificate, it's applied only when the protocol is set to https
|
||||
* **ssl_cert_key**: The path of SSL key, it's applied only when the protocol is set to https
|
||||
* **secretkey_path**: The path of key for encrypt or decrypt the password of a remote registry in a replication policy.
|
||||
* **log_rotate_count**: Log files are rotated **log_rotate_count** times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
* **log_rotate_size**: Log files are rotated only if they grow bigger than **log_rotate_size** bytes. If size is followed by k, the size is assumed to be in kilobytes. If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G are all valid.
|
||||
|
||||
##### Optional parameters
|
||||
* **Email settings**: These parameters are needed for Harbor to be able to send a user a "password reset" email, and are only necessary if that functionality is needed. Also, do note that by default SSL connectivity is _not_ enabled - if your SMTP server requires SSL, but does _not_ support STARTTLS, then you should enable SSL by setting **email_ssl = true**. For a detailed description about "email_identity" please refer to [rfc2595](https://tools.ietf.org/rfc/rfc2595.txt)
|
||||
* **Email settings**: These parameters are needed for Harbor to be able to send a user a "password reset" email, and are only necessary if that functionality is needed. Also, do note that by default SSL connectivity is _not_ enabled - if your SMTP server requires SSL, but does _not_ support STARTTLS, then you should enable SSL by setting **email_ssl = true**. Setting **email_insecure = true** if the email server uses a self-signed or untrusted certificate. For a detailed description about "email_identity" please refer to [rfc2595](https://tools.ietf.org/rfc/rfc2595.txt)
|
||||
* email_server = smtp.mydomain.com
|
||||
* email_server_port = 25
|
||||
* email_identity =
|
||||
|
@ -77,6 +79,7 @@ The parameters are described below - note that at the very least, you will need
|
|||
* email_password = abc
|
||||
* email_from = admin <sample_admin@mydomain.com>
|
||||
* email_ssl = false
|
||||
* email_insecure = false
|
||||
|
||||
* **harbor_admin_password**: The administrator's initial password. This password only takes effect for the first time Harbor launches. After that, this setting is ignored and the administrator's password should be set in the UI. _Note that the default username/password are **admin/Harbor12345** ._
|
||||
* **auth_mode**: The type of authentication that is used. By default, it is **db_auth**, i.e. the credentials are stored in a database.
|
||||
|
@ -94,12 +97,11 @@ may not be able to log in after the upgrade.
|
|||
* **self_registration**: (**on** or **off**. Default is **on**) Enable / Disable the ability for a user to register himself/herself. When disabled, new users can only be created by the Admin user, only an admin user can create new users in Harbor. _NOTE: When **auth_mode** is set to **ldap_auth**, self-registration feature is **always** disabled, and this flag is ignored._
|
||||
* **token_expiration**: The expiration time (in minutes) of a token created by token service, default is 30 minutes.
|
||||
* **project_creation_restriction**: The flag to control what users have permission to create projects. By default everyone can create a project, set to "adminonly" such that only admin can create project.
|
||||
* **verify_remote_cert**: (**on** or **off**. Default is **on**) This flag determines whether or not to verify SSL/TLS certificate when Harbor communicates with a remote registry instance. Setting this attribute to **off** bypasses the SSL/TLS verification, which is often used when the remote instance has a self-signed or untrusted certificate.
|
||||
|
||||
#### Configuring storage backend (optional)
|
||||
|
||||
By default, Harbor stores images on your local filesystem. In a production environment, you may consider
|
||||
using other storage backend instead of the local filesystem, like S3, Openstack Swift, Ceph, etc.
|
||||
using other storage backend instead of the local filesystem, like S3, OpenStack Swift, Ceph, etc.
|
||||
What you need to update is the section of `storage` in the file `common/templates/registry/config.yml`.
|
||||
For example, if you use Openstack Swift as your storage backend, the section may look like this:
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube
|
|||
```
|
||||
- `make/kubernetes/**/*.svc.yaml`: Specify the service of pods. In particular, the externalIP should be set in `make/kubernetes/nginx/nginx.svc.yaml`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
...
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -54,7 +54,7 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube
|
|||
- `make/kubernetes/pv/*.pvc.yaml`: Persistent Volume Claim.
|
||||
You can set capacity of storage in these files. example:
|
||||
|
||||
```
|
||||
```yaml
|
||||
resources:
|
||||
requests:
|
||||
# you can set another value to adapt to your needs
|
||||
|
@ -65,7 +65,7 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube
|
|||
PVs and PVCs are one to one correspondence. If you changed capacity of PVC, you need to set capacity of PV together.
|
||||
example:
|
||||
|
||||
```
|
||||
```yaml
|
||||
capacity:
|
||||
# same value with PVC
|
||||
storage: 100Gi
|
||||
|
@ -73,7 +73,7 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube
|
|||
|
||||
In PV, you should set another way to store data rather than `hostPath`:
|
||||
|
||||
```
|
||||
```yaml
|
||||
# it's default value, you should use others like nfs.
|
||||
hostPath:
|
||||
path: /data/registry
|
||||
|
@ -83,7 +83,7 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube
|
|||
|
||||
Then you can generate ConfigMap files by :
|
||||
|
||||
```
|
||||
```shell
|
||||
python make/kubernetes/k8s-prepare
|
||||
```
|
||||
|
||||
|
@ -112,7 +112,7 @@ You can find all configs of Harbor in `make/kubernetes/templates/`. There are sp
|
|||
- `registry.cm.yaml`: Token service certification and registry config
|
||||
Registry use filesystem to store data of images. You can find it like:
|
||||
|
||||
```
|
||||
```yaml
|
||||
storage:
|
||||
filesystem:
|
||||
rootdirectory: /storage
|
||||
|
@ -128,7 +128,7 @@ You can find all configs of Harbor in `make/kubernetes/templates/`. There are sp
|
|||
### Running
|
||||
When you finished your configuring and generated ConfigMap files, you can run Harbor on kubernetes with these commands:
|
||||
|
||||
```
|
||||
```shell
|
||||
# create pv & pvc
|
||||
kubectl apply -f make/kubernetes/pv/log.pv.yaml
|
||||
kubectl apply -f make/kubernetes/pv/registry.pv.yaml
|
||||
|
@ -166,5 +166,5 @@ After the pods are running, you can access Harbor's UI via the configured endpoi
|
|||
|
||||
#### Limitation
|
||||
1. Current deployment is http only, to enable https you need to either add another layer of proxy or modify the nginx.cm.yaml to enable https and include a correct certificate
|
||||
2. Current deployment does not include Clair and Notary, which are supported in docker-compose deployment. They will be supported in near futuer, stay tuned.
|
||||
2. Current deployment does not include Clair and Notary, which are supported in docker-compose deployment. They will be supported in near future, stay tuned.
|
||||
|
||||
|
|
|
@ -961,6 +961,33 @@ paths:
|
|||
description: Forbidden.
|
||||
'404':
|
||||
description: Repository not found.
|
||||
put:
|
||||
summary: Update description of the repository.
|
||||
description: |
|
||||
This endpoint is used to update description of the repository.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository which will be deleted.
|
||||
- name: description
|
||||
in: body
|
||||
description: The description of the repository.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RepositoryDescription'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Update successfully.
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden.
|
||||
'404':
|
||||
description: Repository not found.
|
||||
'/repositories/{repo_name}/tags/{tag}':
|
||||
get:
|
||||
summary: Get the tag of the repository.
|
||||
|
@ -1158,7 +1185,7 @@ paths:
|
|||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#definitions/VulnerabilityItem'
|
||||
$ref: '#/definitions/VulnerabilityItem'
|
||||
'401':
|
||||
description: User needs to login or call the API with correct credentials.
|
||||
'403':
|
||||
|
@ -1662,31 +1689,6 @@ paths:
|
|||
description: Target not found.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/targets/{id}/ping':
|
||||
post:
|
||||
summary: Ping target.
|
||||
description: |
|
||||
This endpoint is for ping target.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The replication's target ID.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Ping replication's target successfully.
|
||||
'400':
|
||||
description: Can not ping target.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'404':
|
||||
description: Target ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/targets/{id}':
|
||||
put:
|
||||
summary: Update replication's target.
|
||||
|
@ -2480,6 +2482,10 @@ definitions:
|
|||
PingTarget:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int
|
||||
description: Target ID.
|
||||
endpoint:
|
||||
type: string
|
||||
description: The target address URL string.
|
||||
|
@ -2915,6 +2921,11 @@ definitions:
|
|||
policy_id:
|
||||
type: integer
|
||||
description: The ID of replication policy
|
||||
|
||||
RepositoryDescription:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
description: The description of the repository.
|
||||
|
||||
|
||||
|
|
|
@ -2,20 +2,23 @@
|
|||
## Overview
|
||||
This guide walks you through the fundamentals of using Harbor. You'll learn how to use Harbor to:
|
||||
|
||||
* Manage your projects.
|
||||
* Manage members of a project.
|
||||
* Replicate projects to a remote registry.
|
||||
* Search projects and repositories.
|
||||
* Manage Harbor system if you are the system administrator:
|
||||
* Manage users.
|
||||
* Manage destinations.
|
||||
* Manage replication policies.
|
||||
* Manage configuration.
|
||||
* Pull and push images using Docker client.
|
||||
* Delete repositories and images.
|
||||
* Content trust.
|
||||
* Vulnerability scanning via Clair.
|
||||
* Pull image from Harbor in Kubernetes.
|
||||
* [Manage your projects.](#managing-projects)
|
||||
* [Manage members of a project.](#managing-members-of-a-project)
|
||||
* [Replicate projects to a remote registry.](#replicationg-images)
|
||||
* [Search projects and repositories.](#searching-projects-and-repositories)
|
||||
* [Manage Harbor system if you are the system administrator:](#administrator-options)
|
||||
* [Manage users.](#managing-user)
|
||||
* [Manage destinations.](#managing-endpoint)
|
||||
* [Manage replication policies.](#managing-replication)
|
||||
* [Manage authentication.](#managing-authentication)
|
||||
* [Manage project creation.](#managing-project-creation)
|
||||
* [Manage self-registration.](#managing-self-registration)
|
||||
* [Manage email settings.](#managing-email-settings)
|
||||
* [Pull and push images using Docker client.](#pulling-and-pushing-images-using-docker-client)
|
||||
* [Delete repositories and images.](#deleting-repositories)
|
||||
* [Content trust. ](#content-trust)
|
||||
* [Vulnerability scanning via Clair.](#vulnerability-scaning-via-clair)
|
||||
* [Pull image from Harbor in Kubernetes.](#pull-image-from-harbor-in-kubernetes)
|
||||
|
||||
## Role Based Access Control(RBAC)
|
||||
|
||||
|
@ -70,14 +73,28 @@ You can create a project after you signed in. Check on the "Access Level" checkb
|
|||
|
||||
![create project](img/new_create_project.png)
|
||||
|
||||
After the project is created, you can browse repositories, users and logs using the navigation tab.
|
||||
After the project is created, you can browse repositories, members, logs, replication and configuration using the navigation tab.
|
||||
|
||||
![browse project](img/new_browse_project.png)
|
||||
|
||||
All logs can be listed by clicking "Logs". You can apply a filter by username, or operations and dates under "Advanced Search".
|
||||
|
||||
![browse project](img/log_search_advanced.png)
|
||||
|
||||
![browse project](img/new_project_log.png)
|
||||
|
||||
Project properties can be changed by clicking "Configuration".
|
||||
|
||||
* To make all repositories under the project accessible to everyone, select the `Public` checkbox.
|
||||
|
||||
* To prevent un-signed images under the project from being pulled, select the `Enable content trust` checkbox.
|
||||
|
||||
* To prevent vulnerable images under the project from being pulled, select the `Prevent vulnerable images from running` checkbox and change the severity level of vulnerabilities. Images cannot be pulled if their level equals to or higher than the currently selected level.
|
||||
|
||||
* To activate an immediate vulnerability scan on new images that are pushed to the project, select the `Automatically scan images on push` checkbox.
|
||||
|
||||
![browse project](img/project_configuration.png)
|
||||
|
||||
## Managing members of a project
|
||||
### Adding members
|
||||
You can add members with different roles to an existing project.
|
||||
|
@ -98,7 +115,7 @@ There may be a bit of delay during replication according to the situation of the
|
|||
|
||||
**Note:** The replication feature is incompatible between Harbor instance before version 0.3.5(included) and after version 0.3.5.
|
||||
|
||||
Start replication by creating a rule. Click "Add Replication Rule" on the "Replication" tab, fill in the necessary fields, if there is no endpoint in the list, you need to create one, and then click "OK", a rule for this project will be created. If "Enable" is chosen, the project will be replicated to the remote immediately.
|
||||
Replication can be configured by creating a rule. Click "Add Replication Rule" on the "Replication" tab and fill in the necessary fields. If there is no endpoint available in the list, you need to create one. Uncheck "Verify Remote Cert" if the remote registry uses a self-signed or an untrusted certificate. Click "OK" to create a replication rule for this project. If "Enable" is chosen, the project will be replicated to the remote registry immediately.
|
||||
|
||||
![browse project](img/new_create_rule.png)
|
||||
|
||||
|
@ -145,10 +162,6 @@ Use the **Project Creation** drop-down menu to set which users can create projec
|
|||
You can manage whether a user can sign up for a new account. This option is not available if you use LDAP authentication.
|
||||
![browse project](img/new_self_reg.png)
|
||||
|
||||
### Managing verification of remote certificate
|
||||
You can choose whether to verify remote endpoint's certification. You may need to disable certificate verification if the remote registry uses a self-signed or an untrusted certificate.
|
||||
![browse project](img/new_remote_cert.png)
|
||||
|
||||
### Managing email settings
|
||||
You can change Harbor's email settings, the mail server is used to send out responses to users who request to reset their password.
|
||||
![browse project](img/new_config_email.png)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
FROM vmware/mariadb-photon:10.2.8
|
||||
FROM vmware/mariadb-photon:10.2.10
|
||||
|
||||
HEALTHCHECK CMD mysqladmin -uroot -p$MYSQL_ROOT_PASSWORD ping
|
||||
|
||||
COPY registry.sql /docker-entrypoint-initdb.d/
|
||||
COPY registry-flag.sh /docker-entrypoint-initdb.d/
|
||||
|
|
|
@ -97,18 +97,20 @@ insert into project_member (project_id, user_id, role, creation_time, update_tim
|
|||
(1, 1, 1, NOW(), NOW());
|
||||
|
||||
create table project_metadata (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
project_id int NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
value varchar(255),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
||||
PRIMARY KEY (project_id, name),
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT unique_project_id_and_name UNIQUE (project_id,name),
|
||||
FOREIGN KEY (project_id) REFERENCES project(project_id)
|
||||
);
|
||||
|
||||
insert into project_metadata (project_id, name, value, creation_time, update_time, deleted) values
|
||||
(1, 'public', 'true', NOW(), NOW(), 0);
|
||||
insert into project_metadata (id, project_id, name, value, creation_time, update_time, deleted) values
|
||||
(1, 1, 'public', 'true', NOW(), NOW(), 0);
|
||||
|
||||
create table access_log (
|
||||
log_id int NOT NULL AUTO_INCREMENT,
|
||||
|
@ -232,13 +234,15 @@ UNIQUE(namespace)
|
|||
);
|
||||
|
||||
create table properties (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
k varchar(64) NOT NULL,
|
||||
v varchar(128) NOT NULL,
|
||||
primary key (k)
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE (k)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `alembic_version` (
|
||||
`version_num` varchar(32) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
insert into alembic_version values ('1.2.0');
|
||||
insert into alembic_version values ('1.3.0');
|
||||
|
|
|
@ -94,18 +94,19 @@ insert into project_member (project_id, user_id, role, creation_time, update_tim
|
|||
(1, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
create table project_metadata (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_id int NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
value varchar(255),
|
||||
creation_time timestamp,
|
||||
update_time timestamp,
|
||||
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
||||
PRIMARY KEY (project_id, name),
|
||||
UNIQUE(project_id, name) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY (project_id) REFERENCES project(project_id)
|
||||
);
|
||||
|
||||
insert into project_metadata (project_id, name, value, creation_time, update_time, deleted) values
|
||||
(1, 'public', 'true', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0);
|
||||
insert into project_metadata (id, project_id, name, value, creation_time, update_time, deleted) values
|
||||
(1, 1, 'public', 'true', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0);
|
||||
|
||||
create table access_log (
|
||||
log_id INTEGER PRIMARY KEY,
|
||||
|
@ -223,13 +224,14 @@ UNIQUE(namespace)
|
|||
);
|
||||
|
||||
create table properties (
|
||||
id INTEGER PRIMARY KEY,
|
||||
k varchar(64) NOT NULL,
|
||||
v varchar(128) NOT NULL,
|
||||
primary key (k)
|
||||
UNIQUE(k)
|
||||
);
|
||||
|
||||
create table alembic_version (
|
||||
version_num varchar(32) NOT NULL
|
||||
);
|
||||
|
||||
insert into alembic_version values ('0.3.0');
|
||||
insert into alembic_version values ('1.3.0');
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
echo "Log rotate starting..."
|
||||
|
||||
#The logs n days before will be compressed.
|
||||
n=$LOG_ROTATE_DAYS
|
||||
if [ -z "$n" ]
|
||||
then
|
||||
n=3
|
||||
fi
|
||||
|
||||
echo "logs rotate days: $n"
|
||||
|
||||
path=/var/log/docker
|
||||
|
||||
list=""
|
||||
n_days_before=$(($(date +%s) - 3600*24*$n))
|
||||
for dir in $(ls $path | grep -v "tar.gz");
|
||||
do
|
||||
if [ $(date --date=$dir +%s) -lt $n_days_before ]
|
||||
then
|
||||
echo "$dir will be compressed"
|
||||
list="$list $dir"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$list" ]
|
||||
then
|
||||
cd $path
|
||||
tar --remove-files -zcvf $(date -d @$n_days_before +%F)-.tar.gz $list
|
||||
fi
|
||||
|
||||
echo "Log rotate finished."
|
|
@ -2,9 +2,9 @@ FROM vmware/photon:1.0
|
|||
|
||||
#The Docker Daemon has to be running with storage backend btrfs when building the image
|
||||
|
||||
RUN tdnf distro-sync -y || echo \
|
||||
RUN tdnf distro-sync -y \
|
||||
&& tdnf install -y sed shadow procps-ng gawk gzip sudo net-tools \
|
||||
&& groupadd -r -g 999 mysql && useradd --no-log-init -r -g 999 -u 999 mysql \
|
||||
&& groupadd -r -g 10000 mysql && useradd --no-log-init -r -g 10000 -u 10000 mysql \
|
||||
&& tdnf install -y mariadb-server mariadb \
|
||||
&& mkdir /docker-entrypoint-initdb.d /docker-entrypoint-updatedb.d \
|
||||
&& rm -fr /var/lib/mysql \
|
||||
|
@ -18,7 +18,7 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|||
COPY my.cnf /etc/
|
||||
RUN ln -s usr/local/bin/docker-entrypoint.sh /
|
||||
|
||||
VOLUME /var/lib/mysql
|
||||
VOLUME /var/lib/mysql /docker-entrypoint-initdb.d /docker-entrypoint-updatedb.d /tmp /var/run/mysqld
|
||||
EXPOSE 3306
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
FROM vmware/photon:1.0
|
||||
|
||||
RUN tdnf distro-sync -y || echo \
|
||||
RUN tdnf distro-sync -y \
|
||||
&& tdnf install -y nginx \
|
||||
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/error.log \
|
||||
&& mkdir -p /var/run \
|
||||
&& tdnf clean all
|
||||
|
||||
EXPOSE 80
|
||||
VOLUME /var/cache/nginx /var/log/nginx /run
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
|
@ -3,7 +3,7 @@ FROM vmware/photon:1.0
|
|||
ENV PGDATA /var/lib/postgresql/data
|
||||
|
||||
RUN touch /etc/localtime.bak \
|
||||
&& tdnf distro-sync -y || echo \
|
||||
&& tdnf distro-sync -y \
|
||||
&& tdnf install -y sed shadow gzip postgresql\
|
||||
&& groupadd -r postgres --gid=999 \
|
||||
&& useradd -r -g postgres --uid=999 postgres \
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
FROM vmware/photon:1.0
|
||||
|
||||
#base image for rsyslog base on photon
|
||||
|
||||
RUN tdnf distro-sync -y || echo \
|
||||
&& tdnf install -y cronie rsyslog shadow tar gzip \
|
||||
&& mkdir /etc/rsyslog.d/ \
|
||||
&& mkdir /var/spool/rsyslog \
|
||||
&& groupadd syslog \
|
||||
&& useradd -g syslog syslog \
|
||||
&& tdnf clean all
|
|
@ -1,3 +1,4 @@
|
|||
PORT=8080
|
||||
LOG_LEVEL=debug
|
||||
EXT_ENDPOINT=$ui_url
|
||||
AUTH_MODE=$auth_mode
|
||||
|
@ -10,6 +11,7 @@ LDAP_FILTER=$ldap_filter
|
|||
LDAP_UID=$ldap_uid
|
||||
LDAP_SCOPE=$ldap_scope
|
||||
LDAP_TIMEOUT=$ldap_timeout
|
||||
LDAP_VERIFY_CERT=true
|
||||
DATABASE_TYPE=mysql
|
||||
MYSQL_HOST=$db_host
|
||||
MYSQL_PORT=$db_port
|
||||
|
@ -25,6 +27,7 @@ EMAIL_PWD=$email_pwd
|
|||
EMAIL_SSL=$email_ssl
|
||||
EMAIL_FROM=$email_from
|
||||
EMAIL_IDENTITY=$email_identity
|
||||
EMAIL_INSECURE=$email_insecure
|
||||
HARBOR_ADMIN_PASSWORD=$harbor_admin_password
|
||||
PROJECT_CREATION_RESTRICTION=$project_creation_restriction
|
||||
MAX_JOB_WORKERS=$max_job_workers
|
||||
|
@ -41,3 +44,5 @@ RESET=false
|
|||
UAA_ENDPOINT=$uaa_endpoint
|
||||
UAA_CLIENTID=$uaa_clientid
|
||||
UAA_CLIENTSECRET=$uaa_clientsecret
|
||||
UI_URL=http://ui:8080
|
||||
JOBSERVICE_URL=http://jobservice:8080
|
||||
|
|
|
@ -22,4 +22,4 @@ clair:
|
|||
attempts: 3
|
||||
renotifyinterval: 2h
|
||||
http:
|
||||
endpoint: http://ui/service/notifications/clair
|
||||
endpoint: http://ui:8080/service/notifications/clair
|
||||
|
|
|
@ -2,4 +2,4 @@ appname = jobservice
|
|||
runmode = dev
|
||||
|
||||
[dev]
|
||||
httpport = 80
|
||||
httpport = 8080
|
||||
|
|
|
@ -2,4 +2,5 @@ LOG_LEVEL=debug
|
|||
CONFIG_PATH=/etc/jobservice/app.conf
|
||||
UI_SECRET=$ui_secret
|
||||
JOBSERVICE_SECRET=$jobservice_secret
|
||||
ADMINSERVER_URL=http://adminserver:8080
|
||||
GODEBUG=netdns=cgo
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
LOG_ROTATE_DAYS=$log_rotate_days
|
8
make/common/templates/log/logrotate.conf
Normal file
|
@ -0,0 +1,8 @@
|
|||
/var/log/docker/*.log {
|
||||
rotate $log_rotate_count
|
||||
size $log_rotate_size
|
||||
copytruncate
|
||||
compress
|
||||
missingok
|
||||
nodateext
|
||||
}
|
|
@ -18,7 +18,7 @@ http {
|
|||
}
|
||||
|
||||
upstream ui {
|
||||
server ui:80;
|
||||
server ui:8080;
|
||||
}
|
||||
|
||||
log_format timed_combined '$$remote_addr - '
|
||||
|
|
|
@ -18,7 +18,7 @@ http {
|
|||
}
|
||||
|
||||
upstream ui {
|
||||
server ui:80;
|
||||
server ui:8080;
|
||||
}
|
||||
|
||||
log_format timed_combined '$$remote_addr - '
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
version: 0.1
|
||||
log:
|
||||
level: debug
|
||||
level: info
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
|
@ -29,7 +29,7 @@ notifications:
|
|||
endpoints:
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui/service/notifications
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
|
|
45
make/common/templates/registry/config_ha.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
version: 0.1
|
||||
log:
|
||||
level: debug
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
cache:
|
||||
layerinfo: redis
|
||||
Place_holder_for_Storage_configureation
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
delete:
|
||||
enabled: true
|
||||
redis:
|
||||
addr: $redis_url
|
||||
db: 0
|
||||
dialtimeout: 10ms
|
||||
readtimeout: 10ms
|
||||
writetimeout: 10ms
|
||||
pool:
|
||||
maxidle: 16
|
||||
maxactive: 64
|
||||
idletimeout: 300s
|
||||
|
||||
http:
|
||||
addr: :5000
|
||||
secret: placeholder
|
||||
debug:
|
||||
addr: localhost:5001
|
||||
auth:
|
||||
token:
|
||||
issuer: harbor-token-issuer
|
||||
realm: $ui_url/service/token
|
||||
rootcertbundle: /etc/registry/root.crt
|
||||
service: harbor-registry
|
||||
|
||||
notifications:
|
||||
endpoints:
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
|
@ -3,4 +3,4 @@ runmode = dev
|
|||
enablegzip = true
|
||||
|
||||
[dev]
|
||||
httpport = 80
|
||||
httpport = 8080
|
||||
|
|
|
@ -3,4 +3,6 @@ CONFIG_PATH=/etc/ui/app.conf
|
|||
UI_SECRET=$ui_secret
|
||||
JOBSERVICE_SECRET=$jobservice_secret
|
||||
GODEBUG=netdns=cgo
|
||||
ADMINSERVER_URL=http://adminserver:8080
|
||||
UAA_CA_ROOT=/etc/ui/certificates/uaa_ca.pem
|
||||
_REDIS_URL=$redis_url
|
||||
|
|
|
@ -42,7 +42,6 @@ services:
|
|||
- postgres
|
||||
volumes:
|
||||
- ./common/config/clair:/config
|
||||
command: [-config, /config/config.yaml]
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
|
|
|
@ -7,7 +7,7 @@ services:
|
|||
networks:
|
||||
- harbor-notary
|
||||
notary-server:
|
||||
image: vmware/notary-photon:server-0.5.0
|
||||
image: vmware/notary-photon:server-0.5.1
|
||||
container_name: notary-server
|
||||
restart: always
|
||||
networks:
|
||||
|
@ -16,8 +16,6 @@ services:
|
|||
- harbor-notary
|
||||
volumes:
|
||||
- ./common/config/notary:/config
|
||||
entrypoint: /usr/bin/env sh
|
||||
command: -c "/migrations/migrate.sh && notary-server -config=/config/server-config.json -logf=logfmt"
|
||||
depends_on:
|
||||
- notary-db
|
||||
- notary-signer
|
||||
|
@ -27,7 +25,7 @@ services:
|
|||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "notary-server"
|
||||
notary-signer:
|
||||
image: vmware/notary-photon:signer-0.5.0
|
||||
image: vmware/notary-photon:signer-0.5.1
|
||||
container_name: notary-signer
|
||||
restart: always
|
||||
networks:
|
||||
|
@ -39,8 +37,6 @@ services:
|
|||
- ./common/config/notary:/config
|
||||
env_file:
|
||||
- ./common/config/notary/signer_env
|
||||
entrypoint: /usr/bin/env sh
|
||||
command: -c "/migrations/migrate.sh && notary-signer -config=/config/signer-config.json -logf=logfmt"
|
||||
depends_on:
|
||||
- notary-db
|
||||
logging:
|
||||
|
@ -49,7 +45,7 @@ services:
|
|||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "notary-signer"
|
||||
notary-db:
|
||||
image: vmware/mariadb-photon:10.2.8
|
||||
image: vmware/mariadb-photon:10.2.10
|
||||
container_name: notary-db
|
||||
restart: always
|
||||
networks:
|
||||
|
|
|
@ -3,13 +3,12 @@ services:
|
|||
log:
|
||||
image: vmware/harbor-log:__version__
|
||||
container_name: harbor-log
|
||||
env_file:
|
||||
- ./common/config/log/env
|
||||
restart: always
|
||||
volumes:
|
||||
- /var/log/harbor/:/var/log/docker/:z
|
||||
- ./common/config/log/:/etc/logrotate.d/:z
|
||||
ports:
|
||||
- 127.0.0.1:1514:514
|
||||
- 127.0.0.1:1514:10514
|
||||
networks:
|
||||
- harbor
|
||||
registry:
|
||||
|
|
121
make/ha/docker-compose.tpl
Normal file
|
@ -0,0 +1,121 @@
|
|||
version: '2'
|
||||
services:
|
||||
log:
|
||||
image: vmware/harbor-log:__version__
|
||||
container_name: harbor-log
|
||||
restart: always
|
||||
volumes:
|
||||
- /var/log/harbor/:/var/log/docker/:z
|
||||
- ./common/config/log/:/etc/logrotate.d/:z
|
||||
ports:
|
||||
- 127.0.0.1:1514:10514
|
||||
networks:
|
||||
- harbor
|
||||
registry:
|
||||
image: vmware/registry:2.6.2-photon
|
||||
container_name: registry
|
||||
restart: always
|
||||
volumes:
|
||||
- /data/registry:/storage:z
|
||||
- ./common/config/registry/:/etc/registry/:z
|
||||
networks:
|
||||
- harbor
|
||||
environment:
|
||||
- GODEBUG=netdns=cgo
|
||||
command:
|
||||
["serve", "/etc/registry/config.yml"]
|
||||
depends_on:
|
||||
- log
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "registry"
|
||||
adminserver:
|
||||
image: vmware/harbor-adminserver:__version__
|
||||
container_name: harbor-adminserver
|
||||
env_file:
|
||||
- ./common/config/adminserver/env
|
||||
restart: always
|
||||
volumes:
|
||||
- /data/config/:/etc/adminserver/config/:z
|
||||
- /data/secretkey:/etc/adminserver/key:z
|
||||
- /data/:/data/:z
|
||||
networks:
|
||||
- harbor
|
||||
depends_on:
|
||||
- log
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "adminserver"
|
||||
ui:
|
||||
image: vmware/harbor-ui:__version__
|
||||
container_name: harbor-ui
|
||||
env_file:
|
||||
- ./common/config/ui/env
|
||||
restart: always
|
||||
volumes:
|
||||
- ./common/config/ui/app.conf:/etc/ui/app.conf:z
|
||||
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z
|
||||
- ./common/config/ui/certificates/:/etc/ui/certifates/
|
||||
- /data/secretkey:/etc/ui/key:z
|
||||
- /data/ca_download/:/etc/ui/ca/:z
|
||||
- /data/psc/:/etc/ui/token/:z
|
||||
networks:
|
||||
- harbor
|
||||
depends_on:
|
||||
- log
|
||||
- adminserver
|
||||
- registry
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "ui"
|
||||
jobservice:
|
||||
image: vmware/harbor-jobservice:__version__
|
||||
container_name: harbor-jobservice
|
||||
env_file:
|
||||
- ./common/config/jobservice/env
|
||||
restart: always
|
||||
volumes:
|
||||
- /data/job_logs:/var/log/jobs:z
|
||||
- ./common/config/jobservice/app.conf:/etc/jobservice/app.conf:z
|
||||
- /data/secretkey:/etc/jobservice/key:z
|
||||
networks:
|
||||
- harbor
|
||||
depends_on:
|
||||
- ui
|
||||
- adminserver
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "jobservice"
|
||||
proxy:
|
||||
image: vmware/nginx-photon:1.11.13
|
||||
container_name: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ./common/config/nginx:/etc/nginx:z
|
||||
networks:
|
||||
- harbor
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 4443:4443
|
||||
depends_on:
|
||||
- registry
|
||||
- ui
|
||||
- log
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "proxy"
|
||||
networks:
|
||||
harbor:
|
||||
external: false
|
||||
|
80
make/ha/sample/active_active/keepalived_active_active.conf
Normal file
|
@ -0,0 +1,80 @@
|
|||
global_defs {
|
||||
router_id haborlb
|
||||
}
|
||||
vrrp_sync_groups VG1 {
|
||||
group {
|
||||
VI_1
|
||||
}
|
||||
}
|
||||
#Please change to ens160 to the interface name on you loadbalancer hosts.
|
||||
vrrp_instance VI_1 {
|
||||
interface ens160
|
||||
|
||||
track_interface {
|
||||
ens160
|
||||
}
|
||||
|
||||
state MASTER
|
||||
virtual_router_id 51
|
||||
priority 10
|
||||
|
||||
virtual_ipaddress {
|
||||
VIP/32
|
||||
}
|
||||
advert_int 1
|
||||
authentication {
|
||||
auth_type PASS
|
||||
auth_pass d0cker
|
||||
}
|
||||
|
||||
}
|
||||
#Please change VIP, harbor_node1_ip, harbor_node2_ip to real ip address
|
||||
virtual_server VIP 80 {
|
||||
delay_loop 15
|
||||
lb_algo rr
|
||||
lb_kind DR
|
||||
protocol TCP
|
||||
nat_mask 255.255.255.0
|
||||
persistence_timeout 10
|
||||
|
||||
real_server harbor_node1_ip 80 {
|
||||
weight 10
|
||||
TCP_CHECK {
|
||||
connect_timeout 3
|
||||
connect_port 80
|
||||
}
|
||||
}
|
||||
|
||||
real_server harbor_node2_ip 80 {
|
||||
weight 10
|
||||
TCP_CHECK {
|
||||
connect_timeout 3
|
||||
connect_port 80
|
||||
}
|
||||
}
|
||||
}
|
||||
#Please uncomment the follow when harbor running under https
|
||||
#virtual_server VIP 443 {
|
||||
# delay_loop 15
|
||||
# lb_algo rr
|
||||
# lb_kind DR
|
||||
# protocol TCP
|
||||
# nat_mask 255.255.255.0
|
||||
# persistence_timeout 10
|
||||
#
|
||||
# real_server harbor_node1_ip 443 {
|
||||
# weight 10
|
||||
# TCP_CHECK {
|
||||
# connect_timeout 3
|
||||
# connect_port 443
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# real_server harbor_node2_ip 443 {
|
||||
# weight 10
|
||||
# TCP_CHECK {
|
||||
# connect_timeout 3
|
||||
# connect_port 443
|
||||
# }
|
||||
# }
|
||||
#}
|
7
make/ha/sample/active_standby/check_harbor.sh
Normal file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
http_code = `curl -s -o /dev/null -w "%{http_code}" 127.0.0.1`
|
||||
if [ $http_code == 200 ] || [ $http_code == 301 ] ; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
36
make/ha/sample/active_standby/keepalived_active_standby.conf
Normal file
|
@ -0,0 +1,36 @@
|
|||
global_defs {
|
||||
router_id haborlb
|
||||
}
|
||||
vrrp_script check_harbor {
|
||||
script "/usr/local/bin/check_harbor.sh"
|
||||
interval 15
|
||||
fail 5
|
||||
rise 2
|
||||
}
|
||||
vrrp_sync_groups VG1 {
|
||||
group {
|
||||
VI_1
|
||||
}
|
||||
}
|
||||
#Please change to ens160 to the interface name on you loadbalancer hosts.
|
||||
vrrp_instance VI_1 {
|
||||
interface ens160
|
||||
|
||||
track_interface {
|
||||
ens160
|
||||
}
|
||||
|
||||
state MASTER
|
||||
virtual_router_id 51
|
||||
priority 10
|
||||
|
||||
virtual_ipaddress {
|
||||
VIP/32
|
||||
}
|
||||
advert_int 1
|
||||
authentication {
|
||||
auth_type PASS
|
||||
auth_pass d0cker
|
||||
}
|
||||
|
||||
}
|
|
@ -34,8 +34,12 @@ admiral_url = NA
|
|||
#Please update it before deployment, subsequent update will cause Clair's API server and Harbor unable to access Clair's database.
|
||||
clair_db_password = password
|
||||
|
||||
#The logs n days before will be compressed
|
||||
log_rotate_days = 3
|
||||
#Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated.
|
||||
log_rotate_count = 50
|
||||
#Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes.
|
||||
#If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G
|
||||
#are all valid.
|
||||
log_rotate_size = 200M
|
||||
|
||||
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
|
||||
#only take effect in the first boot, the subsequent changes of these properties
|
||||
|
@ -55,6 +59,7 @@ email_username = sample_admin@mydomain.com
|
|||
email_password = abc
|
||||
email_from = admin <sample_admin@mydomain.com>
|
||||
email_ssl = false
|
||||
email_insecure = false
|
||||
|
||||
##The initial password of Harbor admin, only works for the first time when Harbor starts.
|
||||
#It has no effect after the first launch of Harbor.
|
||||
|
@ -101,10 +106,6 @@ token_expiration = 30
|
|||
#Set to "adminonly" so that only admin user can create project.
|
||||
project_creation_restriction = everyone
|
||||
|
||||
#Determine whether the job service should verify the ssl cert when it connects to a remote registry.
|
||||
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
|
||||
verify_remote_cert = on
|
||||
|
||||
#The follow configurations are for Harbor HA mode only
|
||||
|
||||
#the address of the mysql database.
|
||||
|
@ -115,6 +116,8 @@ db_port = 3306
|
|||
|
||||
#The user name of mysql database
|
||||
db_user = root
|
||||
#The redis server address
|
||||
redis_url =
|
||||
#************************END INITIAL PROPERTIES************************
|
||||
#The following attributes only need to be set when auth mode is uaa_auth
|
||||
uaa_endpoint = uaa.mydomain.org
|
||||
|
|
|
@ -58,7 +58,8 @@ item=0
|
|||
with_notary=$false
|
||||
# clair is not enabled by default
|
||||
with_clair=$false
|
||||
|
||||
# HA mode is not enabled by default
|
||||
harbor_ha=$false
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--help)
|
||||
|
@ -68,6 +69,8 @@ while [ $# -gt 0 ]; do
|
|||
with_notary=true;;
|
||||
--with-clair)
|
||||
with_clair=true;;
|
||||
--ha)
|
||||
harbor_ha=true;;
|
||||
*)
|
||||
note "$usage"
|
||||
exit 1;;
|
||||
|
@ -158,24 +161,28 @@ then
|
|||
sed "s/^hostname = .*/hostname = $host/g" -i ./harbor.cfg
|
||||
fi
|
||||
prepare_para=
|
||||
if [ $with_notary ]
|
||||
if [ $with_notary ] && [ ! $harbor_ha ]
|
||||
then
|
||||
prepare_para="${prepare_para} --with-notary"
|
||||
fi
|
||||
if [ $with_clair ]
|
||||
if [ $with_clair ] && [ ! $harbor_ha ]
|
||||
then
|
||||
prepare_para="${prepare_para} --with-clair"
|
||||
fi
|
||||
if [ $harbor_ha ]
|
||||
then
|
||||
prepare_para="${prepare_para} --ha"
|
||||
fi
|
||||
./prepare $prepare_para
|
||||
echo ""
|
||||
|
||||
h2 "[Step $item]: checking existing instance of Harbor ..."; let item+=1
|
||||
docker_compose_list='-f docker-compose.yml'
|
||||
if [ $with_notary ]
|
||||
if [ $with_notary ] && [ ! $harbor_ha ]
|
||||
then
|
||||
docker_compose_list="${docker_compose_list} -f docker-compose.notary.yml"
|
||||
fi
|
||||
if [ $with_clair ]
|
||||
if [ $with_clair ] && [ ! $harbor_ha ]
|
||||
then
|
||||
docker_compose_list="${docker_compose_list} -f docker-compose.clair.yml"
|
||||
fi
|
||||
|
@ -188,6 +195,11 @@ fi
|
|||
echo ""
|
||||
|
||||
h2 "[Step $item]: starting Harbor ..."
|
||||
if [ $harbor_ha ]
|
||||
then
|
||||
mv docker-compose.yml docker-compose.yml.bak
|
||||
cp ha/docker-compose.yml docker-compose.yml
|
||||
fi
|
||||
docker-compose $docker_compose_list up -d
|
||||
|
||||
protocol=http
|
||||
|
|
|
@ -75,7 +75,7 @@ build:
|
|||
@echo "Done."
|
||||
|
||||
@echo "building log container for photon..."
|
||||
$(DOCKERBUILD) -f $(DOCKERFILEPATH_LOG)/$(DOCKERFILENAME_LOG) -t $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) .
|
||||
$(DOCKERBUILD) -f $(DOCKERFILEPATH_LOG)/$(DOCKERFILENAME_LOG) -t $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) $(DOCKERFILEPATH_LOG)
|
||||
@echo "Done."
|
||||
|
||||
cleanimage:
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
FROM vmware/photon:1.0
|
||||
|
||||
RUN tdnf erase vim -y \
|
||||
&& tdnf distro-sync -y || echo \
|
||||
&& tdnf distro-sync -y \
|
||||
&& tdnf install -y sudo \
|
||||
&& tdnf clean all \
|
||||
&& groupadd -r -g 10000 harbor && useradd --no-log-init -r -g 10000 -u 10000 harbor \
|
||||
&& mkdir /harbor/
|
||||
COPY ./make/dev/adminserver/harbor_adminserver /harbor/
|
||||
COPY ./make/dev/adminserver/harbor_adminserver ./make/photon/adminserver/start.sh /harbor/
|
||||
HEALTHCHECK CMD curl -s -o /dev/null -w "%{http_code}" 127.0.0.1:8080/api/configurations|grep 401
|
||||
|
||||
RUN chmod u+x /harbor/harbor_adminserver
|
||||
RUN chmod u+x /harbor/harbor_adminserver /harbor/start.sh
|
||||
WORKDIR /harbor/
|
||||
ENTRYPOINT ["/harbor/harbor_adminserver"]
|
||||
ENTRYPOINT ["/harbor/start.sh"]
|
||||
|
|
5
make/photon/adminserver/start.sh
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
if [ -d /etc/adminserver ]; then
|
||||
chown -R 10000:10000 /etc/adminserver
|
||||
fi
|
||||
sudo -E -u \#10000 "/harbor/harbor_adminserver"
|
|
@ -1,13 +1,27 @@
|
|||
FROM library/photon:1.0
|
||||
FROM vmware/photon:1.0
|
||||
|
||||
RUN tdnf install -y git bzr rpm xz \
|
||||
&& mkdir /clair2.0.1/
|
||||
|
||||
RUN tdnf distro-sync -y \
|
||||
&& tdnf erase vim -y \
|
||||
&& tdnf install -y git shadow sudo bzr rpm xz python-xml \
|
||||
&& tdnf clean all \
|
||||
&& mkdir /clair2.0.1/ \
|
||||
&& groupadd -r -g 10000 clair \
|
||||
&& useradd --no-log-init -m -r -g 10000 -u 10000 clair
|
||||
COPY clair /clair2.0.1/
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY dumb-init /dumb-init
|
||||
|
||||
VOLUME /config
|
||||
|
||||
EXPOSE 6060 6061
|
||||
|
||||
RUN chmod u+x /clair2.0.1/clair
|
||||
RUN chown -R 10000:10000 /clair2.0.1 \
|
||||
&& chmod u+x /clair2.0.1/clair \
|
||||
&& chmod u+x /docker-entrypoint.sh \
|
||||
&& chmod +x /dumb-init
|
||||
|
||||
ENTRYPOINT ["/clair2.0.1/clair"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1:6061/health || exit 1
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
|
5
make/photon/clair/docker-entrypoint.sh
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
chown -R 10000:10000 /config
|
||||
sudo -E -H -u \#10000 sh -c "/dumb-init -- /clair2.0.1/clair -config /config/config.yaml"
|
||||
set +e
|