diff --git a/.drone.yml b/.drone.yml index d9af69b7f..738f48b57 100644 --- a/.drone.yml +++ b/.drone.yml @@ -48,13 +48,9 @@ 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 + - cp harbor-offline-installer-*.tgz bundle/harbor-offline-installer-latest.tgz - ls -la bundle - - ls -la pks-bundle when: repo: vmware/harbor event: [ push, tag ] @@ -83,7 +79,7 @@ pipeline: when: repo: vmware/harbor event: [ push, tag ] - branch: [ master, release-* ] + branch: [ master ] status: success publish-gcs-releases: @@ -100,20 +96,6 @@ 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 diff --git a/.drone.yml.sig b/.drone.yml.sig index b3ba90ca0..8e41dfd02 100644 --- a/.drone.yml.sig +++ b/.drone.yml.sig @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiJ9.IyBIYXJib3IgZHJvbmUuCi0tLQp3b3Jrc3BhY2U6CiAgYmFzZTogL2Ryb25lCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vdm13YXJlL2hhcmJvcgoKcGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogZmFsc2UKCiAgaW50ZWdyYXRpb24tdGVzdC1vbi1wcjoKICAgIGltYWdlOiB2bXdhcmUvaGFyYm9yLWUyZS1lbmdpbmU6MS4zOAogICAgcHVsbDogdHJ1ZQogICAgcHJpdmlsZWdlZDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIEJJTjogYmluCiAgICAgIEdPUEFUSDogL2Ryb25lCiAgICAgIFNIRUxMOiAvYmluL2Jhc2gKICAgICAgTE9HX1RFTVBfRElSOiBpbnN0YWxsLWxvZ3MKICAgICAgR0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWTogICR7R0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWX0KICAgICAgRFJPTkVfU0VSVkVSOiAgJHtEUk9ORV9TRVJWRVJ9CiAgICAgIERST05FX1RPS0VOOiAgJHtEUk9ORV9UT0tFTl9JTlRFfQogICAgICBIQVJCT1JfQURNSU46ICR7SEFSQk9SX0FETUlOfQogICAgICBIQVJCT1JfUEFTU1dPUkQ6ICR7SEFSQk9SX1BBU1NXT1JEfQogICAgICBHU19QUk9KRUNUX0lEOiAke0dTX1BST0pFQ1RfSUR9CiAgICAgIEdTX0NMSUVOVF9FTUFJTDogJHtHU19DTElFTlRfRU1BSUx9CiAgICAgIEdTX1BSSVZBVEVfS0VZOiAke0dTX1BSSVZBVEVfS0VZfQogICAgICBET01BSU46ICR7Q0lfRE9NQUlOfQogICAgICBNQUlMX1BXRDogJHtNQUlMX1BXRH0KICAgICAgTlBNX1VTRVJOQU1FOiAke05QTV9VU0VSTkFNRX0KICAgICAgTlBNX1BBU1NXT1JEOiAke05QTV9QQVNTV09SRH0KICAgIGNvbW1hbmRzOgogICAgICAtIHRlc3RzL2ludGVncmF0aW9uLnNoCiAgICB3aGVuOgogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgYnVuZGxlOgogICAgaW1hZ2U6IHZtd2FyZS9oYXJib3ItZTJlLWVuZ2luZToxLjM4CiAgICBwdWxsOiB0cnVlCiAgICBwcml2aWxlZ2VkOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQklOOiBiaW4KICAgICAgR09QQVRIOiAvZHJvbmUKICAgICAgU0hFTEw6IC9iaW4vYmFzaAogICAgICBCVUlMRF9OVU1CRVI6ICR7RFJPTkVfQlVJTERfTlVNQkVSfQogICAgY29tbWFuZHM6CiAgICAgIC0gZHUgLWtzIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiB8IGF3ayAne3ByaW50ICQxIC8gMTAyNH0nIHwgeyByZWFkIHg7IGVjaG8gJHggTUI7IH0KICAgICAgLSBta2RpciAtcCBidW5kbGUKICAgICAgLSBta2RpciAtcCBwa3MtYnVuZGxlCiAgICAgIC0gZWNobyAkKGdpdCBkZXNjcmliZSAtLXRhZ3MpID4gcGtzLWJ1bmRsZS92ZXJzaW9uIAogICAgICAtIGNwIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiBidW5kbGUKICAgICAgLSBpZiBbICR7RFJPTkVfQlJBTkNIfSA9ICJtYXN0ZXIiIF07IHRoZW4gY3AgaGFyYm9yLW9mZmxpbmUtaW5zdGFsbGVyLSoudGd6IHBrcy1idW5kbGUvaGFyYm9yLW9mZmxpbmUtaW5zdGFsbGVyLWxhdGVzdC1tYXN0ZXIudGd6OyBmaQogICAgICAtIGlmICggZWNobyAke0RST05FX0JSQU5DSH0gfCBncmVwICJwa3MqIiApOyB0aGVuIGNwIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiBwa3MtYnVuZGxlL2hhcmJvci1vZmZsaW5lLWluc3RhbGxlci1sYXRlc3QtcGtzLnRnejsgZmkKICAgICAgLSBscyAtbGEgYnVuZGxlCiAgICAgIC0gbHMgLWxhIHBrcy1idW5kbGUKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCBwa3MtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgbm90aWZ5LXNsYWNrOgogICAgaW1hZ2U6IHBsdWdpbnMvc2xhY2sKICAgIHdlYmhvb2s6ICR7U0xBQ0tfVVJMfQogICAgdXNlcm5hbWU6IGRyb25lCiAgICB0ZW1wbGF0ZTogPgogICAgICBidWlsZCBodHRwczovL2NpLnZjbmEuaW8vdm13YXJlL2hhcmJvci97eyBidWlsZC5udW1iZXIgfX0gZmluaXNoZWQgd2l0aCBhIHt7IGJ1aWxkLnN0YXR1cyB9fSBzdGF0dXMuIFBsZWFzZSBmaW5kIGxvZ3MgYXQgaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL2hhcmJvci1jaS1sb2dzL2ludGVncmF0aW9uX2xvZ3Nfe3sgYnVpbGQubnVtYmVyIH19X3t7IGJ1aWxkLmNvbW1pdCB9fS50YXIuZ3oKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCByZWZzL3RhZ3MvKiBdCiAgICAgIHN0YXR1czogWyBmYWlsdXJlLCBzdWNjZXNzIF0KCiAgcHVibGlzaC1nY3MtYnVpbGRzOgogICAgaW1hZ2U6IG1hcGxhaW4vZHJvbmUtZ2NzOmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgc291cmNlOiBidW5kbGUKICAgIHRhcmdldDogaGFyYm9yLWJ1aWxkcwogICAgYWNsOgogICAgICAtIGFsbFVzZXJzOlJFQURFUgogICAgY2FjaGVfY29udHJvbDogcHVibGljLG1heC1hZ2U9MzYwMAogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLSogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgcHVibGlzaC1nY3MtcmVsZWFzZXM6CiAgICBpbWFnZTogbWFwbGFpbi9kcm9uZS1nY3M6bGF0ZXN0CiAgICBwdWxsOiB0cnVlCiAgICBzb3VyY2U6IGJ1bmRsZQogICAgdGFyZ2V0OiBoYXJib3ItcmVsZWFzZXMKICAgIGFjbDoKICAgICAgLSBhbGxVc2VyczpSRUFERVIKICAgIGNhY2hlX2NvbnRyb2w6IHB1YmxpYyxtYXgtYWdlPTM2MDAKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIHJlbGVhc2UtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgcHVibGlzaC1nY3MtcGtzLWJ1aWxkczoKICAgIGltYWdlOiBtYXBsYWluL2Ryb25lLWdjczpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIHNvdXJjZTogcGtzLWJ1bmRsZQogICAgdGFyZ2V0OiBoYXJib3ItY2ktcGlwZWxpbmUtc3RvcmUvbGF0ZXN0CiAgICBhY2w6CiAgICAgIC0gYWxsVXNlcnM6UkVBREVSCiAgICBjYWNoZV9jb250cm9sOiBwdWJsaWMsbWF4LWFnZT0zNjAwCiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIsIHBrcy0qLCByZWZzL3RhZ3MvKiBdCiAgICAgIHN0YXR1czogc3VjY2VzcwoKICB0cmlnZ2VyOgogICAgaW1hZ2U6IHBsdWdpbnMvZG93bnN0cmVhbQogICAgc2VydmVyOiBodHRwczovL2NpLnZjbmEuaW8KICAgIHRva2VuOiAke0RPV05TVFJFQU1fVE9LRU59CiAgICBmb3JrOiB0cnVlCiAgICByZXBvc2l0b3JpZXM6CiAgICAgICAtIHZtd2FyZS92aWMtcHJvZHVjdAogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyLCByZWxlYXNlLSosIHJlZnMvdGFncy8qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCg.TRzg0jvokGI8PBccqkW4foVBX_1uGzFUhTRaPzMFaeY \ No newline at end of file +eyJhbGciOiJIUzI1NiJ9.IyBIYXJib3IgZHJvbmUuCi0tLQp3b3Jrc3BhY2U6CiAgYmFzZTogL2Ryb25lCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vdm13YXJlL2hhcmJvcgoKcGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogZmFsc2UKCiAgaW50ZWdyYXRpb24tdGVzdC1vbi1wcjoKICAgIGltYWdlOiB2bXdhcmUvaGFyYm9yLWUyZS1lbmdpbmU6MS4zOAogICAgcHVsbDogdHJ1ZQogICAgcHJpdmlsZWdlZDogdHJ1ZQogICAgZW52aXJvbm1lbnQ6CiAgICAgIEJJTjogYmluCiAgICAgIEdPUEFUSDogL2Ryb25lCiAgICAgIFNIRUxMOiAvYmluL2Jhc2gKICAgICAgTE9HX1RFTVBfRElSOiBpbnN0YWxsLWxvZ3MKICAgICAgR0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWTogICR7R0lUSFVCX0FVVE9NQVRJT05fQVBJX0tFWX0KICAgICAgRFJPTkVfU0VSVkVSOiAgJHtEUk9ORV9TRVJWRVJ9CiAgICAgIERST05FX1RPS0VOOiAgJHtEUk9ORV9UT0tFTl9JTlRFfQogICAgICBIQVJCT1JfQURNSU46ICR7SEFSQk9SX0FETUlOfQogICAgICBIQVJCT1JfUEFTU1dPUkQ6ICR7SEFSQk9SX1BBU1NXT1JEfQogICAgICBHU19QUk9KRUNUX0lEOiAke0dTX1BST0pFQ1RfSUR9CiAgICAgIEdTX0NMSUVOVF9FTUFJTDogJHtHU19DTElFTlRfRU1BSUx9CiAgICAgIEdTX1BSSVZBVEVfS0VZOiAke0dTX1BSSVZBVEVfS0VZfQogICAgICBET01BSU46ICR7Q0lfRE9NQUlOfQogICAgICBNQUlMX1BXRDogJHtNQUlMX1BXRH0KICAgICAgTlBNX1VTRVJOQU1FOiAke05QTV9VU0VSTkFNRX0KICAgICAgTlBNX1BBU1NXT1JEOiAke05QTV9QQVNTV09SRH0KICAgIGNvbW1hbmRzOgogICAgICAtIHRlc3RzL2ludGVncmF0aW9uLnNoCiAgICB3aGVuOgogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgYnVuZGxlOgogICAgaW1hZ2U6IHZtd2FyZS9oYXJib3ItZTJlLWVuZ2luZToxLjM4CiAgICBwdWxsOiB0cnVlCiAgICBwcml2aWxlZ2VkOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQklOOiBiaW4KICAgICAgR09QQVRIOiAvZHJvbmUKICAgICAgU0hFTEw6IC9iaW4vYmFzaAogICAgICBCVUlMRF9OVU1CRVI6ICR7RFJPTkVfQlVJTERfTlVNQkVSfQogICAgY29tbWFuZHM6CiAgICAgIC0gZHUgLWtzIGhhcmJvci1vZmZsaW5lLWluc3RhbGxlci0qLnRneiB8IGF3ayAne3ByaW50ICQxIC8gMTAyNH0nIHwgeyByZWFkIHg7IGVjaG8gJHggTUI7IH0KICAgICAgLSBta2RpciAtcCBidW5kbGUKICAgICAgLSBjcCBoYXJib3Itb2ZmbGluZS1pbnN0YWxsZXItKi50Z3ogYnVuZGxlCiAgICAgIC0gY3AgaGFyYm9yLW9mZmxpbmUtaW5zdGFsbGVyLSoudGd6IGJ1bmRsZS9oYXJib3Itb2ZmbGluZS1pbnN0YWxsZXItbGF0ZXN0LnRnegogICAgICAtIGxzIC1sYSBidW5kbGUKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCBwa3MtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MKCiAgbm90aWZ5LXNsYWNrOgogICAgaW1hZ2U6IHBsdWdpbnMvc2xhY2sKICAgIHdlYmhvb2s6ICR7U0xBQ0tfVVJMfQogICAgdXNlcm5hbWU6IGRyb25lCiAgICB0ZW1wbGF0ZTogPgogICAgICBidWlsZCBodHRwczovL2NpLnZjbmEuaW8vdm13YXJlL2hhcmJvci97eyBidWlsZC5udW1iZXIgfX0gZmluaXNoZWQgd2l0aCBhIHt7IGJ1aWxkLnN0YXR1cyB9fSBzdGF0dXMuIFBsZWFzZSBmaW5kIGxvZ3MgYXQgaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL2hhcmJvci1jaS1sb2dzL2ludGVncmF0aW9uX2xvZ3Nfe3sgYnVpbGQubnVtYmVyIH19X3t7IGJ1aWxkLmNvbW1pdCB9fS50YXIuZ3oKICAgIHdoZW46CiAgICAgIHJlcG86IHZtd2FyZS9oYXJib3IKICAgICAgYnJhbmNoOiBbIG1hc3RlciwgcmVsZWFzZS0qLCByZWZzL3RhZ3MvKiBdCiAgICAgIHN0YXR1czogWyBmYWlsdXJlLCBzdWNjZXNzIF0KCiAgcHVibGlzaC1nY3MtYnVpbGRzOgogICAgaW1hZ2U6IG1hcGxhaW4vZHJvbmUtZ2NzOmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgc291cmNlOiBidW5kbGUKICAgIHRhcmdldDogaGFyYm9yLWJ1aWxkcwogICAgYWNsOgogICAgICAtIGFsbFVzZXJzOlJFQURFUgogICAgY2FjaGVfY29udHJvbDogcHVibGljLG1heC1hZ2U9MzYwMAogICAgd2hlbjoKICAgICAgcmVwbzogdm13YXJlL2hhcmJvcgogICAgICBldmVudDogWyBwdXNoLCB0YWcgXQogICAgICBicmFuY2g6IFsgbWFzdGVyIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCgogIHB1Ymxpc2gtZ2NzLXJlbGVhc2VzOgogICAgaW1hZ2U6IG1hcGxhaW4vZHJvbmUtZ2NzOmxhdGVzdAogICAgcHVsbDogdHJ1ZQogICAgc291cmNlOiBidW5kbGUKICAgIHRhcmdldDogaGFyYm9yLXJlbGVhc2VzCiAgICBhY2w6CiAgICAgIC0gYWxsVXNlcnM6UkVBREVSCiAgICBjYWNoZV9jb250cm9sOiBwdWJsaWMsbWF4LWFnZT0zNjAwCiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWxlYXNlLSosIHJlZnMvdGFncy8qIF0KICAgICAgc3RhdHVzOiBzdWNjZXNzCgogIHRyaWdnZXI6CiAgICBpbWFnZTogcGx1Z2lucy9kb3duc3RyZWFtCiAgICBzZXJ2ZXI6IGh0dHBzOi8vY2kudmNuYS5pbwogICAgdG9rZW46ICR7RE9XTlNUUkVBTV9UT0tFTn0KICAgIGZvcms6IHRydWUKICAgIHJlcG9zaXRvcmllczoKICAgICAgIC0gdm13YXJlL3ZpYy1wcm9kdWN0CiAgICB3aGVuOgogICAgICByZXBvOiB2bXdhcmUvaGFyYm9yCiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZyBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIsIHJlbGVhc2UtKiwgcmVmcy90YWdzLyogXQogICAgICBzdGF0dXM6IHN1Y2Nlc3MK.0TeBeHyYbP8xrqxi1RUDjXnB0tqChcuuDhNv4hbbUJs \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 294124bba..336c92a06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,7 +79,7 @@ script: - sudo mkdir -p /harbor - sudo mv ./VERSION /harbor/VERSION - sudo service mysql stop - - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 + - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 - cat ./src/ui_ng/lib/npm-ut-test-results - sudo ./tests/testprepare.sh - sudo make -f make/photon/Makefile -e MARIADBVERSION=10.2.10 -e VERSIONTAG=dev @@ -106,7 +106,7 @@ script: - 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 + - sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 NOTARYFLAG=true CLAIRFLAG=true - sleep 10 - docker ps - ./tests/validatecontainers.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 639812eca..5bb8746a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,9 @@ Please submit a PR to contain changes bit by bit. A PR consisting of a lot featu You can propose new designs for existing Harbor features. You can also design entirely new features. Please do open an issue on Github for discussion first. This is necessary to ensure the overall architecture is consistent and to avoid duplicated work in the roadmap. +### Dependency management +Harbor uses [dep](https://github.com/golang/dep) for dependency management of go code. The official maintainers will take the responsibility for managing the code in `vendor` directory. Please don't try to submit PR to update the dependency code, open an issue instead. If your PR requires a change in the vendor code please make sure you discuss it with maintainers in advance. + ### Conventions Fork Harbor's repository and make changes on your own fork in a new branch. The branch should be named XXX-description where XXX is the number of the issue. Please run the full test suite on your branch before creating a PR. diff --git a/Makefile b/Makefile index 690826df1..d42a8c97e 100644 --- a/Makefile +++ b/Makefile @@ -210,7 +210,7 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \ $(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \ $(DOCKERIMAGENAME_DB):$(VERSIONTAG) \ $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \ - vmware/nginx-photon:$(NGINXVERSION)-$(VERSIONTAG) vmware/registry-photon:$(REGISTRYVERSION)-$(VERSIONTAG) \ + vmware/nginx-photon:$(NGINXVERSION) vmware/registry-photon:$(REGISTRYVERSION)-$(VERSIONTAG) \ vmware/photon:$(PHOTONVERSION) PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(GITTAGVERSION).tgz \ $(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz \ @@ -227,13 +227,13 @@ DOCKERCOMPOSE_LIST=-f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSEFILENAME) ifeq ($(NOTARYFLAG), true) DOCKERSAVE_PARA+= vmware/notary-server-photon:$(NOTARYVERSION)-$(VERSIONTAG) vmware/notary-signer-photon:$(NOTARYVERSION)-$(VERSIONTAG) \ - vmware/mariadb-photon:$(MARIADBVERSION)-$(VERSIONTAG) + vmware/mariadb-photon:$(MARIADBVERSION) PACKAGE_OFFLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) PACKAGE_ONLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME) endif ifeq ($(CLAIRFLAG), true) - DOCKERSAVE_PARA+= vmware/clair-photon:$(CLAIRVERSION)-$(VERSIONTAG) vmware/postgresql-photon:$(CLAIRDBVERSION)-$(VERSIONTAG) + DOCKERSAVE_PARA+= vmware/clair-photon:$(CLAIRVERSION)-$(VERSIONTAG) vmware/postgresql-photon:$(CLAIRDBVERSION) PACKAGE_OFFLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME) PACKAGE_ONLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME) DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME) @@ -336,10 +336,10 @@ package_online: modify_composefile package_offline: compile version build modify_sourcefiles modify_composefile @echo "packing offline package ..." @cp -r make $(HARBORPKG) - @cp LICENSE $(HARBORPKG)/LICENSE @cp NOTICE $(HARBORPKG)/NOTICE - @cp $(HARBORPKG)/common/db/registry.sql $(HARBORPKG)/ha/ + @cp $(HARBORPKG)/photon/db/registry.sql $(HARBORPKG)/ha/ + @if [ "$(MIGRATORFLAG)" = "true" ] ; then \ echo "pulling DB migrator..."; \ $(DOCKERPULL) vmware/harbor-db-migrator:$(MIGRATORVERSION); \ diff --git a/docs/img/ovainstall/DeployOVFmenu.png b/docs/img/ovainstall/DeployOVFmenu.png new file mode 100644 index 000000000..8c85e2b98 Binary files /dev/null and b/docs/img/ovainstall/DeployOVFmenu.png differ diff --git a/docs/img/ovainstall/afterlogin.png b/docs/img/ovainstall/afterlogin.png new file mode 100644 index 000000000..a49cae846 Binary files /dev/null and b/docs/img/ovainstall/afterlogin.png differ diff --git a/docs/img/ovainstall/complete.png b/docs/img/ovainstall/complete.png new file mode 100644 index 000000000..69a561394 Binary files /dev/null and b/docs/img/ovainstall/complete.png differ diff --git a/docs/img/ovainstall/custom_cert.png b/docs/img/ovainstall/custom_cert.png new file mode 100644 index 000000000..1b0befc79 Binary files /dev/null and b/docs/img/ovainstall/custom_cert.png differ diff --git a/docs/img/ovainstall/customizeharbor.png b/docs/img/ovainstall/customizeharbor.png new file mode 100644 index 000000000..1e5a95340 Binary files /dev/null and b/docs/img/ovainstall/customizeharbor.png differ diff --git a/docs/img/ovainstall/customizeldap.png b/docs/img/ovainstall/customizeldap.png new file mode 100644 index 000000000..98ec49ae2 Binary files /dev/null and b/docs/img/ovainstall/customizeldap.png differ diff --git a/docs/img/ovainstall/datastore.png b/docs/img/ovainstall/datastore.png new file mode 100644 index 000000000..48c5222b4 Binary files /dev/null and b/docs/img/ovainstall/datastore.png differ diff --git a/docs/img/ovainstall/importova.png b/docs/img/ovainstall/importova.png new file mode 100644 index 000000000..8af239f9c Binary files /dev/null and b/docs/img/ovainstall/importova.png differ diff --git a/docs/img/ovainstall/login.png b/docs/img/ovainstall/login.png new file mode 100644 index 000000000..3d96f14a5 Binary files /dev/null and b/docs/img/ovainstall/login.png differ diff --git a/docs/img/ovainstall/namelocation.png b/docs/img/ovainstall/namelocation.png new file mode 100644 index 000000000..a93694582 Binary files /dev/null and b/docs/img/ovainstall/namelocation.png differ diff --git a/docs/img/ovainstall/network.png b/docs/img/ovainstall/network.png new file mode 100644 index 000000000..b54b0b03b Binary files /dev/null and b/docs/img/ovainstall/network.png differ diff --git a/docs/img/ovainstall/network2.png b/docs/img/ovainstall/network2.png new file mode 100644 index 000000000..2ad678080 Binary files /dev/null and b/docs/img/ovainstall/network2.png differ diff --git a/docs/img/ovainstall/poweron.png b/docs/img/ovainstall/poweron.png new file mode 100644 index 000000000..0addfdbf6 Binary files /dev/null and b/docs/img/ovainstall/poweron.png differ diff --git a/docs/img/ovainstall/poweron2.png b/docs/img/ovainstall/poweron2.png new file mode 100644 index 000000000..a615d6abf Binary files /dev/null and b/docs/img/ovainstall/poweron2.png differ diff --git a/docs/img/ovainstall/resource.png b/docs/img/ovainstall/resource.png new file mode 100644 index 000000000..affcc0342 Binary files /dev/null and b/docs/img/ovainstall/resource.png differ diff --git a/docs/img/ovainstall/reviewdetail.png b/docs/img/ovainstall/reviewdetail.png new file mode 100644 index 000000000..a9952bc27 Binary files /dev/null and b/docs/img/ovainstall/reviewdetail.png differ diff --git a/docs/img/ovainstall/system.png b/docs/img/ovainstall/system.png new file mode 100644 index 000000000..40ca2e326 Binary files /dev/null and b/docs/img/ovainstall/system.png differ diff --git a/docs/install_guide_ova.md b/docs/install_guide_ova.md new file mode 100644 index 000000000..05c04153f --- /dev/null +++ b/docs/install_guide_ova.md @@ -0,0 +1,111 @@ +# Deploying Harbor from OVA + +**Prerequisites** + +- Download the build of the OVA installer from the **[official release](https://github.com/vmware/harbor/releases)** page. +- Import the appliance to a vCenter Server instance. Deploying the appliance directly on an ESXi host is not supported. +- The system requirements are as follows: + - vCenter Server 6.0 or 6.5. + - ESXi 6.0 or 6.5 for all hosts. + - 2 vCPUs or more. + - 8GB RAM or more. + - At least 80GB free disk space on the datastore. +- Ensure that vCenter user has the following privileges: + - Datastore > Allocate space + - Datastore > Low level file Operations + - Folder > Create Folder + - Folder > Delete Folder + - Network > Assign network + - Resource > Assign virtual machine to resource pool + - Virtual machine > Configuration > Add new disk + - Virtual Machine > Configuration > Add existing disk + - Virtual Machine > Configuration > Add or remove device + - Virtual Machine > Configuration > Change CPU count + - Virtual Machine > Configuration > Change resource + - Virtual Machine > Configuration > Memory + - Virtual Machine > Configuration > Modify device settings + - Virtual Machine > Configuration > Remove disk + - Virtual Machine > Configuration > Rename + - Virtual Machine > Configuration > Settings + - Virtual machine > Configuration > Advanced + - Virtual Machine > Interaction > Power off + - Virtual Machine > Interaction > Power on + - Virtual Machine > Inventory > Create from existing + - Virtual Machine > Inventory > Create new + - Virtual Machine > Inventory > Remove + - Virtual Machine > Provisioning > Clone virtual machine + - Virtual Machine > Provisioning > Customize + - Virtual Machine > Provisioning > Read customization specifications + - vApp > Import + - Profile-driven storage -> Profile-driven storage view +- Ensure that all vCenter Server instances and ESXi hosts in the environment in which you are deploying the appliance have network time protocol (NTP) running. Running NTP prevents problems arising from clock skew between Harbor and its clients. +- Use the Flex-based vSphere Web Client to deploy the appliance. You cannot deploy Harbor OVA file from the HTML5 vSphere Client or from the legacy Windows client. + +**Procedure** +1. In the vSphere Web Client, right-click a host in the vCenter Server inventory, select **Deploy OVF template** + ![Screenshot of Deploy OVF template](img/ovainstall/DeployOVFmenu.png) +2. Select template: navigate to the OVA file or input the URL of the ova file in URL field. + ![Screenshot of Import ova](img/ovainstall/importova.png) +3. Follow the installer prompts to perform basic configuration of the appliance and to select the vSphere resources for it to use. + - Accept or modify the appliance name. + - Select the destination datacenter or folder: + ![Screenshot of appliance name](img/ovainstall/namelocation.png) + - Select the destination host, cluster, or resource pool: + ![Screenshot of resource pool](img/ovainstall/resource.png) + - Select the disk format and the destination datastore: + ![Screenshot of datastore](img/ovainstall/datastore.png) + - Select the network that the appliance connects to: + ![Screenshot of network](img/ovainstall/network.png) + +4. On the **Customize template** page, configure Harbor’s SSL certificates. There are two options for SSL: auto-generated certificate and customized certificate. + - Auto-generated certificate. Leave blank the fields of CA Certificate, Server Certificate and Server Key. Go to Step 5. + - Customized certificate. If you need to customize Harbor CA Certificate, Server Certificate and Server Key, copy and paste in the content of those files into the corresponding text boxes. Remember to include all content of the files. Because the Harbor OVA is launched with Full Qualified Domain Name (FQDN), the certificate should be generated with FQDN of the host. The host should be configured with the same FQDN in Step 7. + ![Screenshot of customize cert](img/ovainstall/custom_cert.png) + +5. In the section of Harbor Configuration, select the Authentication Mode and set the Administrator Password. If Authentication Mode is set to ldap_auth, LDAP configuration in the next section is required. **Note:** The ldap_auth mode is for both LDAP server and Active Directory. + ![Screenshot of customizing harbor](img/ovainstall/customizeharbor.png) + If the Authentication Mode is set to ldap_auth, you need to configure Harbor's LDAP settings: + - LDAP Base DN: The base DN to look up users. + - LDAP UID: The attribute to match a user, such as uid, cn, email or other attributes. + - LDAP URL: The URL for LDAP endpoint. + - Search DN: The user's DN who has the permission to search the LDAP server. + - Search DN Password: The password for search DN. + - Search Filter: The filter to search users. + - Search Scope: The scope to search users. + ![Screenshot of customizing LDAP](img/ovainstall/customizeldap.png) + Refer to **[Harbor's Installation Guide](installation_guide.md)** for more information about these settings. + + +6. On the **Customize template** page, under **System**, set the root password for the appliance VM and the option for **Permit Root Login**. + + Setting the root password for the appliance is mandatory. + + - If you want to have SSH access to the Harbor appliance for troubleshooting, set **Permit Root Login** to true. + ![Screenshot of customizing template system](img/ovainstall/system.png) + +7. Expand **Networking Properties** and optionally configure a static IP address for the appliance VM. + + - To use DHCP, leave the **Networking Properties** blank. + - If a customized SSL certificate is configured, you need to configure Domain Name the same as the FQDN of the certificate in Step 4. + ![Screenshot of network detail](img/ovainstall/network2.png) + **IMPORTANT**: If you set a static IP address for the appliance, use spaces to separate DNS servers. Do not use comma separation for DNS servers. + +8. When the deployment completes, refresh the current page and power on the appliance VM. It will take several minutes after powering on as it needs to load Docker images.  + + ![Screenshot of power on](img/ovainstall/poweron.png) + + Go to the **Summary** tab of the appliance VM and note the DNS Name. + +9. (Optional) If you provided a static network configuration, view the network status of the appliance. + + 1. In the **Summary** tab of the appliance VM, launch the VM console. + 2. In the VM console, press the right arrow key. + + The network status shows whether the network settings that you provided during the deployment match the settings with which the appliance is running. If there are mismatches, power off the appliance and select **Edit Settings** > **vApp Options** to correct the network settings. + +10. In a browser, go to https://*<DNS Name>*. The *<DNS Name>* is noted in Step 7. When prompted, enter the username admin and the password of admin set in Step 4.  + ![Screenshot of login harbor](img/ovainstall/login.png) + + If everything worked properly, you should see the administration console. Refer to **[Harbor User Guide](user_guide.md)** for how to use Harbor. + + ![Screenshot of after login](img/ovainstall/afterlogin.png) \ No newline at end of file diff --git a/docs/installation_guide.md b/docs/installation_guide.md index 1e11f5208..edd4d6644 100644 --- a/docs/installation_guide.md +++ b/docs/installation_guide.md @@ -1,10 +1,11 @@ # Installation and Configuration Guide -Harbor can be installed by one of two approaches: +Harbor can be installed by one of three approaches: - **Online installer:** The installer downloads Harbor's images from Docker hub. For this reason, the installer is very small in size. - **Offline installer:** Use this installer when the host does not have an Internet connection. The installer contains pre-built images so its size is larger. +- **OVA installer:** Use this installer when user have a vCenter environment, Harbor is launched after OVA deployed. Detail information please refer **[Harbor OVA install guide](install_guide_ova.md)** All installers can be downloaded from the **[official release](https://github.com/vmware/harbor/releases)** page. diff --git a/docs/kubernetes_deployment.md b/docs/kubernetes_deployment.md index 5c41b5513..f2c5e36d6 100644 --- a/docs/kubernetes_deployment.md +++ b/docs/kubernetes_deployment.md @@ -4,7 +4,7 @@ This Document decribes how to deploy Harbor on Kubernetes. It has been verified ### Prerequisite -* You should have domain knowledge about Kubernetes (Replication Controller, Service, Persistent Volume, Persistent Volume Claim, Config Map). +* You should have domain knowledge about Kubernetes (Deployment, Service, Persistent Volume, Persistent Volume Claim, Config Map, Ingress). * **Optional**: Load the docker images onto woker nodes. *If you skip this step, worker node will pull images from Docker Hub when starting the pods.* * Download the offline installer of Harbor v1.2.0 from the [release](https://github.com/vmware/harbor/releases) page. * Uncompress the offline installer and get the images tgz file harbor.*.tgz, transfer it to each of the worker nodes. @@ -34,23 +34,8 @@ These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kube #To accept access from outside of Kubernetes cluster, it should be set to a worker node. hostname = 10.192.168.5 ``` -- `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 - spec: - ports: - - name: http - port: 80 - selector: - name: nginx-apps - externalIPs: - - 10.192.168.5 - ``` - -- `make/kubernetes/**/*.rc.yaml`: Specify configs of containers. +- `make/kubernetes/**/*.svc.yaml`: Specify the service of pods. +- `make/kubernetes/**/*.deploy.yaml`: Specify configs of containers. - `make/kubernetes/pv/*.pvc.yaml`: Persistent Volume Claim. You can set capacity of storage in these files. example: @@ -91,10 +76,10 @@ These files will be generated: - make/kubernetes/jobservice/jobservice.cm.yaml - make/kubernetes/mysql/mysql.cm.yaml -- make/kubernetes/nginx/nginx.cm.yaml - make/kubernetes/registry/registry.cm.yaml - make/kubernetes/ui/ui.cm.yaml - make/kubernetes/adminserver/adminserver.cm.yaml +- make/kubernetes/ingress.yaml #### Advanced Configuration If Basic Configuration was not covering your requirements, you can read this section for more details. @@ -108,7 +93,7 @@ You can find all configs of Harbor in `make/kubernetes/templates/`. There are sp - `jobservice.cm.yaml`: ENV and web config of jobservice - `mysql.cm.yaml`: Root passowrd of MySQL -- `nginx.cm.yaml`: Https certification and nginx config. If you are fimiliar with nginx, you can modify it. +- `ingress.yaml`: Https certification and ingress config. If you are fimiliar with ingress, you can modify it. - `registry.cm.yaml`: Token service certification and registry config Registry use filesystem to store data of images. You can find it like: @@ -140,7 +125,6 @@ kubectl apply -f make/kubernetes/pv/storage.pvc.yaml # create config map kubectl apply -f make/kubernetes/jobservice/jobservice.cm.yaml kubectl apply -f make/kubernetes/mysql/mysql.cm.yaml -kubectl apply -f make/kubernetes/nginx/nginx.cm.yaml kubectl apply -f make/kubernetes/registry/registry.cm.yaml kubectl apply -f make/kubernetes/ui/ui.cm.yaml kubectl apply -f make/kubernetes/adminserver/adminserver.cm.yaml @@ -148,23 +132,24 @@ kubectl apply -f make/kubernetes/adminserver/adminserver.cm.yaml # create service kubectl apply -f make/kubernetes/jobservice/jobservice.svc.yaml kubectl apply -f make/kubernetes/mysql/mysql.svc.yaml -kubectl apply -f make/kubernetes/nginx/nginx.svc.yaml kubectl apply -f make/kubernetes/registry/registry.svc.yaml kubectl apply -f make/kubernetes/ui/ui.svc.yaml kubectl apply -f make/kubernetes/adminserver/adminserver.svc.yaml -# create k8s rc -kubectl apply -f make/kubernetes/registry/registry.rc.yaml -kubectl apply -f make/kubernetes/mysql/mysql.rc.yaml -kubectl apply -f make/kubernetes/jobservice/jobservice.rc.yaml -kubectl apply -f make/kubernetes/ui/ui.rc.yaml -kubectl apply -f make/kubernetes/nginx/nginx.rc.yaml -kubectl apply -f make/kubernetes/adminserver/adminserver.rc.yaml +# create k8s deployment +kubectl apply -f make/kubernetes/registry/registry.deploy.yaml +kubectl apply -f make/kubernetes/mysql/mysql.deploy.yaml +kubectl apply -f make/kubernetes/jobservice/jobservice.deploy.yaml +kubectl apply -f make/kubernetes/ui/ui.deploy.yaml +kubectl apply -f make/kubernetes/adminserver/adminserver.deploy.yaml + +# create k8s ingress +kubectl apply -f make/kubernetes/ingress.yaml ``` After the pods are running, you can access Harbor's UI via the configured endpoint `10.192.168.5` or issue docker commands such as `docker login 10.192.168.5` to interact with the registry. #### 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 +1. Current deployment is http only, to enable https you need to either add another layer of proxy or modify the ingress.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 future, stay tuned. diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env index cfcd1b591..54118c1a2 100644 --- a/make/common/templates/adminserver/env +++ b/make/common/templates/adminserver/env @@ -11,7 +11,7 @@ LDAP_FILTER=$ldap_filter LDAP_UID=$ldap_uid LDAP_SCOPE=$ldap_scope LDAP_TIMEOUT=$ldap_timeout -LDAP_VERIFY_CERT=true +LDAP_VERIFY_CERT=$ldap_verify_cert DATABASE_TYPE=mysql MYSQL_HOST=$db_host MYSQL_PORT=$db_port @@ -39,10 +39,15 @@ GODEBUG=netdns=cgo ADMIRAL_URL=$admiral_url WITH_NOTARY=$with_notary WITH_CLAIR=$with_clair -CLAIR_DB_PASSWORD=$pg_password +CLAIR_DB_PASSWORD=$clair_db_password +CLAIR_DB_HOST=$clair_db_host +CLAIR_DB_PORT=$clair_db_port +CLAIR_DB_USERNAME=$clair_db_username +CLAIR_DB=$clair_db RESET=false UAA_ENDPOINT=$uaa_endpoint UAA_CLIENTID=$uaa_clientid UAA_CLIENTSECRET=$uaa_clientsecret +UAA_VERIFY_CERT=$uaa_verify_cert UI_URL=http://ui:8080 JOBSERVICE_URL=http://jobservice:8080 diff --git a/make/common/templates/clair/config.yaml b/make/common/templates/clair/config.yaml index c09dd2585..c5219afd0 100644 --- a/make/common/templates/clair/config.yaml +++ b/make/common/templates/clair/config.yaml @@ -2,7 +2,7 @@ clair: database: type: pgsql options: - source: postgresql://postgres:$password@postgres:5432?sslmode=disable + source: postgresql://$username:$password@$host:$port?sslmode=disable # Number of elements kept in the cache # Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database. diff --git a/make/dev/nodeclarity/Dockerfile b/make/dev/nodeclarity/Dockerfile index d4268c0d3..e80eb52f8 100644 --- a/make/dev/nodeclarity/Dockerfile +++ b/make/dev/nodeclarity/Dockerfile @@ -6,6 +6,12 @@ RUN mkdir -p /harbor_src COPY src/ui_ng/package.json /harbor_resources COPY make/dev/nodeclarity/entrypoint.sh / +# Install Chrome +RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - +RUN echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list +RUN apt-get update && apt-get -y install google-chrome-stable + +# Install npm package WORKDIR /harbor_resources RUN npm __proxy__ install -g @angular/cli && \ diff --git a/make/harbor.cfg b/make/harbor.cfg index 8895943ca..8df07e977 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -95,6 +95,9 @@ ldap_scope = 3 #Timeout (in seconds) when connecting to an LDAP Server. The default value (and most reasonable) is 5 seconds. ldap_timeout = 5 +#Verify certificate from LDAP server +ldap_verify_cert = true + #Turn on or off the self-registration feature self_registration = on @@ -107,7 +110,7 @@ token_expiration = 30 project_creation_restriction = everyone #The follow configurations are for Harbor HA mode only - +##################################################### #the address of the mysql database. db_host = mysql @@ -118,10 +121,25 @@ db_port = 3306 db_user = root #The redis server address redis_url = + +#Clair DB host address +clair_db_host = postgres + +#Clair DB connect port +clair_db_port = 5432 + +#Clair DB username +clair_db_username = postgres + +#Clair default database +clair_db = postgres + + +################### end of HA section ##################### #************************END INITIAL PROPERTIES************************ #The following attributes only need to be set when auth mode is uaa_auth uaa_endpoint = uaa.mydomain.org -uaa_clientid= id -uaa_clientsecret= secret -uaa_ca_root= /path/to/uaa_ca.pem +uaa_clientid = id +uaa_clientsecret = secret +uaa_verify_cert = true ############# diff --git a/make/kubernetes/adminserver/adminserver.rc.yaml b/make/kubernetes/adminserver/adminserver.deploy.yaml similarity index 98% rename from make/kubernetes/adminserver/adminserver.rc.yaml rename to make/kubernetes/adminserver/adminserver.deploy.yaml index 70fd5a89a..e1e2efaa3 100644 --- a/make/kubernetes/adminserver/adminserver.rc.yaml +++ b/make/kubernetes/adminserver/adminserver.deploy.yaml @@ -1,13 +1,11 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: adminserver-rc + name: adminserver labels: - name: adminserver-rc + name: adminserver spec: replicas: 1 - selector: - name: adminserver-apps template: metadata: labels: diff --git a/make/kubernetes/jobservice/jobservice.rc.yaml b/make/kubernetes/jobservice/jobservice.deploy.yaml similarity index 92% rename from make/kubernetes/jobservice/jobservice.rc.yaml rename to make/kubernetes/jobservice/jobservice.deploy.yaml index e4f617c4f..4e18db0e5 100644 --- a/make/kubernetes/jobservice/jobservice.rc.yaml +++ b/make/kubernetes/jobservice/jobservice.deploy.yaml @@ -1,13 +1,11 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: jobservice-rc + name: jobservice labels: - name: jobservice-rc + name: jobservice spec: replicas: 1 - selector: - name: jobservice-apps template: metadata: labels: diff --git a/make/kubernetes/k8s-prepare b/make/kubernetes/k8s-prepare index b4fc8a643..a8c15a661 100755 --- a/make/kubernetes/k8s-prepare +++ b/make/kubernetes/k8s-prepare @@ -210,6 +210,7 @@ output_dir = base_dir generate_template(os.path.join(template_dir, 'ui.cm.yaml'), os.path.join(output_dir, 'ui/ui.cm.yaml')) generate_template(os.path.join(template_dir, 'jobservice.cm.yaml'), os.path.join(output_dir, 'jobservice/jobservice.cm.yaml')) generate_template(os.path.join(template_dir, 'mysql.cm.yaml'), os.path.join(output_dir, 'mysql/mysql.cm.yaml')) -generate_template(os.path.join(template_dir, 'nginx.cm.yaml'), os.path.join(output_dir, 'nginx/nginx.cm.yaml')) generate_template(os.path.join(template_dir, 'registry.cm.yaml'), os.path.join(output_dir, 'registry/registry.cm.yaml')) generate_template(os.path.join(template_dir, 'adminserver.cm.yaml'), os.path.join(output_dir, 'adminserver/adminserver.cm.yaml')) +generate_template(os.path.join(template_dir, 'ingress.yaml'), os.path.join(output_dir, 'ingress.yaml')) + diff --git a/make/kubernetes/mysql/mysql.rc.yaml b/make/kubernetes/mysql/mysql.deploy.yaml similarity index 85% rename from make/kubernetes/mysql/mysql.rc.yaml rename to make/kubernetes/mysql/mysql.deploy.yaml index 78592a5f8..8d1a2809b 100644 --- a/make/kubernetes/mysql/mysql.rc.yaml +++ b/make/kubernetes/mysql/mysql.deploy.yaml @@ -1,13 +1,11 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: mysql-rc + name: mysql labels: - name: mysql-rc + name: mysql spec: replicas: 1 - selector: - name: mysql-apps template: metadata: labels: diff --git a/make/kubernetes/nginx/nginx.rc.yaml b/make/kubernetes/nginx/nginx.rc.yaml deleted file mode 100644 index fd81b1671..000000000 --- a/make/kubernetes/nginx/nginx.rc.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: v1 -kind: ReplicationController -metadata: - name: nginx-rc - labels: - name: nginx-rc -spec: - replicas: 1 - selector: - name: nginx-apps - template: - metadata: - labels: - name: nginx-apps - spec: - containers: - - name: nginx-app - image: vmware/nginx-photon:1.11.13 - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - - containerPort: 443 - volumeMounts: - - name: config - mountPath: /etc/nginx - volumes: - - name: config - configMap: - name: harbor-nginx-config - items: - - key: config - path: nginx.conf - - key: pkey - path: https.key - - key: cert - path: https.crt diff --git a/make/kubernetes/nginx/nginx.svc.yaml b/make/kubernetes/nginx/nginx.svc.yaml deleted file mode 100644 index 9749dad96..000000000 --- a/make/kubernetes/nginx/nginx.svc.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: nginx -spec: - ports: - - name: http - port: 80 - selector: - name: nginx-apps -# Set the external IP to an IP of the cluster node, so that the service can be accessed from outside the kubernetes cluster. -# externalIPs: -# - 10.192.168.5 diff --git a/make/kubernetes/registry/registry.rc.yaml b/make/kubernetes/registry/registry.deploy.yaml similarity index 81% rename from make/kubernetes/registry/registry.rc.yaml rename to make/kubernetes/registry/registry.deploy.yaml index 53753b548..aadb2bf78 100644 --- a/make/kubernetes/registry/registry.rc.yaml +++ b/make/kubernetes/registry/registry.deploy.yaml @@ -1,13 +1,11 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: registry-rc + name: registry labels: - name: registry-rc + name: registry spec: replicas: 1 - selector: - name: registry-apps template: metadata: labels: @@ -22,7 +20,7 @@ spec: - containerPort: 5001 volumeMounts: - name: config - mountPath: /etc/docker/registry + mountPath: /etc/registry - name: storage mountPath: /storage volumes: diff --git a/make/kubernetes/templates/ingress.yaml b/make/kubernetes/templates/ingress.yaml new file mode 100644 index 000000000..5ad84da5c --- /dev/null +++ b/make/kubernetes/templates/ingress.yaml @@ -0,0 +1,22 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: harbor +spec: + rules: + - host: {{hostname}} + http: + paths: + - path: / + backend: + serviceName: ui + servicePort: 80 + - path: /v2 + backend: + serviceName: registry + servicePort: repo + - path: /service + backend: + serviceName: ui + servicePort: 80 + diff --git a/make/kubernetes/templates/nginx.cm.yaml b/make/kubernetes/templates/nginx.cm.yaml deleted file mode 100644 index f0430d540..000000000 --- a/make/kubernetes/templates/nginx.cm.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: harbor-nginx-config -data: - config: | - worker_processes auto; - - events { - worker_connections 1024; - use epoll; - multi_accept on; - } - - http { - tcp_nodelay on; - - # this is necessary for us to be able to disable request buffering in all cases - proxy_http_version 1.1; - - - upstream registry { - server registry:5000; - } - - upstream ui { - server ui:80; - } - server { - listen 80; - server_name {{hostname}}; - - # disable any limits to avoid HTTP 413 for large image uploads - client_max_body_size 0; - - # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) - chunked_transfer_encoding on; - - # rewrite ^/(.*) https://$server_name:443/$1 permanent; - - location / { - proxy_pass http://ui/; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings. - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_buffering off; - proxy_request_buffering off; - } - - location /v1/ { - return 404; - } - - location /v2/ { - proxy_pass http://registry/v2/; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings. - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_buffering off; - proxy_request_buffering off; - - } - - location /service/ { - proxy_pass http://ui/service/; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings. - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_buffering off; - proxy_request_buffering off; - } - } - } - pkey: | - {{4 https_pkey}} - cert: | - {{4 https_cert}} diff --git a/make/kubernetes/templates/registry.cm.yaml b/make/kubernetes/templates/registry.cm.yaml index ca7e4f0a5..7184d88f4 100644 --- a/make/kubernetes/templates/registry.cm.yaml +++ b/make/kubernetes/templates/registry.cm.yaml @@ -28,7 +28,7 @@ data: token: issuer: harbor-token-issuer realm: {{ui_url}}/service/token - rootcertbundle: /etc/docker/registry/root.crt + rootcertbundle: /etc/registry/root.crt service: harbor-registry notifications: endpoints: diff --git a/make/kubernetes/ui/ui.rc.yaml b/make/kubernetes/ui/ui.deploy.yaml similarity index 93% rename from make/kubernetes/ui/ui.rc.yaml rename to make/kubernetes/ui/ui.deploy.yaml index 808fa5b00..50c8d190a 100644 --- a/make/kubernetes/ui/ui.rc.yaml +++ b/make/kubernetes/ui/ui.deploy.yaml @@ -1,13 +1,11 @@ -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - name: ui-rc + name: ui labels: - name: ui-rc + name: ui spec: replicas: 1 - selector: - name: ui-apps template: metadata: labels: diff --git a/make/photon/clair/Dockerfile b/make/photon/clair/Dockerfile index 6bcde5f33..b62cc9e67 100644 --- a/make/photon/clair/Dockerfile +++ b/make/photon/clair/Dockerfile @@ -1,7 +1,7 @@ FROM vmware/photon:1.0 -RUN tdnf distro-sync -y || echo \ +RUN tdnf distro-sync -y \ && tdnf erase vim -y \ && tdnf install -y git shadow sudo bzr rpm xz python-xml \ && tdnf clean all \ diff --git a/make/photon/log/Dockerfile b/make/photon/log/Dockerfile index 18e33f457..1db6314b2 100644 --- a/make/photon/log/Dockerfile +++ b/make/photon/log/Dockerfile @@ -1,6 +1,6 @@ FROM vmware/photon:1.0 -RUN tdnf distro-sync -y || echo \ +RUN tdnf distro-sync -y \ && tdnf install -y cronie rsyslog logrotate shadow tar gzip sudo net-tools\ && mkdir /etc/rsyslog.d/ \ && mkdir /var/spool/rsyslog \ diff --git a/make/photon/mariadb/Dockerfile b/make/photon/mariadb/Dockerfile index d857810a5..a8cbb8821 100644 --- a/make/photon/mariadb/Dockerfile +++ b/make/photon/mariadb/Dockerfile @@ -2,7 +2,7 @@ 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 10000 mysql && useradd --no-log-init -r -g 10000 -u 10000 mysql \ && tdnf install -y mariadb-server mariadb \ diff --git a/make/photon/nginx/Dockerfile b/make/photon/nginx/Dockerfile index 1c85bb5e6..9f6bd4353 100644 --- a/make/photon/nginx/Dockerfile +++ b/make/photon/nginx/Dockerfile @@ -1,6 +1,6 @@ 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 \ diff --git a/make/photon/notary/server.Dockerfile b/make/photon/notary/server.Dockerfile index 9519dc412..bd9ff8db0 100644 --- a/make/photon/notary/server.Dockerfile +++ b/make/photon/notary/server.Dockerfile @@ -1,6 +1,6 @@ FROM vmware/photon:1.0 -RUN tdnf distro-sync -y || echo \ +RUN tdnf distro-sync -y \ && tdnf erase vim -y \ && tdnf install -y shadow sudo \ && tdnf clean all \ diff --git a/make/photon/notary/signer.Dockerfile b/make/photon/notary/signer.Dockerfile index 34544dfad..4977caf55 100644 --- a/make/photon/notary/signer.Dockerfile +++ b/make/photon/notary/signer.Dockerfile @@ -1,6 +1,6 @@ FROM vmware/photon:1.0 -RUN tdnf distro-sync -y || echo \ +RUN tdnf distro-sync -y \ && tdnf erase vim -y \ && tdnf install -y shadow sudo \ && tdnf clean all \ diff --git a/make/photon/postgresql/Dockerfile b/make/photon/postgresql/Dockerfile index f849234d4..37fbc1095 100644 --- a/make/photon/postgresql/Dockerfile +++ b/make/photon/postgresql/Dockerfile @@ -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 \ diff --git a/make/photon/registry/Dockerfile b/make/photon/registry/Dockerfile index 1b655f227..2e17a8447 100644 --- a/make/photon/registry/Dockerfile +++ b/make/photon/registry/Dockerfile @@ -3,7 +3,7 @@ FROM vmware/photon:1.0 MAINTAINER wangyan@vmware.com # The original script in the docker offical registry image. -RUN tdnf distro-sync -y || echo \ +RUN tdnf distro-sync -y \ && tdnf erase vim -y \ && tdnf install sudo -y \ && tdnf clean all \ diff --git a/make/prepare b/make/prepare index 407ddb32f..226fcc707 100755 --- a/make/prepare +++ b/make/prepare @@ -217,6 +217,7 @@ else: ldap_uid = rcp.get("configuration", "ldap_uid") ldap_scope = rcp.get("configuration", "ldap_scope") ldap_timeout = rcp.get("configuration", "ldap_timeout") +ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert") db_password = rcp.get("configuration", "db_password") db_host = rcp.get("configuration", "db_host") db_user = rcp.get("configuration", "db_user") @@ -234,11 +235,16 @@ if rcp.has_option("configuration", "admiral_url"): admiral_url = rcp.get("configuration", "admiral_url") else: admiral_url = "" -pg_password = rcp.get("configuration", "clair_db_password") +clair_db_password = rcp.get("configuration", "clair_db_password") +clair_db_host = rcp.get("configuration", "clair_db_host") +clair_db_port = rcp.get("configuration", "clair_db_port") +clair_db_username = rcp.get("configuration", "clair_db_username") +clair_db = rcp.get("configuration", "clair_db") + uaa_endpoint = rcp.get("configuration", "uaa_endpoint") uaa_clientid = rcp.get("configuration", "uaa_clientid") uaa_clientsecret = rcp.get("configuration", "uaa_clientsecret") -uaa_ca_root = rcp.get("configuration", "uaa_ca_root") +uaa_verify_cert = rcp.get("configuration", "uaa_verify_cert") secret_key = get_secret_key(secretkey_path) log_rotate_count = rcp.get("configuration", "log_rotate_count") @@ -291,12 +297,6 @@ if protocol == "https": else: render(os.path.join(templates_dir, "nginx", "nginx.http.conf"), nginx_conf) - -if auth_mode == "uaa_auth": - if os.path.isfile(uaa_ca_root): - shutil.copy2(uaa_ca_root, os.path.join(ui_certificates_dir, "uaa_ca.pem")) - else: - raise Exception("Error: Invalid path for uaa ca root: %s" % uaa_ca_root) render(os.path.join(templates_dir, "adminserver", "env"), adminserver_conf_env, @@ -310,6 +310,7 @@ render(os.path.join(templates_dir, "adminserver", "env"), ldap_filter=ldap_filter, ldap_uid=ldap_uid, ldap_scope=ldap_scope, + ldap_verify_cert=ldap_verify_cert, ldap_timeout=ldap_timeout, db_password=db_password, db_host=db_host, @@ -332,10 +333,15 @@ render(os.path.join(templates_dir, "adminserver", "env"), admiral_url=admiral_url, with_notary=args.notary_mode, with_clair=args.clair_mode, - pg_password=pg_password, + clair_db_password=clair_db_password, + clair_db_host=clair_db_host, + clair_db_port=clair_db_port, + clair_db_username=clair_db_username, + clair_db=clair_db, uaa_endpoint=uaa_endpoint, uaa_clientid=uaa_clientid, - uaa_clientsecret=uaa_clientsecret + uaa_clientsecret=uaa_clientsecret, + uaa_verify_cert=uaa_verify_cert ) render(os.path.join(templates_dir, "ui", "env"), @@ -500,9 +506,14 @@ if args.clair_mode: shutil.rmtree(os.path.join(clair_config_dir, "postgresql-init.d")) shutil.copytree(os.path.join(clair_temp_dir, "postgresql-init.d"), os.path.join(clair_config_dir, "postgresql-init.d")) postgres_env = os.path.join(clair_config_dir, "postgres_env") - render(os.path.join(clair_temp_dir, "postgres_env"), postgres_env, password = pg_password) + render(os.path.join(clair_temp_dir, "postgres_env"), postgres_env, password = clair_db_password) clair_conf = os.path.join(clair_config_dir, "config.yaml") - render(os.path.join(clair_temp_dir, "config.yaml"), clair_conf, password = pg_password) + render(os.path.join(clair_temp_dir, "config.yaml"), + clair_conf, + password = clair_db_password, + username = clair_db_username, + host = clair_db_host, + port = clair_db_port) if args.ha_mode: prepare_ha(rcp, args) diff --git a/src/Gopkg.lock b/src/Gopkg.lock new file mode 100644 index 000000000..620abcfa4 --- /dev/null +++ b/src/Gopkg.lock @@ -0,0 +1,250 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/Sirupsen/logrus" + packages = ["."] + revision = "a283a10442df8dc09befd873fab202bf8a253d6a" + +[[projects]] + name = "github.com/Unknwon/goconfig" + packages = ["."] + revision = "5f601ca6ef4d5cea8d52be2f8b3a420ee4b574a5" + +[[projects]] + branch = "master" + name = "github.com/agl/ed25519" + packages = [ + ".", + "edwards25519" + ] + revision = "5312a61534124124185d41f09206b9fef1d88403" + +[[projects]] + name = "github.com/astaxie/beego" + packages = [ + ".", + "cache", + "config", + "context", + "grace", + "logs", + "orm", + "session", + "session/redis", + "toolbox", + "utils", + "validation" + ] + revision = "1aeb3d90512734def678c7aa9f612fe6f659e6b5" + version = "v1.6.1" + +[[projects]] + name = "github.com/beego/i18n" + packages = ["."] + revision = "e87155e8f0c05bf323d0b13470e1b97af0cb5652" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/dghubble/sling" + packages = ["."] + revision = "eb56e89ac5088bebb12eef3cb4b293300f43608b" + version = "v1.1.0" + +[[projects]] + name = "github.com/dgrijalva/jwt-go" + packages = ["."] + revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c" + version = "v3.0.0" + +[[projects]] + name = "github.com/docker/distribution" + packages = [ + ".", + "context", + "digest", + "manifest", + "manifest/schema1", + "manifest/schema2", + "reference", + "registry/auth", + "registry/auth/token", + "registry/client/auth/challenge", + "uuid" + ] + revision = "325b0804fef3a66309d962357aac3c2ce3f4d329" + version = "v2.6.0" + +[[projects]] + branch = "master" + name = "github.com/docker/go" + packages = ["canonical/json"] + revision = "d30aec9fd63c35133f8f79c3412ad91a3b08be06" + +[[projects]] + branch = "master" + name = "github.com/docker/libtrust" + packages = ["."] + revision = "aabc10ec26b754e797f9028f4589c5b7bd90dc20" + +[[projects]] + name = "github.com/docker/notary" + packages = [ + ".", + "client", + "client/changelist", + "cryptoservice", + "storage", + "trustmanager", + "trustmanager/yubikey", + "trustpinning", + "tuf", + "tuf/data", + "tuf/signed", + "tuf/utils", + "tuf/validation" + ] + revision = "c04e3e6d05881045def11167c51d4a8baa34899a" + +[[projects]] + name = "github.com/garyburd/redigo" + packages = [ + "internal", + "redis" + ] + revision = "47dc60e71eed504e3ef8e77ee3c6fe720f3be57f" + version = "v1.3.0" + +[[projects]] + name = "github.com/go-sql-driver/mysql" + packages = ["."] + revision = "a732e14c62dde3285440047bba97581bc472ae18" + version = "v1.2" + +[[projects]] + name = "github.com/golang/protobuf" + packages = ["proto"] + revision = "130e6b02ab059e7b717a096f397c5b60111cae74" + +[[projects]] + branch = "master" + name = "github.com/google/go-querystring" + packages = ["query"] + revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" + +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "aed02d124ae4a0e94fea4541c8effd05bf0c8296" + +[[projects]] + name = "github.com/gorilla/handlers" + packages = ["."] + revision = "13d73096a474cac93275c679c7b8a2dc17ddba82" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "780415097119f6f61c55475fe59b66f3c3e9ea53" + +[[projects]] + name = "github.com/lib/pq" + packages = [ + ".", + "oid" + ] + revision = "dd1fe2071026ce53f36a39112e645b4d4f5793a4" + +[[projects]] + name = "github.com/mattn/go-sqlite3" + packages = ["."] + revision = "3fb7a0e792edd47bf0cf1e919dfc14e2be412e15" + +[[projects]] + name = "github.com/miekg/pkcs11" + packages = ["."] + revision = "7283ca79f35edb89bc1b4ecae7f86a3680ce737f" + +[[projects]] + name = "github.com/opencontainers/go-digest" + packages = ["."] + revision = "aa2ec055abd10d26d539eb630a92241b781ce4bc" + version = "v1.0.0-rc0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = [ + "assert", + "require" + ] + revision = "4d4bfba8f1d1027c4fdbe371823030df51419987" + +[[projects]] + name = "golang.org/x/crypto" + packages = ["pbkdf2"] + revision = "5f961cd492ac9d43fc33a8ef646bae79d113fd97" + +[[projects]] + name = "golang.org/x/net" + packages = [ + "context", + "context/ctxhttp" + ] + revision = "075e191f18186a8ff2becaf64478e30f4545cdad" + +[[projects]] + name = "golang.org/x/oauth2" + packages = [ + ".", + "internal" + ] + revision = "bb50c06baba3d0c76f9d125c0719093e315b5b44" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix"] + revision = "571f7bbbe08da2a8955aed9d4db316e78630e9a3" + +[[projects]] + name = "google.golang.org/appengine" + packages = [ + "internal", + "internal/base", + "internal/datastore", + "internal/log", + "internal/remote_api", + "internal/urlfetch", + "urlfetch" + ] + revision = "24e4144ec923c2374f6b06610c0df16a9222c3d9" + +[[projects]] + name = "gopkg.in/asn1-ber.v1" + packages = ["."] + revision = "4e86f4367175e39f69d9358a5f17b4dda270378d" + version = "v1.1" + +[[projects]] + name = "gopkg.in/ldap.v2" + packages = ["."] + revision = "8168ee085ee43257585e50c6441aadf54ecb2c9f" + version = "v2.5.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "9c0f8cd26043afa12693fed0005998c7194c4ea77c8dae19c4363ebadfd600ef" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/src/Gopkg.toml b/src/Gopkg.toml new file mode 100644 index 000000000..836d90926 --- /dev/null +++ b/src/Gopkg.toml @@ -0,0 +1,53 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/astaxie/beego" + version = "1.6.1" + +[[constraint]] + name = "github.com/dghubble/sling" + version = "1.1.0" + +[[constraint]] + name = "github.com/dgrijalva/jwt-go" + version = "3.0.0" + +[[constraint]] + name = "github.com/docker/distribution" + version = "2.6.0" + +[[constraint]] + branch = "master" + name = "github.com/docker/libtrust" + +[[constraint]] + name = "github.com/go-sql-driver/mysql" + version = "1.2.0" + +[[constraint]] + name = "github.com/opencontainers/go-digest" + version = "1.0.0-rc0" + +[[constraint]] + name = "gopkg.in/ldap.v2" + version = "2.5.0" diff --git a/src/adminserver/systemcfg/store/database/driver_db.go b/src/adminserver/systemcfg/store/database/driver_db.go index db97ab90b..85d76e213 100644 --- a/src/adminserver/systemcfg/store/database/driver_db.go +++ b/src/adminserver/systemcfg/store/database/driver_db.go @@ -15,36 +15,40 @@ package database import ( - "github.com/vmware/harbor/src/common/dao" - "github.com/vmware/harbor/src/common/models" + "fmt" "github.com/vmware/harbor/src/adminserver/systemcfg/store" "github.com/vmware/harbor/src/common" - "fmt" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "strconv" ) const ( name = "database" - ) -var( +) + +var ( numKeys = map[string]bool{ - common.EmailPort:true, - common.LDAPScope:true, - common.LDAPTimeout:true, - common.TokenExpiration:true, - common.MySQLPort:true, - common.MaxJobWorkers:true, - common.CfgExpiration:true, - } - boolKeys = map[string]bool{ - common.WithClair:true, - common.WithNotary:true, - common.SelfRegistration:true, - common.EmailSSL:true, - common.EmailInsecure:true, - common.LDAPVerifyCert:true, + common.EmailPort: true, + common.LDAPScope: true, + common.LDAPTimeout: true, + common.TokenExpiration: true, + common.MySQLPort: true, + common.MaxJobWorkers: true, + common.CfgExpiration: true, + common.ClairDBPort: true, } - ) + boolKeys = map[string]bool{ + common.WithClair: true, + common.WithNotary: true, + common.SelfRegistration: true, + common.EmailSSL: true, + common.EmailInsecure: true, + common.LDAPVerifyCert: true, + common.UAAVerifyCert: true, + } +) + type cfgStore struct { name string } @@ -55,14 +59,15 @@ func (c *cfgStore) Name() string { } // NewCfgStore New a cfg store for database driver -func NewCfgStore() (store.Driver, error){ +func NewCfgStore() (store.Driver, error) { return &cfgStore{ name: name, }, nil } + // Read configuration from database func (c *cfgStore) Read() (map[string]interface{}, error) { - configEntries,error := dao.GetConfigEntries() + configEntries, error := dao.GetConfigEntries() if error != nil { return nil, error } @@ -70,53 +75,54 @@ func (c *cfgStore) Read() (map[string]interface{}, error) { } // WrapperConfig Wrapper the configuration -func WrapperConfig (configEntries []*models.ConfigEntry) (map[string]interface{}, error) { +func WrapperConfig(configEntries []*models.ConfigEntry) (map[string]interface{}, error) { config := make(map[string]interface{}) - for _,entry := range configEntries{ - if numKeys[entry.Key]{ + for _, entry := range configEntries { + if numKeys[entry.Key] { strvalue, err := strconv.Atoi(entry.Value) if err != nil { return nil, err } config[entry.Key] = float64(strvalue) - }else if boolKeys[entry.Key] { + } else if boolKeys[entry.Key] { strvalue, err := strconv.ParseBool(entry.Value) if err != nil { return nil, err } - config[entry.Key]=strvalue - }else{ + config[entry.Key] = strvalue + } else { config[entry.Key] = entry.Value } } return config, nil } + // Write save configuration to database func (c *cfgStore) Write(config map[string]interface{}) error { - configEntries ,_:= TranslateConfig(config) + configEntries, _ := TranslateConfig(config) return dao.SaveConfigEntries(configEntries) } // TranslateConfig Translate configuration from int, bool, float64 to string -func TranslateConfig(config map[string]interface{}) ([]models.ConfigEntry,error) { +func TranslateConfig(config map[string]interface{}) ([]models.ConfigEntry, error) { var configEntries []models.ConfigEntry for k, v := range config { var entry = new(models.ConfigEntry) entry.Key = k switch v.(type) { case string: - entry.Value=v.(string) + entry.Value = v.(string) case int: - entry.Value=strconv.Itoa(v.(int)) + entry.Value = strconv.Itoa(v.(int)) case bool: - entry.Value=strconv.FormatBool(v.(bool)) + entry.Value = strconv.FormatBool(v.(bool)) case float64: - entry.Value=strconv.Itoa(int(v.(float64))) + entry.Value = strconv.Itoa(int(v.(float64))) default: return nil, fmt.Errorf("unknown type %v", v) } - configEntries = append(configEntries,*entry) + configEntries = append(configEntries, *entry) } - return configEntries,nil + return configEntries, nil } diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go index 3765e5259..513731f08 100644 --- a/src/adminserver/systemcfg/systemcfg.go +++ b/src/adminserver/systemcfg/systemcfg.go @@ -22,14 +22,14 @@ import ( enpt "github.com/vmware/harbor/src/adminserver/systemcfg/encrypt" "github.com/vmware/harbor/src/adminserver/systemcfg/store" + "github.com/vmware/harbor/src/adminserver/systemcfg/store/database" "github.com/vmware/harbor/src/adminserver/systemcfg/store/encrypt" + "github.com/vmware/harbor/src/adminserver/systemcfg/store/json" "github.com/vmware/harbor/src/common" comcfg "github.com/vmware/harbor/src/common/config" - "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/adminserver/systemcfg/store/database" - "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/dao" - "github.com/vmware/harbor/src/adminserver/systemcfg/store/json" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" ) const ( @@ -130,11 +130,19 @@ var ( parse: parseStringToBool, }, common.ClairDBPassword: "CLAIR_DB_PASSWORD", + common.ClairDB: "CLAIR_DB", + common.ClairDBUsername: "CLAIR_DB_USERNAME", + common.ClairDBHost: "CLAIR_DB_HOST", + common.ClairDBPort: "CLAIR_DB_PORT", common.UAAEndpoint: "UAA_ENDPOINT", common.UAAClientID: "UAA_CLIENTID", common.UAAClientSecret: "UAA_CLIENTSECRET", - common.UIURL: "UI_URL", - common.JobServiceURL: "JOBSERVICE_URL", + common.UAAVerifyCert: &parser{ + env: "UAA_VERIFY_CERT", + parse: parseStringToBool, + }, + common.UIURL: "UI_URL", + common.JobServiceURL: "JOBSERVICE_URL", } // configurations need read from environment variables @@ -163,6 +171,10 @@ var ( common.UAAEndpoint: "UAA_ENDPOINT", common.UAAClientID: "UAA_CLIENTID", common.UAAClientSecret: "UAA_CLIENTSECRET", + common.UAAVerifyCert: &parser{ + env: "UAA_VERIFY_CERT", + parse: parseStringToBool, + }, } ) @@ -262,7 +274,7 @@ func initCfgStore() (err error) { } err = CfgStore.Write(jsonconfig) if err != nil { - log.Error("Failed to update old configuration to dattabase") + log.Error("Failed to update old configuration to database") return err } } @@ -327,7 +339,7 @@ func LoadFromEnv(cfgs map[string]interface{}, all bool) error { } // GetDatabaseFromCfg Create database object from config -func GetDatabaseFromCfg(cfg map[string]interface{}) (*models.Database){ +func GetDatabaseFromCfg(cfg map[string]interface{}) *models.Database { database := &models.Database{} database.Type = cfg[common.DatabaseType].(string) mysql := &models.MySQL{} diff --git a/src/common/api/base.go b/src/common/api/base.go index 57147d104..8d6de31fc 100644 --- a/src/common/api/base.go +++ b/src/common/api/base.go @@ -21,11 +21,8 @@ import ( "strconv" "github.com/astaxie/beego/validation" - "github.com/vmware/harbor/src/common/dao" - "github.com/vmware/harbor/src/common/models" http_error "github.com/vmware/harbor/src/common/utils/error" "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/ui/auth" "github.com/astaxie/beego" ) @@ -122,7 +119,7 @@ func (b *BaseAPI) DecodeJSONReq(v interface{}) { err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v) if err != nil { log.Errorf("Error while decoding the json request, error: %v, %v", - err, string(b.Ctx.Input.CopyBody(1<<32)[:])) + err, string(b.Ctx.Input.CopyBody(1 << 32)[:])) b.CustomAbort(http.StatusBadRequest, "Invalid json request") } } @@ -151,59 +148,6 @@ func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) { b.Validate(v) } -// ValidateUser checks if the request triggered by a valid user -// TODO remove -func (b *BaseAPI) ValidateUser() int { - userID, needsCheck, ok := b.GetUserIDForRequest() - if !ok { - log.Warning("No user id in session, canceling request") - b.CustomAbort(http.StatusUnauthorized, "") - } - if needsCheck { - u, err := dao.GetUser(models.User{UserID: userID}) - if err != nil { - log.Errorf("Error occurred in GetUser, error: %v", err) - b.CustomAbort(http.StatusInternalServerError, "Internal error.") - } - if u == nil { - log.Warningf("User was deleted already, user id: %d, canceling request.", userID) - b.CustomAbort(http.StatusUnauthorized, "") - } - } - return userID -} - -// GetUserIDForRequest tries to get user ID from basic auth header and session. -// It returns the user ID, whether need further verification(when the id is from session) and if the action is successful -// TODO remove -func (b *BaseAPI) GetUserIDForRequest() (int, bool, bool) { - username, password, ok := b.Ctx.Request.BasicAuth() - if ok { - log.Infof("Requst with Basic Authentication header, username: %s", username) - user, err := auth.Login(models.AuthModel{ - Principal: username, - Password: password, - }) - if err != nil { - log.Errorf("Error while trying to login, username: %s, error: %v", username, err) - user = nil - } - if user != nil { - b.SetSession("userId", user.UserID) - b.SetSession("username", user.Username) - // User login successfully no further check required. - return user.UserID, false, true - } - } - sessionUserID, ok := b.GetSession("userId").(int) - if ok { - // The ID is from session - return sessionUserID, true, true - } - log.Debug("No valid user id in session.") - return 0, false, false -} - // Redirect does redirection to resource URI with http header status code. func (b *BaseAPI) Redirect(statusCode int, resouceID string) { requestURI := b.Ctx.Request.RequestURI diff --git a/src/common/const.go b/src/common/const.go index 308f89857..82be6df84 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -18,6 +18,7 @@ package common const ( DBAuth = "db_auth" LDAPAuth = "ldap_auth" + UAAAuth = "uaa_auth" ProCrtRestrEveryone = "everyone" ProCrtRestrAdmOnly = "adminonly" LDAPScopeBase = 1 @@ -70,10 +71,15 @@ const ( WithClair = "with_clair" ScanAllPolicy = "scan_all_policy" ClairDBPassword = "clair_db_password" + ClairDBHost = "clair_db_host" + ClairDBPort = "clair_db_port" + ClairDB = "clair_db" + ClairDBUsername = "clair_db_username" UAAEndpoint = "uaa_endpoint" UAAClientID = "uaa_client_id" UAAClientSecret = "uaa_client_secret" - DefaultClairEndpoint = "http://clair:6060" + UAAVerifyCert = "uaa_verify_cert" + DefaultClairEndpoint = "http://clair:6060" CfgDriverDB = "db" CfgDriverJSON = "json" ) diff --git a/src/common/dao/base.go b/src/common/dao/base.go index 1da44e0f2..97ee05fe3 100644 --- a/src/common/dao/base.go +++ b/src/common/dao/base.go @@ -43,20 +43,20 @@ type Database interface { } // InitClairDB ... -func InitClairDB(password string) error { +func InitClairDB(clairDB *models.PostGreSQL) error { //Except for password other information will not be configurable, so keep it hard coded for 1.2.0. p := &pgsql{ - host: "postgres", - port: 5432, - usr: "postgres", - pwd: password, - database: "postgres", + host: clairDB.Host, + port: clairDB.Port, + usr: clairDB.Username, + pwd: clairDB.Password, + database: clairDB.Database, sslmode: false, } if err := p.Register(ClairDBAlias); err != nil { return err } - log.Info("initialized clair databas") + log.Info("initialized clair database") return nil } @@ -116,6 +116,12 @@ func GetOrmer() orm.Ormer { func ClearTable(table string) error { o := GetOrmer() sql := fmt.Sprintf("delete from %s where 1=1", table) + if table == models.ProjectTable { + sql = fmt.Sprintf("delete from %s where project_id > 1", table) + } + if table == models.UserTable { + sql = fmt.Sprintf("delete from %s where user_id > 2", table) + } _, err := o.Raw(sql).Exec() return err } diff --git a/src/common/dao/dao_test.go b/src/common/dao/dao_test.go index c169f11ba..9be616be1 100644 --- a/src/common/dao/dao_test.go +++ b/src/common/dao/dao_test.go @@ -1067,7 +1067,7 @@ func TestAddRepJob(t *testing.T) { func TestUpdateRepJobStatus(t *testing.T) { err := UpdateRepJobStatus(jobID, models.JobFinished) if err != nil { - t.Errorf("Error occured in UpdateRepJobStatus, error: %v, id: %d", err, jobID) + t.Errorf("Error occurred in UpdateRepJobStatus, error: %v, id: %d", err, jobID) return } j, err := GetRepJob(jobID) @@ -1082,7 +1082,7 @@ func TestUpdateRepJobStatus(t *testing.T) { } err = UpdateRepJobStatus(jobID, models.JobPending) if err != nil { - t.Errorf("Error occured in UpdateRepJobStatus when update it back to status pending, error: %v, id: %d", err, jobID) + t.Errorf("Error occurred in UpdateRepJobStatus when update it back to status pending, error: %v, id: %d", err, jobID) return } } @@ -1090,7 +1090,7 @@ func TestUpdateRepJobStatus(t *testing.T) { func TestGetRepPolicyByProject(t *testing.T) { p1, err := GetRepPolicyByProject(99) if err != nil { - t.Errorf("Error occured in GetRepPolicyByProject:%v, project ID: %d", err, 99) + t.Errorf("Error occurred in GetRepPolicyByProject:%v, project ID: %d", err, 99) return } if len(p1) > 0 { @@ -1116,7 +1116,7 @@ func TestGetRepPolicyByProject(t *testing.T) { func TestGetRepJobByPolicy(t *testing.T) { jobs, err := GetRepJobByPolicy(999) if err != nil { - t.Errorf("Error occured in GetRepJobByPolicy: %v, policy ID: %d", err, 999) + t.Errorf("Error occurred in GetRepJobByPolicy: %v, policy ID: %d", err, 999) return } if len(jobs) > 0 { @@ -1125,7 +1125,7 @@ func TestGetRepJobByPolicy(t *testing.T) { } jobs, err = GetRepJobByPolicy(policyID) if err != nil { - t.Errorf("Error occured in GetRepJobByPolicy: %v, policy ID: %d", err, policyID) + t.Errorf("Error occurred in GetRepJobByPolicy: %v, policy ID: %d", err, policyID) return } if len(jobs) != 1 { @@ -1141,7 +1141,7 @@ func TestGetRepJobByPolicy(t *testing.T) { func TestFilterRepJobs(t *testing.T) { jobs, _, err := FilterRepJobs(policyID, "", "", nil, nil, 1000, 0) if err != nil { - t.Errorf("Error occured in FilterRepJobs: %v, policy ID: %d", err, policyID) + t.Errorf("Error occurred in FilterRepJobs: %v, policy ID: %d", err, policyID) return } if len(jobs) != 1 { @@ -1157,13 +1157,13 @@ func TestFilterRepJobs(t *testing.T) { func TestDeleteRepJob(t *testing.T) { err := DeleteRepJob(jobID) if err != nil { - t.Errorf("Error occured in DeleteRepJob: %v, id: %d", err, jobID) + t.Errorf("Error occurred in DeleteRepJob: %v, id: %d", err, jobID) return } t.Logf("deleted rep job, id: %d", jobID) j, err := GetRepJob(jobID) if err != nil { - t.Errorf("Error occured in GetRepJob:%v", err) + t.Errorf("Error occurred in GetRepJob:%v", err) return } if j != nil { @@ -1226,7 +1226,7 @@ func TestGetRepoJobToStop(t *testing.T) { func TestDeleteRepTarget(t *testing.T) { err := DeleteRepTarget(targetID) if err != nil { - t.Errorf("Error occured in DeleteRepTarget: %v, id: %d", err, targetID) + t.Errorf("Error occurred in DeleteRepTarget: %v, id: %d", err, targetID) return } t.Logf("deleted target, id: %d", targetID) @@ -1259,13 +1259,13 @@ func TestUpdateRepPolicy(t *testing.T) { func TestDeleteRepPolicy(t *testing.T) { err := DeleteRepPolicy(policyID) if err != nil { - t.Errorf("Error occured in DeleteRepPolicy: %v, id: %d", err, policyID) + t.Errorf("Error occurred in DeleteRepPolicy: %v, id: %d", err, policyID) return } t.Logf("delete rep policy, id: %d", policyID) p, err := GetRepPolicy(policyID) if err != nil && err != orm.ErrNoRows { - t.Errorf("Error occured in GetRepPolicy:%v", err) + t.Errorf("Error occurred in GetRepPolicy:%v", err) } if p != nil && p.Deleted != 1 { t.Errorf("Able to find rep policy after deletion, id: %d", policyID) diff --git a/src/common/dao/scan_job.go b/src/common/dao/scan_job.go index ae62ddc9a..4da813aec 100644 --- a/src/common/dao/scan_job.go +++ b/src/common/dao/scan_job.go @@ -167,7 +167,7 @@ func UpdateImgScanOverview(digest, detailsKey string, sev models.Severity, compO return nil } -// ListImgScanOverviews list all records in table img_scan_overview, it is called in notificaiton handler when it needs to refresh the severity of all images. +// ListImgScanOverviews list all records in table img_scan_overview, it is called in notification handler when it needs to refresh the severity of all images. func ListImgScanOverviews() ([]*models.ImgScanOverview, error) { var res []*models.ImgScanOverview o := GetOrmer() diff --git a/src/common/models/config.go b/src/common/models/config.go index 52de3e6e8..42caf3be8 100644 --- a/src/common/models/config.go +++ b/src/common/models/config.go @@ -57,6 +57,15 @@ type SQLite struct { File string `json:"file"` } +// PostGreSQL ... +type PostGreSQL struct { + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + Database string `json:"database"` +} + // Email ... type Email struct { Host string `json:"host"` diff --git a/src/common/models/project.go b/src/common/models/project.go index 1228a191b..4864076cf 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -19,6 +19,9 @@ import ( "time" ) +// ProjectTable is the table name for project +const ProjectTable = "project" + // Project holds the details of a project. type Project struct { ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` @@ -174,3 +177,8 @@ type ProjectQueryResult struct { Total int64 Projects []*Project } + +//TableName is required by beego orm to map Project to table project +func (p *Project) TableName() string { + return ProjectTable +} diff --git a/src/common/models/uaa.go b/src/common/models/uaa.go index 46469c94c..889ddeb6f 100644 --- a/src/common/models/uaa.go +++ b/src/common/models/uaa.go @@ -19,5 +19,5 @@ type UAASettings struct { Endpoint string ClientID string ClientSecret string - CARootPath string + VerifyCert bool } diff --git a/src/common/models/user.go b/src/common/models/user.go index fff2725d5..e46a09cda 100644 --- a/src/common/models/user.go +++ b/src/common/models/user.go @@ -18,6 +18,9 @@ import ( "time" ) +// UserTable is the name of table in DB that holds the user object +const UserTable = "user" + // User holds the details of a user. type User struct { UserID int `orm:"pk;auto;column(user_id)" json:"user_id"` @@ -45,3 +48,8 @@ type UserQuery struct { Email string Pagination *Pagination } + +// TableName ... +func (u *User) TableName() string { + return UserTable +} diff --git a/src/common/notifier/notifier.go b/src/common/notifier/notifier.go index d88690b8d..c2519cfc3 100644 --- a/src/common/notifier/notifier.go +++ b/src/common/notifier/notifier.go @@ -31,7 +31,7 @@ type HandlerChannel struct { //To indicate how many handler instances bound with this chan. boundCount uint32 - //The chan for controling concurrent executions. + //The chan for controlling concurrent executions. channel chan bool } @@ -199,7 +199,7 @@ func (nw *NotificationWatcher) Notify(notification Notification) error { }() if err := hd.Handle(notification.Value); err != nil { //Currently, we just log the error - log.Errorf("Error occurred when triggerring handler %s of topic %s: %s\n", reflect.TypeOf(hd).String(), notification.Topic, err.Error()) + log.Errorf("Error occurred when triggering handler %s of topic %s: %s\n", reflect.TypeOf(hd).String(), notification.Topic, err.Error()) } else { log.Infof("Handle notification with topic '%s': %#v\n", notification.Topic, notification.Value) } diff --git a/src/common/scheduler/policy/alternate_policy.go b/src/common/scheduler/policy/alternate_policy.go index dc1d3864f..fe72959c8 100644 --- a/src/common/scheduler/policy/alternate_policy.go +++ b/src/common/scheduler/policy/alternate_policy.go @@ -93,7 +93,7 @@ func (alp *AlternatePolicy) Done() <-chan bool { //AttachTasks is an implementation of same method in policy interface. func (alp *AlternatePolicy) AttachTasks(tasks ...task.Task) error { - if tasks == nil || len(tasks) == 0 { + if len(tasks) == 0 { return errors.New("No tasks can be attached") } diff --git a/src/common/utils/ldap/ldap.go b/src/common/utils/ldap/ldap.go index 8357a4dce..85d75592f 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/common/utils/ldap/ldap.go @@ -353,7 +353,7 @@ func (session *Session) createUserFilter(username string) string { if username == "" { filterTag = "*" } else { - filterTag = username + filterTag = goldap.EscapeFilter(username) } ldapFilter := session.ldapConfig.LdapFilter diff --git a/src/common/utils/test/adminserver.go b/src/common/utils/test/adminserver.go index a5382ce5f..8c4397275 100644 --- a/src/common/utils/test/adminserver.go +++ b/src/common/utils/test/adminserver.go @@ -60,9 +60,15 @@ var adminServerDefaultConfig = map[string]interface{}{ common.AdmiralEndpoint: "http://www.vmware.com", common.WithNotary: false, common.WithClair: false, + common.ClairDBUsername: "postgres", + common.ClairDBHost: "postgres", + common.ClairDB: "postgres", + common.ClairDBPort: 5432, + common.ClairDBPassword: "password", common.UAAClientID: "testid", common.UAAClientSecret: "testsecret", common.UAAEndpoint: "10.192.168.5", + common.UAAVerifyCert: false, common.UIURL: "http://myui:8888/", common.JobServiceURL: "http://myjob:8888/", } diff --git a/src/common/utils/uaa/client.go b/src/common/utils/uaa/client.go index 0f6511f5d..8aab55027 100644 --- a/src/common/utils/uaa/client.go +++ b/src/common/utils/uaa/client.go @@ -19,12 +19,25 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "fmt" "io/ioutil" "net/http" "strings" "github.com/vmware/harbor/src/common/utils/log" "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +const ( + //TokenURLSuffix ... + TokenURLSuffix = "/oauth/token" + //AuthURLSuffix ... + AuthURLSuffix = "/oauth/authorize" + //UserInfoURLSuffix ... + UserInfoURLSuffix = "/userinfo" + //UsersURLSuffix ... + UsersURLSuffix = "/Users" ) // Client provides funcs to interact with UAA. @@ -33,6 +46,8 @@ type Client interface { PasswordAuth(username, password string) (*oauth2.Token, error) //GetUserInfoByToken send the token to OIDC endpoint to get user info, currently it's also used to validate the token. GetUserInfo(token string) (*UserInfo, error) + //SearchUser searches a user based on user name. + SearchUser(name string) ([]*SearchUserEntry, error) } // ClientConfig values to initialize UAA Client @@ -56,21 +71,43 @@ type UserInfo struct { Email string `json:"email"` } +//SearchUserEmailEntry ... +type SearchUserEmailEntry struct { + Value string `json:"value"` + Primary bool `json:"primary"` +} + +//SearchUserEntry is the struct of an entry of user within search result. +type SearchUserEntry struct { + ID string `json:"id"` + ExtID string `json:"externalId"` + UserName string `json:"userName"` + Emails []SearchUserEmailEntry `json:"emails"` + Groups []interface{} +} + +//SearchUserRes is the struct to parse the result of search user API of UAA +type SearchUserRes struct { + Resources []*SearchUserEntry `json:"resources"` + TotalResults int `json:"totalResults"` + Schemas []string `json:"schemas"` +} + // DefaultClient leverages oauth2 pacakge for oauth features type defaultClient struct { httpClient *http.Client oauth2Cfg *oauth2.Config + twoLegCfg *clientcredentials.Config endpoint string //TODO: add public key, etc... } func (dc *defaultClient) PasswordAuth(username, password string) (*oauth2.Token, error) { - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient) - return dc.oauth2Cfg.PasswordCredentialsToken(ctx, username, password) + return dc.oauth2Cfg.PasswordCredentialsToken(dc.prepareCtx(), username, password) } func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) { - userInfoURL := dc.endpoint + "/uaa/userinfo" + userInfoURL := dc.endpoint + UserInfoURLSuffix req, err := http.NewRequest(http.MethodGet, userInfoURL, nil) if err != nil { return nil, err @@ -92,6 +129,45 @@ func (dc *defaultClient) GetUserInfo(token string) (*UserInfo, error) { return info, nil } +func (dc *defaultClient) SearchUser(username string) ([]*SearchUserEntry, error) { + token, err := dc.twoLegCfg.Token(dc.prepareCtx()) + if err != nil { + return nil, err + } + url := dc.endpoint + UsersURLSuffix + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Add("filter", fmt.Sprintf("Username eq '%s'", username)) + req.URL.RawQuery = q.Encode() + token.SetAuthHeader(req) + log.Debugf("request URL: %s", req.URL) + resp, err := dc.httpClient.Do(req) + if err != nil { + return nil, err + } + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Unexpected status code for searching user in UAA: %d, response: %s", resp.StatusCode, string(bytes)) + } + res := &SearchUserRes{} + if err := json.Unmarshal(bytes, res); err != nil { + return nil, err + } + return res.Resources, nil +} + +func (dc *defaultClient) prepareCtx() context.Context { + return context.WithValue(context.Background(), oauth2.HTTPClient, dc.httpClient) +} + // NewDefaultClient creates an instance of defaultClient. func NewDefaultClient(cfg *ClientConfig) (Client, error) { url := cfg.Endpoint @@ -125,14 +201,21 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) { ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ - TokenURL: url + "/uaa/oauth/token", - AuthURL: url + "/uaa/oauth/authorize", + TokenURL: url + TokenURLSuffix, + AuthURL: url + AuthURLSuffix, }, } + cc := &clientcredentials.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + TokenURL: url + TokenURLSuffix, + } + return &defaultClient{ httpClient: hc, oauth2Cfg: oc, + twoLegCfg: cc, endpoint: url, }, nil } diff --git a/src/common/utils/uaa/client_test.go b/src/common/utils/uaa/client_test.go index ead0c0273..774392106 100644 --- a/src/common/utils/uaa/client_test.go +++ b/src/common/utils/uaa/client_test.go @@ -30,15 +30,18 @@ func TestMain(m *testing.M) { } } -func TestPasswordAuth(t *testing.T) { - cfg := &ClientConfig{ +func getCfg() *ClientConfig { + return &ClientConfig{ ClientID: "uaa", ClientSecret: "secret", Endpoint: mockUAAServer.URL, SkipTLSVerify: true, } +} + +func TestPasswordAuth(t *testing.T) { assert := assert.New(t) - client, err := NewDefaultClient(cfg) + client, err := NewDefaultClient(getCfg()) assert.Nil(err) _, err = client.PasswordAuth("user1", "pass1") assert.Nil(err) @@ -47,14 +50,8 @@ func TestPasswordAuth(t *testing.T) { } func TestUserInfo(t *testing.T) { - cfg := &ClientConfig{ - ClientID: "uaa", - ClientSecret: "secret", - Endpoint: mockUAAServer.URL, - SkipTLSVerify: true, - } assert := assert.New(t) - client, err := NewDefaultClient(cfg) + client, err := NewDefaultClient(getCfg()) assert.Nil(err) token, err := ioutil.ReadFile(path.Join(currPath(), "test", "./good-access-token.txt")) if err != nil { @@ -68,6 +65,21 @@ func TestUserInfo(t *testing.T) { assert.NotNil(err2) } +func TestSearchUser(t *testing.T) { + assert := assert.New(t) + client, err := NewDefaultClient(getCfg()) + assert.Nil(err) + res1, err := client.SearchUser("one") + assert.Nil(err) + assert.Equal(1, len(res1)) + if len(res1) == 1 { + assert.Equal("one", res1[0].UserName) + } + res2, err := client.SearchUser("none") + assert.Nil(err) + assert.Equal(0, len(res2)) +} + func currPath() string { _, f, _, ok := runtime.Caller(0) if !ok { diff --git a/src/common/utils/uaa/fake_client.go b/src/common/utils/uaa/fake_client.go index 6e02bb1b6..0f0af38a4 100644 --- a/src/common/utils/uaa/fake_client.go +++ b/src/common/utils/uaa/fake_client.go @@ -37,3 +37,40 @@ func (fc *FakeClient) PasswordAuth(username, password string) (*oauth2.Token, er func (fc *FakeClient) GetUserInfo(token string) (*UserInfo, error) { return nil, nil } + +// SearchUser ... +func (fc *FakeClient) SearchUser(name string) ([]*SearchUserEntry, error) { + res := []*SearchUserEntry{} + entryOne := &SearchUserEntry{ + ExtID: "some-external-id-1", + ID: "u-0001", + UserName: "one", + Emails: []SearchUserEmailEntry{SearchUserEmailEntry{ + Primary: false, + Value: "one@email.com", + }}, + } + entryTwoA := &SearchUserEntry{ + ExtID: "some-external-id-2-a", + ID: "u-0002a", + UserName: "two", + Emails: []SearchUserEmailEntry{SearchUserEmailEntry{ + Primary: false, + Value: "two@email.com", + }}, + } + entryTwoB := &SearchUserEntry{ + ExtID: "some-external-id-2-b", + ID: "u-0002b", + UserName: "two", + } + if name == "one" { + res = append(res, entryOne) + } else if name == "two" { + res = append(res, entryTwoA) + res = append(res, entryTwoB) + } else if name == "error" { + return res, fmt.Errorf("some error") + } + return res, nil +} diff --git a/src/common/utils/uaa/test/good-access-token.txt b/src/common/utils/uaa/test/good-access-token.txt index 2c63bb1d2..cc0fd29b2 100644 --- a/src/common/utils/uaa/test/good-access-token.txt +++ b/src/common/utils/uaa/test/good-access-token.txt @@ -1 +1 @@ -eyJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNmRjYjg1YzMzZjU0OGM5ODk2YjI4MDEwN2IyOWM0NiIsInN1YiI6IjlhMTM0ODhmLWYzY2YtNDdhNi05OGYwLTRmZWQyMWY0MzUyMCIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiOWExMzQ4OGYtZjNjZi00N2E2LTk4ZjAtNGZlZDIxZjQzNTIwIiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6InVzZXIwMSIsImVtYWlsIjoidXNlcjAxQHVzZXIuZnJvbS5sZGFwLmNmIiwiYXV0aF90aW1lIjoxNTExNDA1NDEwLCJyZXZfc2lnIjoiOGEwYmY5OWQiLCJpYXQiOjE1MTE0MDU0MTAsImV4cCI6MTUxMTQ0ODYxMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My91YWEvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ1YWEiLCJhdWQiOlsia3ViZXJuZXRlcyIsIm9wZW5pZCJdfQ.I7VBx_cQoYkotRJ8KdmESAf_xjzp-R44BRz9ngHPUnoqr4rSMin-Ful8wNzEnaYaG56_mrIPuLOb6vXGWW1svRU892GOK9WQRSiFp7O81V7f1bH6JXnIGvyBNl3JOkDB9d5wXn137h9vNKq3Z9TF3jD7oXR_OENS8paclW5EAjmjGvEVIhObMmHCLhsJshTWIoP8AwoP1m9iqak_-t0c99HWaf1AgVUtT2i9Jb63ndJGA6BkOSRH_YxXmM_qtXmk_0kRA5oLDR2UGA4TVXCYp1_8iwQYjvGBVxO24I5jJh_zDYs5YLTFeNzMTPEhAl_Te6NiE91gRXq6KiVk9tTfuA +eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg diff --git a/src/common/utils/uaa/test/no-user.json b/src/common/utils/uaa/test/no-user.json new file mode 100644 index 000000000..a59e370e2 --- /dev/null +++ b/src/common/utils/uaa/test/no-user.json @@ -0,0 +1 @@ +{"resources":[],"startIndex":1,"itemsPerPage":100,"totalResults":0,"schemas":["urn:scim:schemas:core:1.0"]} diff --git a/src/common/utils/uaa/test/one-user.json b/src/common/utils/uaa/test/one-user.json new file mode 100644 index 000000000..6147d7a40 --- /dev/null +++ b/src/common/utils/uaa/test/one-user.json @@ -0,0 +1 @@ +{"resources":[{"id":"6af888a1-92fa-4a30-82dd-4db28f2e15f0","externalId":"cn=one,dc=vmware,dc=com","meta":{"version":0,"created":"2017-12-20T22:54:34.493Z","lastModified":"2017-12-20T22:54:34.493Z"},"userName":"one","name":{},"emails":[{"value":"one@example.com","primary":false}],"groups":[{"value":"546a79d3-609b-49df-8111-56eee574fc99","display":"roles","type":"DIRECT"},{"value":"7e5eb7dc-8067-424a-a593-8620e9ef4962","display":"approvals.me","type":"DIRECT"},{"value":"9a946687-7be7-4a79-9742-462ae52e4833","display":"password.write","type":"DIRECT"},{"value":"f263a309-f855-405b-bcc4-e1c7453420c3","display":"uaa.offline_token","type":"DIRECT"},{"value":"80898c93-64a8-46cc-ba15-37fec9e2e56d","display":"uaa.user","type":"DIRECT"},{"value":"f8605b49-0dbc-47cf-a993-9691b7e313ab","display":"scim.userids","type":"DIRECT"},{"value":"83237f80-e709-40b9-8599-ab08b0f141a9","display":"oauth.approvals","type":"DIRECT"},{"value":"f685504d-c760-41cf-9c26-da8edddf643e","display":"user_attributes","type":"DIRECT"},{"value":"4243ba6a-001f-4052-8059-ada841a14e62","display":"cloud_controller.write","type":"DIRECT"},{"value":"36a94fb1-3bd2-4db6-8246-8a00256b080f","display":"profile","type":"DIRECT"},{"value":"0ead714c-02f1-403b-bd24-950089772f47","display":"scim.me","type":"DIRECT"},{"value":"a0944e04-1007-43ba-9745-e1ed62de21f5","display":"cloud_controller.read","type":"DIRECT"},{"value":"e9f2b839-9e2d-45b4-9179-5fce07cd013b","display":"cloud_controller_service_permissions.read","type":"DIRECT"},{"value":"2bb835d6-c62d-477b-a1df-780bb3ec560b","display":"openid","type":"DIRECT"}],"approvals":[],"active":true,"verified":true,"origin":"ldap","zoneId":"uaa","passwordLastModified":"2017-12-20T22:54:34.000Z","lastLogonTime":1513839274546,"schemas":["urn:scim:schemas:core:1.0"]}],"startIndex":1,"itemsPerPage":100,"totalResults":1,"schemas":["urn:scim:schemas:core:1.0"]} diff --git a/src/common/utils/uaa/test/server.go b/src/common/utils/uaa/test/server.go index 5f6e1737c..07f4dc34d 100644 --- a/src/common/utils/uaa/test/server.go +++ b/src/common/utils/uaa/test/server.go @@ -53,23 +53,36 @@ func (t *tokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { http.Error(rw, "invalid client id/secret in header", http.StatusUnauthorized) return } - if gt := req.FormValue("grant_type"); gt != "password" { + gt := req.FormValue("grant_type") + if gt == "password" { + reqUsername := req.FormValue("username") + reqPasswd := req.FormValue("password") + if reqUsername == t.username && reqPasswd == t.password { + serveToken(rw) + } else { + http.Error(rw, fmt.Sprintf("invalid username/password %s/%s", reqUsername, reqPasswd), http.StatusUnauthorized) + } + } else if gt == "client_credentials" { + serveToken(rw) + } else { http.Error(rw, fmt.Sprintf("invalid grant_type: %s", gt), http.StatusBadRequest) return } - reqUsername := req.FormValue("username") - reqPasswd := req.FormValue("password") - if reqUsername == t.username && reqPasswd == t.password { - token, err := ioutil.ReadFile(path.Join(currPath(), "./uaa-token.json")) - if err != nil { - panic(err) - } - _, err2 := rw.Write(token) - if err2 != nil { - panic(err2) - } - } else { - http.Error(rw, fmt.Sprintf("invalid username/password %s/%s", reqUsername, reqPasswd), http.StatusUnauthorized) +} + +func serveToken(rw http.ResponseWriter) { + serveJSONFile(rw, "uaa-token.json") +} + +func serveJSONFile(rw http.ResponseWriter, filename string) { + data, err := ioutil.ReadFile(path.Join(currPath(), filename)) + if err != nil { + panic(err) + } + rw.Header().Add("Content-Type", "application/json") + _, err2 := rw.Write(data) + if err2 != nil { + panic(err2) } } @@ -78,27 +91,52 @@ type userInfoHandler struct { } func (u *userInfoHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - v := req.Header.Get("Authorization") + v := req.Header.Get("authorization") prefix := v[0:7] reqToken := v[7:] if strings.ToLower(prefix) != "bearer " || reqToken != u.token { http.Error(rw, "invalid token", http.StatusUnauthorized) return } - userInfo, err := ioutil.ReadFile(path.Join(currPath(), "./user-info.json")) - if err != nil { - panic(err) + serveJSONFile(rw, "./user-info.json") +} + +type searchUserHandler struct { + token string +} + +func (su *searchUserHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + v := req.Header.Get("authorization") + if v == "" { + v = req.Header.Get("Authorization") } - _, err2 := rw.Write(userInfo) - if err2 != nil { - panic(err2) + prefix := v[0:7] + reqToken := v[7:] + if strings.ToLower(prefix) != "bearer " || reqToken != su.token { + http.Error(rw, "invalid token", http.StatusUnauthorized) + return } + f := req.URL.Query().Get("filter") + elements := strings.Split(f, " ") + if len(elements) == 3 { + if elements[0] == "Username" && elements[1] == "eq" { + if elements[2] == "'one'" { + serveJSONFile(rw, "one-user.json") + return + } + serveJSONFile(rw, "no-user.json") + return + } + http.Error(rw, "invalid request", http.StatusBadRequest) + return + } + http.Error(rw, fmt.Sprintf("Invalid request, elements: %v", elements), http.StatusBadRequest) } // NewMockServer ... func NewMockServer(cfg *MockServerConfig) *httptest.Server { mux := http.NewServeMux() - mux.Handle("/uaa/oauth/token", &tokenHandler{ + mux.Handle("/oauth/token", &tokenHandler{ cfg.ClientID, cfg.ClientSecret, cfg.Username, @@ -108,6 +146,7 @@ func NewMockServer(cfg *MockServerConfig) *httptest.Server { if err != nil { panic(err) } - mux.Handle("/uaa/userinfo", &userInfoHandler{strings.TrimSpace(string(token))}) + mux.Handle("/userinfo", &userInfoHandler{strings.TrimSpace(string(token))}) + mux.Handle("/Users", &searchUserHandler{strings.TrimSpace(string(token))}) return httptest.NewTLSServer(mux) } diff --git a/src/common/utils/uaa/test/uaa-token.json b/src/common/utils/uaa/test/uaa-token.json index 3dc66d6db..8a4d52e8d 100644 --- a/src/common/utils/uaa/test/uaa-token.json +++ b/src/common/utils/uaa/test/uaa-token.json @@ -1 +1 @@ -{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJkZTU0ZjJkMDlkODc0ZTliOTExZDk4YWQ1MTQzMjljZC1yIiwic3ViIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwic2NvcGUiOlsib3BlbmlkIl0sImlhdCI6MTUwNjg1MDQyNiwiZXhwIjoxNTA5NDQyNDI2LCJjaWQiOiJrdWJlcm5ldGVzIiwiY2xpZW50X2lkIjoia3ViZXJuZXRlcyIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJvcmlnaW4iOiJsZGFwIiwidXNlcl9pZCI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInJldl9zaWciOiI2ZDljZTdlMCIsImF1ZCI6WyJrdWJlcm5ldGVzIiwib3BlbmlkIl19.oW4xK3QBjMtjUH_AWWyO6A0QwbIbTwrEFnc-hulj3QbLoULvC2V3L53rcKhT1gOtj8aaQTZFdBEQNGjBpzjFU8bpwxb0szyPMkc5PjXjcJGltL3MvmBf3P0TuUxJU9vP3FjrvwwueNAafLAyRIHy8yA3ZngzkL8KCI0ps51gCRU2oOe9hGDv2ZrsZ21u760hFGiRq5-7HWJu3VMqhMVRkUyPD_3j9AGZr6gf3o_7S9oJYwEDxPZaBhhVZI6QHeQNa07w7jCqTX97_fcpeTMbrBJiz_5yD9-kJZneI4xzAMIyNwAcbSJYrL7WZ2H01heGwWFEkrrv68YUJ762jB4WAw","expires_in":43199,"scope":"openid","jti":"279a6b614a33465cb61e38ff9a78cf1e"} \ No newline at end of file +{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIyNzlhNmI2MTRhMzM0NjVjYjYxZTM4ZmY5YTc4Y2YxZSIsInN1YiI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJrdWJlcm5ldGVzIiwiY2lkIjoia3ViZXJuZXRlcyIsImF6cCI6Imt1YmVybmV0ZXMiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwib3JpZ2luIjoibGRhcCIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbkB1c2VyLmZyb20ubGRhcC5jZiIsImF1dGhfdGltZSI6MTUwNjg1MDQyNiwicmV2X3NpZyI6IjZkOWNlN2UwIiwiaWF0IjoxNTA2ODUwNDI2LCJleHAiOjE1MDY4OTM2MjYsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbImt1YmVybmV0ZXMiLCJvcGVuaWQiXX0.Ni2yJ7Gp6OnEdhcWyfGeCm1yG_rqQgf9BA0raJ37hdH-_ZRQ4HIELkWLv3gGPuWPV4HX6EKKerjJWXCPKihyiIIVT-W7VkwFMdDv9e4aA_h2eXjxHeUdjl0Cgw7gSAPSmm_QtkeLPuj15Ngd31yiuBoxy49_sjCyn3hjd8LP2ENEVtpk2vcCiQigW-YWbDaG64im1IP6jjRruwRdPF0Idjf4vuimFG-tiRdauDvnZc90W5fIJ3AUFW_ryGnSvc7E0rBZFYOgD5BB_3HLmWzB64-D3AVe9h5wQXOBorEaXLlSXfm16RQHFI_duSh3YOZUjHLuUYIRCuKaK5RPi0Fztg","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImxlZ2FjeS10b2tlbi1rZXkiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJkZTU0ZjJkMDlkODc0ZTliOTExZDk4YWQ1MTQzMjljZC1yIiwic3ViIjoiMjAxMTE3MTktY2UzYS00NGFhLTkyYWMtMTc2YzRlMzgxZjY2Iiwic2NvcGUiOlsib3BlbmlkIl0sImlhdCI6MTUwNjg1MDQyNiwiZXhwIjoxNTA5NDQyNDI2LCJjaWQiOiJrdWJlcm5ldGVzIiwiY2xpZW50X2lkIjoia3ViZXJuZXRlcyIsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojg0NDMvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJvcmlnaW4iOiJsZGFwIiwidXNlcl9pZCI6IjIwMTExNzE5LWNlM2EtNDRhYS05MmFjLTE3NmM0ZTM4MWY2NiIsInJldl9zaWciOiI2ZDljZTdlMCIsImF1ZCI6WyJrdWJlcm5ldGVzIiwib3BlbmlkIl19.oW4xK3QBjMtjUH_AWWyO6A0QwbIbTwrEFnc-hulj3QbLoULvC2V3L53rcKhT1gOtj8aaQTZFdBEQNGjBpzjFU8bpwxb0szyPMkc5PjXjcJGltL3MvmBf3P0TuUxJU9vP3FjrvwwueNAafLAyRIHy8yA3ZngzkL8KCI0ps51gCRU2oOe9hGDv2ZrsZ21u760hFGiRq5-7HWJu3VMqhMVRkUyPD_3j9AGZr6gf3o_7S9oJYwEDxPZaBhhVZI6QHeQNa07w7jCqTX97_fcpeTMbrBJiz_5yD9-kJZneI4xzAMIyNwAcbSJYrL7WZ2H01heGwWFEkrrv68YUJ762jB4WAw","expires_in":43199,"jti":"279a6b614a33465cb61e38ff9a78cf1e", "scope":"clients.read password.write clients.secret uaa.resource openid clients.write uaa.admin scim.write scim.read client_id"} diff --git a/src/ui/api/config.go b/src/ui/api/config.go index 79beb1d52..d2e19bd08 100644 --- a/src/ui/api/config.go +++ b/src/ui/api/config.go @@ -52,6 +52,10 @@ var ( common.ProjectCreationRestriction, common.TokenExpiration, common.ScanAllPolicy, + common.UAAClientID, + common.UAAClientSecret, + common.UAAEndpoint, + common.UAAVerifyCert, } stringKeys = []string{ @@ -68,6 +72,8 @@ var ( common.EmailFrom, common.EmailIdentity, common.ProjectCreationRestriction, + common.UAAClientID, + common.UAAEndpoint, } numKeys = []string{ @@ -82,11 +88,13 @@ var ( common.EmailInsecure, common.SelfRegistration, common.LDAPVerifyCert, + common.UAAVerifyCert, } passwordKeys = []string{ common.EmailPassword, common.LDAPSearchPwd, + common.UAAClientSecret, } ) @@ -223,8 +231,8 @@ func validateCfg(c map[string]interface{}) (bool, error) { } if value, ok := strMap[common.AUTHMode]; ok { - if value != common.DBAuth && value != common.LDAPAuth { - return false, fmt.Errorf("invalid %s, shoud be %s or %s", common.AUTHMode, common.DBAuth, common.LDAPAuth) + if value != common.DBAuth && value != common.LDAPAuth && value != common.UAAAuth { + return false, fmt.Errorf("invalid %s, shoud be one of %s, %s, %s", common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth) } flag, err := authModeCanBeModified() if err != nil { @@ -329,8 +337,12 @@ func convertForGet(cfg map[string]interface{}) (map[string]*value, error) { if err != nil { return nil, err } - result[common.AUTHMode].Editable = flag - + //All configuration of UAA will be un-editable for PKS 1.0 (1.4) + result[common.AUTHMode].Editable = flag && result[common.AUTHMode].Value.(string) != common.UAAAuth + result[common.UAAEndpoint].Editable = false + // result[common.UAAClientSecret].Editable = false + result[common.UAAVerifyCert].Editable = false + result[common.UAAClientID].Editable = false return result, nil } diff --git a/src/ui/api/ldap.go b/src/ui/api/ldap.go index 64d8fd199..f3f2005a5 100644 --- a/src/ui/api/ldap.go +++ b/src/ui/api/ldap.go @@ -15,9 +15,7 @@ package api import ( - "fmt" "net/http" - "strings" "github.com/vmware/harbor/src/common/models" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" @@ -30,7 +28,13 @@ type LdapAPI struct { BaseController } -const metaChars = "&|!=~*<>()" +const ( + pingErrorMessage = "LDAP connection test failed!" + loadSystemErrorMessage = "Can't load system configuration!" + canNotOpenLdapSession = "Can't open LDAP session!" + searchLdapFailMessage = "LDAP search failed!" + importUserError = "Found internal error when importing LDAP user!" +) // Prepare ... func (l *LdapAPI) Prepare() { @@ -57,7 +61,7 @@ func (l *LdapAPI) Ping() { ldapSession, err = ldapUtils.LoadSystemLdapConfig() if err != nil { log.Errorf("Can't load system configuration, error: %v", err) - l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err)) + l.RenderError(http.StatusInternalServerError, pingErrorMessage) return } err = ldapSession.ConnectionTest() @@ -68,7 +72,7 @@ func (l *LdapAPI) Ping() { if err != nil { log.Errorf("ldap connect fail, error: %v", err) - l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap connect fail: %v", err)) + l.RenderError(http.StatusBadRequest, pingErrorMessage) return } } @@ -84,7 +88,7 @@ func (l *LdapAPI) Search() { ldapSession, err = ldapUtils.LoadSystemLdapConfig() if err != nil { log.Errorf("can't load system configuration, error: %v", err) - l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err)) + l.RenderError(http.StatusInternalServerError, loadSystemErrorMessage) return } } else { @@ -94,28 +98,18 @@ func (l *LdapAPI) Search() { if err = ldapSession.Open(); err != nil { log.Errorf("can't Open ldap session, error: %v", err) - l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't open ldap session: %v", err)) + l.RenderError(http.StatusInternalServerError, canNotOpenLdapSession) return } defer ldapSession.Close() searchName := l.GetString("username") - if searchName != "" { - for _, c := range metaChars { - if strings.ContainsRune(searchName, c) { - log.Errorf("the search username contains meta char: %q", c) - l.RenderError(http.StatusBadRequest, fmt.Sprintf("the search username contains meta char: %q", c)) - return - } - } - } - ldapUsers, err = ldapSession.SearchUser(searchName) if err != nil { log.Errorf("Ldap search fail, error: %v", err) - l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap search fail: %v", err)) + l.RenderError(http.StatusBadRequest, searchLdapFailMessage) return } @@ -136,13 +130,13 @@ func (l *LdapAPI) ImportUser() { if err != nil { log.Errorf("Ldap import user fail, error: %v", err) - l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap import user fail: %v", err)) + l.RenderError(http.StatusBadRequest, importUserError) return } if len(ldapFailedImportUsers) > 0 { log.Errorf("Import ldap user have internal error") - l.RenderError(http.StatusInternalServerError, fmt.Sprintf("import ldap user have internal error")) + l.RenderError(http.StatusInternalServerError, importUserError) l.Data["json"] = ldapFailedImportUsers l.ServeJSON() return @@ -175,13 +169,6 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. continue } - for _, c := range metaChars { - if strings.ContainsRune(u.UID, c) { - u.Error = "invaild_username" - break - } - } - if u.Error != "" { failedImportUser = append(failedImportUser, u) continue diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index 509180ca2..84551effd 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "net/http" "strconv" + "strings" "time" "github.com/docker/distribution/manifest/schema1" @@ -66,6 +67,11 @@ type tagDetail struct { DockerVersion string `json:"docker_version"` Author string `json:"author"` Created time.Time `json:"created"` + Config *cfg `json:"config"` +} + +type cfg struct { + Labels map[string]string `json:"labels"` } type tagResp struct { @@ -473,9 +479,28 @@ func getTagDetail(client *registry.Repository, tag string) (*tagDetail, error) { return detail, err } + populateAuthor(detail) + return detail, nil } +func populateAuthor(detail *tagDetail) { + // has author info already + if len(detail.Author) > 0 { + return + } + + // try to set author with the value of label "maintainer" + if detail.Config != nil { + for k, v := range detail.Config.Labels { + if strings.ToLower(k) == "maintainer" { + detail.Author = v + return + } + } + } +} + // GetManifests returns the manifest of a tag func (ra *RepositoryAPI) GetManifests() { repoName := ra.GetString(":splat") @@ -638,7 +663,7 @@ func (ra *RepositoryAPI) Put() { } project, _ := utils.ParseRepository(name) - if !ra.SecurityCtx.HasAllPerm(project) { + if !ra.SecurityCtx.HasWritePerm(project) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } diff --git a/src/ui/api/repository_test.go b/src/ui/api/repository_test.go index 42f96ec55..62ebe61bc 100644 --- a/src/ui/api/repository_test.go +++ b/src/ui/api/repository_test.go @@ -199,3 +199,27 @@ func TestGetReposTop(t *testing.T) { fmt.Printf("\n") } + +func TestPopulateAuthor(t *testing.T) { + author := "author" + detail := &tagDetail{ + Author: author, + } + populateAuthor(detail) + assert.Equal(t, author, detail.Author) + + detail = &tagDetail{} + populateAuthor(detail) + assert.Equal(t, "", detail.Author) + + maintainer := "maintainer" + detail = &tagDetail{ + Config: &cfg{ + Labels: map[string]string{ + "Maintainer": maintainer, + }, + }, + } + populateAuthor(detail) + assert.Equal(t, maintainer, detail.Author) +} diff --git a/src/ui/auth/authenticator.go b/src/ui/auth/authenticator.go index d58fa4ac7..111f001aa 100644 --- a/src/ui/auth/authenticator.go +++ b/src/ui/auth/authenticator.go @@ -46,10 +46,11 @@ var registry = make(map[string]AuthenticateHelper) // Register add different authenticators to registry map. func Register(name string, h AuthenticateHelper) { if _, dup := registry[name]; dup { - log.Infof("authenticator: %s has been registered", name) + log.Infof("authenticator: %s has been registered,skip", name) return } registry[name] = h + log.Debugf("Registered authencation helper for auth mode: %s", name) } // Login authenticates user credentials based on setting. diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 5c11e8561..885ef8824 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -28,8 +28,6 @@ import ( // Auth implements AuthenticateHelper interface to authenticate against LDAP type Auth struct{} -const metaChars = "&|!=~*<>()" - // Authenticate checks user's credential against LDAP based on basedn template and LDAP URL, // if the check is successful a dummy record will be inserted into DB, such that this user can // be associated to other entities in the system. @@ -40,11 +38,6 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { log.Debugf("LDAP authentication failed for empty user id.") return nil, nil } - for _, c := range metaChars { - if strings.ContainsRune(p, c) { - return nil, fmt.Errorf("the principal contains meta char: %q", c) - } - } ldapSession, err := ldapUtils.LoadSystemLdapConfig() diff --git a/src/ui/auth/uaa/uaa.go b/src/ui/auth/uaa/uaa.go index d2c25edd0..22557a9bf 100644 --- a/src/ui/auth/uaa/uaa.go +++ b/src/ui/auth/uaa/uaa.go @@ -15,79 +15,118 @@ package uaa import ( + "fmt" + "strings" "sync" + "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/uaa" + "github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/config" ) -var lock = &sync.Mutex{} -var client uaa.Client - -//GetClient returns the client instance, if the client is not created it creates one. -func GetClient() (uaa.Client, error) { - lock.Lock() - defer lock.Unlock() - if client != nil { - return client, nil - } +//CreateClient create a UAA Client instance based on system configuration. +func CreateClient() (uaa.Client, error) { UAASettings, err := config.UAASettings() if err != nil { return nil, err } cfg := &uaa.ClientConfig{ - ClientID: UAASettings.ClientID, - ClientSecret: UAASettings.ClientSecret, - Endpoint: UAASettings.Endpoint, - CARootPath: UAASettings.CARootPath, + ClientID: UAASettings.ClientID, + ClientSecret: UAASettings.ClientSecret, + Endpoint: UAASettings.Endpoint, + SkipTLSVerify: !UAASettings.VerifyCert, } - client, err = uaa.NewDefaultClient(cfg) - return client, err + return uaa.NewDefaultClient(cfg) } -func doAuth(username, password string, client uaa.Client) (*models.User, error) { - t, err := client.PasswordAuth(username, password) +// Auth is the implementation of AuthenticateHelper to access uaa for authentication. +type Auth struct { + sync.Mutex + client uaa.Client +} + +//Authenticate ... +func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { + if err := u.ensureClient(); err != nil { + return nil, err + } + t, err := u.client.PasswordAuth(m.Principal, m.Password) if t != nil && err == nil { //TODO: See if it's possible to get more information from token. - u := &models.User{ - Username: username, - Password: "1234567ab", - Email: username + "@placeholder.com", - Realname: username, - } - err = dao.OnBoardUser(u) - if err == nil { - return u, nil + user := &models.User{ + Username: m.Principal, } + err = u.OnBoardUser(user) + return user, err } return nil, err } -// Auth is the implementation of AuthenticateHelper to access uaa for authentication. -type Auth struct{} +// OnBoardUser will check if a user exists in user table, if not insert the user and +// put the id in the pointer of user model, if it does exist, return the user's profile. +func (u *Auth) OnBoardUser(user *models.User) error { + user.Username = strings.TrimSpace(user.Username) + if len(user.Username) == 0 { + return fmt.Errorf("The Username is empty") + } + if len(user.Password) == 0 { + user.Password = "1234567ab" + } + if len(user.Realname) == 0 { + user.Realname = user.Username + } + if len(user.Email) == 0 { + //TODO: handle the case when user.Username itself is an email address. + user.Email = user.Username + "@uaa.placeholder" + } + user.Comment = "From UAA" + return dao.OnBoardUser(user) +} -//Authenticate ... -func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { - client, err := GetClient() +// SearchUser search user on uaa server, transform it to Harbor's user model +func (u *Auth) SearchUser(username string) (*models.User, error) { + if err := u.ensureClient(); err != nil { + return nil, err + } + l, err := u.client.SearchUser(username) if err != nil { return nil, err } - return doAuth(m.Principal, m.Password, client) + if len(l) == 0 { + return nil, nil + } + if len(l) > 1 { + return nil, fmt.Errorf("Multiple entries found for username: %s", username) + } + e := l[0] + email := "" + if len(e.Emails) > 0 { + email = e.Emails[0].Value + } + return &models.User{ + Username: username, + Email: email, + }, nil } -// OnBoardUser will check if a user exists in user table, if not insert the user and -// put the id in the pointer of user model, if it does exist, return the user's profile. -// func (u *Auth) OnBoardUser(user *models.User) error { -// panic("not implemented") -// } - -// // SearchUser - search user on uaa server -// func (u *Auth) SearchUser(username string) (*models.User, error) { -// panic("not implemented") -// } - -// func init() { -// auth.Register(auth.UAAAuth, &Auth{}) -// } +func (u *Auth) ensureClient() error { + if u.client != nil { + return nil + } + u.Lock() + defer u.Unlock() + if u.client == nil { + c, err := CreateClient() + if err != nil { + return err + } + u.client = c + } + return nil +} +func init() { + auth.Register(common.UAAAuth, &Auth{}) +} diff --git a/src/ui/auth/uaa/uaa_test.go b/src/ui/auth/uaa/uaa_test.go index 748c362c1..fa25f8436 100644 --- a/src/ui/auth/uaa/uaa_test.go +++ b/src/ui/auth/uaa/uaa_test.go @@ -17,45 +17,159 @@ package uaa import ( "github.com/stretchr/testify/assert" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" utilstest "github.com/vmware/harbor/src/common/utils/test" "github.com/vmware/harbor/src/common/utils/uaa" "github.com/vmware/harbor/src/ui/config" "os" + "strconv" "testing" ) -func TestGetClient(t *testing.T) { - assert := assert.New(t) +func TestMain(m *testing.M) { + dbHost := os.Getenv("MYSQL_HOST") + if len(dbHost) == 0 { + log.Fatalf("environment variable MYSQL_HOST is not set") + } + dbUser := os.Getenv("MYSQL_USR") + if len(dbUser) == 0 { + log.Fatalf("environment variable MYSQL_USR is not set") + } + dbPortStr := os.Getenv("MYSQL_PORT") + if len(dbPortStr) == 0 { + log.Fatalf("environment variable MYSQL_PORT is not set") + } + dbPort, err := strconv.Atoi(dbPortStr) + if err != nil { + log.Fatalf("invalid MYSQL_PORT: %v", err) + } + + dbPassword := os.Getenv("MYSQL_PWD") + dbDatabase := os.Getenv("MYSQL_DATABASE") + if len(dbDatabase) == 0 { + log.Fatalf("environment variable MYSQL_DATABASE is not set") + } + + database := &models.Database{ + Type: "mysql", + MySQL: &models.MySQL{ + Host: dbHost, + Port: dbPort, + Username: dbUser, + Password: dbPassword, + Database: dbDatabase, + }, + } + dao.InitDatabase(database) server, err := utilstest.NewAdminserver(nil) if err != nil { - t.Fatalf("failed to create a mock admin server: %v", err) + panic(err) } defer server.Close() if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil { - t.Fatalf("failed to set env %s: %v", "ADMINSERVER_URL", err) + panic(err) } err = config.Init() if err != nil { - t.Fatalf("failed to init config: %v", err) + panic(err) } - c, err := GetClient() + + err = dao.ClearTable("project_member") + if err != nil { + panic(err) + } + err = dao.ClearTable("project_metadata") + if err != nil { + panic(err) + } + err = dao.ClearTable("access_log") + if err != nil { + panic(err) + } + err = dao.ClearTable("project") + if err != nil { + panic(err) + } + err = dao.ClearTable("user") + if err != nil { + panic(err) + } + + rc := m.Run() + os.Exit(rc) +} + +func TestCreateClient(t *testing.T) { + assert := assert.New(t) + c, err := CreateClient() assert.Nil(err) assert.NotNil(c) } -func TestDoAuth(t *testing.T) { +func TestAuthenticate(t *testing.T) { assert := assert.New(t) client := &uaa.FakeClient{ Username: "user1", Password: "password1", } - dao.PrepareTestForMySQL() - u1, err1 := doAuth("user1", "password1", client) + auth := Auth{client: client} + m1 := models.AuthModel{ + Principal: "user1", + Password: "password1", + } + u1, err1 := auth.Authenticate(m1) assert.Nil(err1) - assert.True(u1.UserID > 0) - u2, err2 := doAuth("wrong", "wrong", client) + assert.NotNil(u1) + m2 := models.AuthModel{ + Principal: "wrong", + Password: "wrong", + } + u2, err2 := auth.Authenticate(m2) assert.NotNil(err2) assert.Nil(u2) + err3 := dao.ClearTable(models.UserTable) + assert.Nil(err3) +} + +func TestOnBoardUser(t *testing.T) { + assert := assert.New(t) + auth := Auth{} + um1 := &models.User{ + Username: " ", + } + err1 := auth.OnBoardUser(um1) + assert.NotNil(err1) + um2 := &models.User{ + Username: "test ", + } + user2, _ := dao.GetUser(models.User{Username: "test"}) + assert.Nil(user2) + err2 := auth.OnBoardUser(um2) + assert.Nil(err2) + user, _ := dao.GetUser(models.User{Username: "test"}) + assert.Equal("test", user.Realname) + assert.Equal("test", user.Username) + assert.Equal("test@uaa.placeholder", user.Email) +} + +func TestSearchUser(t *testing.T) { + assert := assert.New(t) + client := &uaa.FakeClient{ + Username: "user1", + Password: "password1", + } + auth := Auth{client: client} + _, err0 := auth.SearchUser("error") + assert.NotNil(err0) + u1, err1 := auth.SearchUser("one") + assert.Nil(err1) + assert.Equal("one@email.com", u1.Email) + _, err2 := auth.SearchUser("two") + assert.NotNil(err2) + user3, err3 := auth.SearchUser("none") + assert.Nil(user3) + assert.Nil(err3) } diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 8fef0a17e..d651aa35c 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -392,15 +392,21 @@ func ClairEndpoint() string { return common.DefaultClairEndpoint } -// ClairDBPassword returns the password for accessing Clair's DB. -func ClairDBPassword() (string, error) { +// ClairDB return Clair db info +func ClairDB() (*models.PostGreSQL, error){ cfg, err := mg.Get() if err != nil { - return "", err + log.Errorf("Failed to get configuration of Clair DB, Error detail %v", err) + return nil, err } - return cfg[common.ClairDBPassword].(string), nil + clairDB := &models.PostGreSQL{} + clairDB.Host = cfg[common.ClairDBHost].(string) + clairDB.Port = int(cfg[common.ClairDBPort].(float64)) + clairDB.Username = cfg[common.ClairDBUsername].(string) + clairDB.Password = cfg[common.ClairDBPassword].(string) + clairDB.Database = cfg[common.ClairDB].(string) + return clairDB, nil } - // AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string. func AdmiralEndpoint() string { cfg, err := mg.Get() @@ -454,9 +460,7 @@ func UAASettings() (*models.UAASettings, error) { Endpoint: cfg[common.UAAEndpoint].(string), ClientID: cfg[common.UAAClientID].(string), ClientSecret: cfg[common.UAAClientSecret].(string), - } - if len(os.Getenv("UAA_CA_ROOT")) != 0 { - us.CARootPath = os.Getenv("UAA_CA_ROOT") + VerifyCert: cfg[common.UAAVerifyCert].(bool), } return us, nil } diff --git a/src/ui/config/config_test.go b/src/ui/config/config_test.go index 60d0d8609..033dbc01b 100644 --- a/src/ui/config/config_test.go +++ b/src/ui/config/config_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/vmware/harbor/src/common/utils/test" + "github.com/vmware/harbor/src/common" ) // test functions under package ui/config @@ -117,6 +118,18 @@ func TestConfig(t *testing.T) { if _, err := Database(); err != nil { t.Fatalf("failed to get database: %v", err) } + + clairDB, err := ClairDB(); + if err != nil { + t.Fatalf("failed to get clair DB %v", err) + } + adminServerDefaultConfig := test.GetDefaultConfigMap() + assert.Equal(adminServerDefaultConfig[common.ClairDB],clairDB.Database) + assert.Equal(adminServerDefaultConfig[common.ClairDBUsername],clairDB.Username) + assert.Equal(adminServerDefaultConfig[common.ClairDBPassword],clairDB.Password) + assert.Equal(adminServerDefaultConfig[common.ClairDBHost], clairDB.Host) + assert.Equal(adminServerDefaultConfig[common.ClairDBPort], clairDB.Port) + if InternalNotaryEndpoint() != "http://notary-server:4443" { t.Errorf("Unexpected notary endpoint: %s", InternalNotaryEndpoint()) } @@ -163,7 +176,7 @@ func TestConfig(t *testing.T) { t.Fatalf("failed to get UAA setting, error: %v", err) } - if us.ClientID != "testid" || us.ClientSecret != "testsecret" || us.Endpoint != "10.192.168.5" { + if us.ClientID != "testid" || us.ClientSecret != "testsecret" || us.Endpoint != "10.192.168.5" || us.VerifyCert { t.Errorf("Unexpected UAA setting: %+v", *us) } assert.Equal("http://myjob:8888", InternalJobServiceURL()) diff --git a/src/ui/main.go b/src/ui/main.go index 44ffb8e00..f322db446 100644 --- a/src/ui/main.go +++ b/src/ui/main.go @@ -95,11 +95,11 @@ func main() { log.Fatalf("failed to initialize database: %v", err) } if config.WithClair() { - clairDBPassword, err := config.ClairDBPassword() + clairDB, err := config.ClairDB() if err != nil { log.Fatalf("failed to load clair database information: %v", err) } - if err := dao.InitClairDB(clairDBPassword); err != nil { + if err := dao.InitClairDB(clairDB); err != nil { log.Fatalf("failed to initialize clair database: %v", err) } } diff --git a/src/ui/router.go b/src/ui/router.go index 6ad3be9fc..fd6d34c78 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -32,15 +32,11 @@ func initRouters() { //Page Controllers: beego.Router("/", &controllers.IndexController{}) - beego.Router("/sign-in", &controllers.IndexController{}) - beego.Router("/sign-up", &controllers.IndexController{}) beego.Router("/reset_password", &controllers.IndexController{}) beego.Router("/harbor", &controllers.IndexController{}) beego.Router("/harbor/sign-in", &controllers.IndexController{}) - beego.Router("/harbor/sign-up", &controllers.IndexController{}) - beego.Router("/harbor/dashboard", &controllers.IndexController{}) beego.Router("/harbor/projects", &controllers.IndexController{}) beego.Router("/harbor/projects/:id/repositories", &controllers.IndexController{}) beego.Router("/harbor/projects/:id/repositories/*", &controllers.IndexController{}) diff --git a/src/ui_ng/karma.conf.js b/src/ui_ng/karma.conf.js index c96997aef..4846d21e2 100644 --- a/src/ui_ng/karma.conf.js +++ b/src/ui_ng/karma.conf.js @@ -7,7 +7,7 @@ module.exports = function (config) { frameworks: ['jasmine', 'angular-cli'], plugins: [ require('karma-jasmine'), - require('karma-phantomjs-launcher'), + require('karma-chrome-launcher'), require('karma-mocha-reporter'), require('karma-remap-istanbul'), require('angular-cli/plugins/karma') @@ -38,7 +38,13 @@ module.exports = function (config) { colors: true, logLevel: config.LOG_INFO, autoWatch: true, - browsers: ['PhantomJS'], + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, singleRun: true }); }; diff --git a/src/ui_ng/lib/karma.conf.js b/src/ui_ng/lib/karma.conf.js index 755f19f9f..60f16373c 100644 --- a/src/ui_ng/lib/karma.conf.js +++ b/src/ui_ng/lib/karma.conf.js @@ -7,7 +7,7 @@ module.exports = function (config) { frameworks: ['jasmine', '@angular/cli'], plugins: [ require('karma-jasmine'), - require('karma-phantomjs-launcher'), + require('karma-chrome-launcher'), require('karma-mocha-reporter'), require('karma-remap-istanbul'), require('@angular/cli/plugins/karma') @@ -38,7 +38,13 @@ module.exports = function (config) { colors: true, logLevel: config.LOG_INFO, autoWatch: true, - browsers: ['PhantomJS'], + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, singleRun: true }); }; diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index f810fdf0e..cfc448ae9 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.0", + "version": "0.6.2", "description": "Harbor shared UI components based on Clarity and Angular4", "scripts": { "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", @@ -17,21 +17,21 @@ "build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy" }, "dependencies": { - "@angular/animations": "~4.1.3", - "@angular/common": "~4.1.3", - "@angular/compiler": "~4.1.3", - "@angular/core": "~4.1.3", - "@angular/forms": "~4.1.3", - "@angular/http": "~4.1.3", - "@angular/platform-browser": "~4.1.3", - "@angular/platform-browser-dynamic": "~4.1.3", - "@angular/router": "~4.1.3", + "@angular/animations": "^4.3.0", + "@angular/common": "^4.3.0", + "@angular/compiler": "^4.3.0", + "@angular/core": "^4.3.0", + "@angular/forms": "^4.3.0", + "@angular/http": "^4.3.0", + "@angular/platform-browser": "^4.3.0", + "@angular/platform-browser-dynamic": "^4.3.0", + "@angular/router": "^4.3.0", "@ngx-translate/core": "^6.0.0", "@ngx-translate/http-loader": "0.0.3", - "@webcomponents/custom-elements": "1.0.0-alpha.3", - "clarity-angular": "^0.9.8", - "clarity-icons": "^0.9.8", - "clarity-ui": "^0.9.8", + "@webcomponents/custom-elements": "^1.0.0", + "clarity-angular": "^0.10.17", + "clarity-icons": "^0.10.17", + "clarity-ui": "^0.10.17", "core-js": "^2.4.1", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", @@ -42,31 +42,30 @@ "zone.js": "^0.8.4" }, "devDependencies": { - "@angular/cli": "^1.0.0", - "@angular/compiler-cli": "~4.1.3", + "@angular/cli": "1.4.0", + "@angular/compiler-cli": "^4.3.0", "@types/core-js": "^0.9.41", - "@types/jasmine": "~2.2.30", - "@types/node": "^6.0.42", + "@types/jasmine": "~2.5.53", + "@types/node": "~6.0.60", "bootstrap": "4.0.0-alpha.5", - "codelyzer": "~2.0.0-beta.4", + "codelyzer": "~3.1.1", "copyfiles": "^1.2.0", "enhanced-resolve": "^3.0.0", - "jasmine-core": "2.4.1", - "jasmine-spec-reporter": "2.5.0", - "karma": "1.2.0", - "karma-chrome-launcher": "^2.2.0", - "karma-cli": "^1.0.1", - "karma-jasmine": "^1.0.2", - "karma-mocha-reporter": "^2.2.1", - "karma-phantomjs-launcher": "^1.0.0", + "jasmine-core": "~2.6.2", + "jasmine-spec-reporter": "~4.1.0", + "karma": "~1.7.0", + "karma-chrome-launcher": "~2.1.1", + "karma-cli": "~1.0.1", + "karma-jasmine": "~1.1.0", + "karma-mocha-reporter": "^2.2.4", "karma-remap-istanbul": "^0.2.1", "protractor": "^4.0.9", "rimraf": "^2.6.1", "rollup": "^0.41.6", "rollup-plugin-node-resolve": "^3.0.0", - "ts-node": "1.2.1", - "tslint": "^4.1.1", - "typescript": "~2.2.0", + "ts-node": "~3.2.0", + "tslint": "~5.7.0", + "typescript": "~2.3.3", "typings": "^1.4.0", "uglify-js": "^2.8.22", "webdriver-manager": "10.2.5" diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index c00f2df0b..3d406f77b 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.0", + "version": "0.6.2", "description": "Harbor shared UI components based on Clarity and Angular4", "author": "VMware", "module": "index.js", @@ -19,21 +19,21 @@ }, "homepage": "https://github.com/vmware/harbor#readme", "peerDependencies": { - "@angular/animations": "^4.0.1", - "@angular/common": "^4.0.1", - "@angular/compiler": "^4.0.1", - "@angular/core": "^4.0.1", - "@angular/forms": "^4.0.1", - "@angular/http": "^4.0.1", - "@angular/platform-browser": "^4.0.1", - "@angular/platform-browser-dynamic": "^4.0.1", - "@angular/router": "^4.0.1", + "@angular/animations": "^4.3.0", + "@angular/common": "^4.3.0", + "@angular/compiler": "^4.3.0", + "@angular/core": "^4.3.0", + "@angular/forms": "^4.3.0", + "@angular/http": "^4.3.0", + "@angular/platform-browser": "^4.3.0", + "@angular/platform-browser-dynamic": "^4.3.0", + "@angular/router": "^4.3.0", "@ngx-translate/core": "^6.0.0", "@ngx-translate/http-loader": "0.0.3", - "@webcomponents/custom-elements": "1.0.0-alpha.3", - "clarity-angular": "^0.9.8", - "clarity-icons": "^0.9.8", - "clarity-ui": "^0.9.8", + "@webcomponents/custom-elements": "^1.0.0", + "clarity-angular": "^0.10.17", + "clarity-icons": "^0.10.17", + "clarity-ui": "^0.10.17", "core-js": "^2.4.1", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", diff --git a/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts index 69e4c7b85..57e53d36f 100644 --- a/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts +++ b/src/ui_ng/lib/src/config/vulnerability/vulnerability-config.component.template.ts @@ -4,23 +4,23 @@ export const VULNERABILITY_CONFIG_HTML: string = `
- + - + {{'CONFIG.SCANNING.DB_NOT_READY' | translate }} - + - + {{ updatedTimestamp | date:'MM/dd/y HH:mm:ss' }}
diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts new file mode 100644 index 000000000..b4b160ee1 --- /dev/null +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts @@ -0,0 +1,27 @@ + +/** + * Created by pengf on 11/22/2017. + */ + +export class BatchInfo { + name: string; + status: string; + loading: boolean; + errorState: boolean; + errorInfo: string; + constructor() { + this.status = 'pending'; + this.loading = false; + this.errorState = false; + this.errorInfo = ''; + } +} + +export function BathInfoChanges(list: BatchInfo, status: string, loading = false, errStatus = false, errorInfo = '') { + list.status = status; + list.loading = loading; + list.errorState = errStatus; + list.errorInfo = errorInfo; + return list; +} + diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.css.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.css.ts index 0a156cdb7..1507908c9 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.css.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.css.ts @@ -18,4 +18,12 @@ export const CONFIRMATION_DIALOG_STYLE: string = ` width: 80%; white-space: pre-wrap; } +.batchInfoUl{ + padding: 20px; list-style-type: none; +} +.batchInfoUl li {line-height: 24px;border-bottom: 1px solid #e8e8e8;} +.batchInfoUl li span:first-child {padding-right: 20px; width: 210px; display: inline-block; color:#666;} +.batchInfoUl li span:last-child {width: 260px; display: inline-block; color:#666;} +.batchInfoUl li span i {display: inline-block; line-height: 1.2em; font-size: 0.8em; color: #999;} +.batchInfoUl li span a{cursor: pointer; text-decoration: underline;} `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts index 15797278b..e882ba5c6 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts @@ -6,19 +6,33 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = `
{{dialogContent}}
+
+ +