mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 12:15:20 +01:00
Merge branch 'master' into dev
This commit is contained in:
commit
27802f7620
6
.github/ISSUE_TEMPLATE
vendored
6
.github/ISSUE_TEMPLATE
vendored
@ -1,5 +1,5 @@
|
|||||||
If you are reporting a problem, please make sure the following information are provided:
|
If you are reporting a problem, please make sure the following information are provided:
|
||||||
1)Version of docker engine and docker-compose
|
1)Version of docker engine and docker-compose.
|
||||||
2)Config files of harbor, you can get them by packaging "Deploy/config" directory
|
2)Config files of harbor, you can get them by packaging "harbor.cfg" and files in the same directory, including subdirectory.
|
||||||
3)Log files, you can get them by package the /var/log/harbor/
|
3)Log files, you can get them by package the /var/log/harbor/ .
|
||||||
|
|
||||||
|
11
README.md
11
README.md
@ -33,6 +33,7 @@ Refer to **[User Guide](docs/user_guide.md)** for more details on how to use Har
|
|||||||
**Slack:** Join Harbor's community here: [VMware {code}](https://code.vmware.com/join/), Channel: #harbor.
|
**Slack:** Join Harbor's community here: [VMware {code}](https://code.vmware.com/join/), Channel: #harbor.
|
||||||
**Email:** harbor@ vmware.com .
|
**Email:** harbor@ vmware.com .
|
||||||
**WeChat Group:** Add WeChat id *connect1688* to join WeChat discussion group.
|
**WeChat Group:** Add WeChat id *connect1688* to join WeChat discussion group.
|
||||||
|
More info on [partners and users](partners.md).
|
||||||
|
|
||||||
### Contribution
|
### Contribution
|
||||||
We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). Contact us for any questions: harbor @vmware.com .
|
We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). Contact us for any questions: harbor @vmware.com .
|
||||||
@ -46,13 +47,3 @@ This project uses open source components which have additional licensing terms.
|
|||||||
* Docker Registry 2.5: [docker image](https://hub.docker.com/_/registry/), [license](https://github.com/docker/distribution/blob/master/LICENSE)
|
* Docker Registry 2.5: [docker image](https://hub.docker.com/_/registry/), [license](https://github.com/docker/distribution/blob/master/LICENSE)
|
||||||
* MySQL 5.6: [docker image](https://hub.docker.com/_/mysql/), [license](https://github.com/docker-library/mysql/blob/master/LICENSE)
|
* MySQL 5.6: [docker image](https://hub.docker.com/_/mysql/), [license](https://github.com/docker-library/mysql/blob/master/LICENSE)
|
||||||
* NGINX 1.11.5: [docker image](https://hub.docker.com/_/nginx/), [license](https://github.com/nginxinc/docker-nginx/blob/master/LICENSE)
|
* NGINX 1.11.5: [docker image](https://hub.docker.com/_/nginx/), [license](https://github.com/nginxinc/docker-nginx/blob/master/LICENSE)
|
||||||
|
|
||||||
### Partners
|
|
||||||
<a href="https://www.shurenyun.com/" border="0" target="_blank"><img alt="DataMan" src="docs/img/dataman.png"></a> <a href="http://www.slamtec.com" target="_blank" border="0"><img alt="SlamTec" src="docs/img/slamteclogo.png"></a>
|
|
||||||
<a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
|
|
||||||
|
|
||||||
### Users
|
|
||||||
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.dianrong.com/" border="0" target="_blank"><img alt="Dianrong" src="docs/img/dianrong.png"></a>
|
|
||||||
|
|
||||||
### Supporting Technologies
|
|
||||||
<img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>.
|
|
||||||
|
29
contrib/registryapi/README.md
Normal file
29
contrib/registryapi/README.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# registryapi
|
||||||
|
api for docker registry by token authorization
|
||||||
|
|
||||||
|
+ a simple api class which lies in registryapi.py, which simulates the interactions
|
||||||
|
between docker registry and the vendor authorization platform like harbor.
|
||||||
|
```
|
||||||
|
usage:
|
||||||
|
from registryapi import RegistryApi
|
||||||
|
api = RegistryApi('username', 'password', 'http://www.your_registry_url.com/')
|
||||||
|
repos = api.getRepositoryList()
|
||||||
|
tags = api.getTagList('public/ubuntu')
|
||||||
|
manifest = api.getManifest('public/ubuntu', 'latest')
|
||||||
|
res = api.deleteManifest('public/ubuntu', '23424545**4343')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
+ a simple client tool based on api class, which contains basic read and delete
|
||||||
|
operations for repo, tag, manifest
|
||||||
|
```
|
||||||
|
usage:
|
||||||
|
./cli.py --username username --password passwrod --registry_endpoint http://www.your_registry_url.com/ target action params
|
||||||
|
|
||||||
|
target can be: repo, tag, manifest
|
||||||
|
action can be: list, get, delete
|
||||||
|
params can be: --repo --ref --tag
|
||||||
|
|
||||||
|
more see: ./cli.py -h
|
||||||
|
|
||||||
|
```
|
135
contrib/registryapi/cli.py
Executable file
135
contrib/registryapi/cli.py
Executable file
@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
# bug-report: feilengcui008@gmail.com
|
||||||
|
|
||||||
|
""" cli tool """
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from registry import RegistryApi
|
||||||
|
|
||||||
|
|
||||||
|
class ApiProxy(object):
|
||||||
|
""" user RegistryApi """
|
||||||
|
def __init__(self, registry, args):
|
||||||
|
self.registry = registry
|
||||||
|
self.args = args
|
||||||
|
self.callbacks = dict()
|
||||||
|
self.register_callback("repo", "list", self.list_repo)
|
||||||
|
self.register_callback("tag", "list", self.list_tag)
|
||||||
|
self.register_callback("tag", "delete", self.delete_tag)
|
||||||
|
self.register_callback("manifest", "list", self.list_manifest)
|
||||||
|
self.register_callback("manifest", "delete", self.delete_manifest)
|
||||||
|
self.register_callback("manifest", "get", self.get_manifest)
|
||||||
|
|
||||||
|
def register_callback(self, target, action, func):
|
||||||
|
""" register real actions """
|
||||||
|
if not target in self.callbacks.keys():
|
||||||
|
self.callbacks[target] = {action: func}
|
||||||
|
return
|
||||||
|
self.callbacks[target][action] = func
|
||||||
|
|
||||||
|
def execute(self, target, action):
|
||||||
|
""" execute """
|
||||||
|
print json.dumps(self.callbacks[target][action](), indent=4, sort_keys=True)
|
||||||
|
|
||||||
|
def list_repo(self):
|
||||||
|
""" list repo """
|
||||||
|
return self.registry.getRepositoryList(self.args.num)
|
||||||
|
|
||||||
|
def list_tag(self):
|
||||||
|
""" list tag """
|
||||||
|
return self.registry.getTagList(self.args.repo)
|
||||||
|
|
||||||
|
def delete_tag(self):
|
||||||
|
""" delete tag """
|
||||||
|
(_, ref) = self.registry.existManifest(self.args.repo, self.args.tag)
|
||||||
|
if ref is not None:
|
||||||
|
return self.registry.deleteManifest(self.args.repo, ref)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_manifest(self):
|
||||||
|
""" list manifest """
|
||||||
|
tags = self.registry.getTagList(self.args.repo)["tags"]
|
||||||
|
manifests = list()
|
||||||
|
if tags is None:
|
||||||
|
return None
|
||||||
|
for i in tags:
|
||||||
|
content = self.registry.getManifestWithConf(self.args.repo, i)
|
||||||
|
manifests.append({i: content})
|
||||||
|
return manifests
|
||||||
|
|
||||||
|
def delete_manifest(self):
|
||||||
|
""" delete manifest """
|
||||||
|
return self.registry.deleteManifest(self.args.repo, self.args.ref)
|
||||||
|
|
||||||
|
def get_manifest(self):
|
||||||
|
""" get manifest """
|
||||||
|
return self.registry.getManifestWithConf(self.args.repo, self.args.tag)
|
||||||
|
|
||||||
|
|
||||||
|
# since just a script tool, we do not construct whole target->action->args
|
||||||
|
# structure with oo abstractions which has more flexibility, just register
|
||||||
|
# parser directly
|
||||||
|
def get_parser():
|
||||||
|
""" return a parser """
|
||||||
|
parser = argparse.ArgumentParser("cli")
|
||||||
|
|
||||||
|
parser.add_argument('--username', action='store', required=True, help='username')
|
||||||
|
parser.add_argument('--password', action='store', required=True, help='password')
|
||||||
|
parser.add_argument('--registry_endpoint', action='store', required=True,
|
||||||
|
help='registry endpoint')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest='target', help='target to operate on')
|
||||||
|
|
||||||
|
# repo target
|
||||||
|
repo_target_parser = subparsers.add_parser('repo', help='target repository')
|
||||||
|
repo_target_subparsers = repo_target_parser.add_subparsers(dest='action',
|
||||||
|
help='repository subcommand')
|
||||||
|
repo_cmd_parser = repo_target_subparsers.add_parser('list', help='list repositories')
|
||||||
|
repo_cmd_parser.add_argument('--num', action='store', required=False, default=None,
|
||||||
|
help='the number of data to return')
|
||||||
|
|
||||||
|
# tag target
|
||||||
|
tag_target_parser = subparsers.add_parser('tag', help='target tag')
|
||||||
|
tag_target_subparsers = tag_target_parser.add_subparsers(dest='action',
|
||||||
|
help='tag subcommand')
|
||||||
|
tag_list_parser = tag_target_subparsers.add_parser('list', help='list tags')
|
||||||
|
tag_list_parser.add_argument('--repo', action='store', required=True, help='list tags')
|
||||||
|
tag_delete_parser = tag_target_subparsers.add_parser('delete', help='delete tag')
|
||||||
|
tag_delete_parser.add_argument('--repo', action='store', required=True, help='delete tags')
|
||||||
|
tag_delete_parser.add_argument('--tag', action='store', required=True,
|
||||||
|
help='tag reference')
|
||||||
|
|
||||||
|
# manifest target
|
||||||
|
manifest_target_parser = subparsers.add_parser('manifest', help='target manifest')
|
||||||
|
manifest_target_subparsers = manifest_target_parser.add_subparsers(dest='action',
|
||||||
|
help='manifest subcommand')
|
||||||
|
manifest_list_parser = manifest_target_subparsers.add_parser('list', help='list manifests')
|
||||||
|
manifest_list_parser.add_argument('--repo', action='store', required=True,
|
||||||
|
help='list manifests')
|
||||||
|
manifest_delete_parser = manifest_target_subparsers.add_parser('delete', help='delete manifest')
|
||||||
|
manifest_delete_parser.add_argument('--repo', action='store', required=True,
|
||||||
|
help='delete manifest')
|
||||||
|
manifest_delete_parser.add_argument('--ref', action='store', required=True,
|
||||||
|
help='manifest reference')
|
||||||
|
manifest_get_parser = manifest_target_subparsers.add_parser('get', help='get manifest content')
|
||||||
|
manifest_get_parser.add_argument('--repo', action='store', required=True, help='delete tags')
|
||||||
|
manifest_get_parser.add_argument('--tag', action='store', required=True,
|
||||||
|
help='manifest reference')
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
""" main entrance """
|
||||||
|
parser = get_parser()
|
||||||
|
options = parser.parse_args(sys.argv[1:])
|
||||||
|
registry = RegistryApi(options.username, options.password, options.registry_endpoint)
|
||||||
|
proxy = ApiProxy(registry, options)
|
||||||
|
proxy.execute(options.target, options.action)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
165
contrib/registryapi/registry.py
Normal file
165
contrib/registryapi/registry.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
# bug-report: feilengcui008@gmail.com
|
||||||
|
|
||||||
|
""" api for docker registry """
|
||||||
|
|
||||||
|
import urllib2
|
||||||
|
import urllib
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryException(Exception):
|
||||||
|
""" registry api related exception """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryApi(object):
|
||||||
|
""" interact with docker registry and harbor """
|
||||||
|
def __init__(self, username, password, registry_endpoint):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.basic_token = base64.encodestring("%s:%s" % (str(username), str(password)))[0:-1]
|
||||||
|
self.registry_endpoint = registry_endpoint.rstrip('/')
|
||||||
|
auth = self.pingRegistry("%s/v2/_catalog" % (self.registry_endpoint,))
|
||||||
|
if auth is None:
|
||||||
|
raise RegistryException("get token realm and service failed")
|
||||||
|
self.token_endpoint = auth[0]
|
||||||
|
self.service = auth[1]
|
||||||
|
|
||||||
|
def pingRegistry(self, registry_endpoint):
|
||||||
|
""" ping v2 registry and get realm and service """
|
||||||
|
headers = dict()
|
||||||
|
try:
|
||||||
|
res = urllib2.urlopen(registry_endpoint)
|
||||||
|
except urllib2.HTTPError as e:
|
||||||
|
headers = e.hdrs.dict
|
||||||
|
try:
|
||||||
|
(realm, service, _) = headers['www-authenticate'].split(',')
|
||||||
|
return (realm[14:-1:], service[9:-1])
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getBearerTokenForScope(self, scope):
|
||||||
|
""" get bearer token from harbor """
|
||||||
|
payload = urllib.urlencode({'service': self.service, 'scope': scope})
|
||||||
|
url = "%s?%s" % (self.token_endpoint, payload)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.add_header('Authorization', 'Basic %s' % (self.basic_token,))
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
return json.loads(response.read())["token"]
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getRepositoryList(self, n=None):
|
||||||
|
""" get repository list """
|
||||||
|
scope = "registry:catalog:*"
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
return None
|
||||||
|
url = "%s/v2/_catalog" % (self.registry_endpoint,)
|
||||||
|
if n is not None:
|
||||||
|
url = "%s?n=%s" % (url, str(n))
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
return json.loads(response.read())
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getTagList(self, repository):
|
||||||
|
""" get tag list for repository """
|
||||||
|
scope = "repository:%s:pull" % (repository,)
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
return None
|
||||||
|
url = "%s/v2/%s/tags/list" % (self.registry_endpoint, repository)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
return json.loads(response.read())
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getManifest(self, repository, reference="latest", v1=False):
|
||||||
|
""" get manifest for tag or digest """
|
||||||
|
scope = "repository:%s:pull" % (repository,)
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
return None
|
||||||
|
url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, reference)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.get_method = lambda: 'GET'
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json')
|
||||||
|
if v1:
|
||||||
|
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v1+json')
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
return json.loads(response.read())
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def existManifest(self, repository, reference, v1=False):
|
||||||
|
""" check to see it manifest exist """
|
||||||
|
scope = "repository:%s:pull" % (repository,)
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
raise RegistryException("manifestExist failed due to token error")
|
||||||
|
url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, reference)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.get_method = lambda: 'HEAD'
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json')
|
||||||
|
if v1:
|
||||||
|
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v1+json')
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
return (True, response.headers.dict["docker-content-digest"])
|
||||||
|
except Exception as e:
|
||||||
|
return (False, None)
|
||||||
|
|
||||||
|
def deleteManifest(self, repository, reference):
|
||||||
|
""" delete manifest by tag """
|
||||||
|
(is_exist, digest) = self.existManifest(repository, reference)
|
||||||
|
if not is_exist:
|
||||||
|
raise RegistryException("manifest not exist")
|
||||||
|
scope = "repository:%s:pull,push" % (repository,)
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
raise RegistryException("delete manifest failed due to token error")
|
||||||
|
url = "%s/v2/%s/manifests/%s" % (self.registry_endpoint, repository, digest)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.get_method = lambda: 'DELETE'
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
try:
|
||||||
|
urllib2.urlopen(req)
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getManifestWithConf(self, repository, reference="latest"):
|
||||||
|
""" get manifest for tag or digest """
|
||||||
|
manifest = self.getManifest(repository, reference)
|
||||||
|
if manifest is None:
|
||||||
|
raise RegistryException("manifest for %s %s not exist" % (repository, reference))
|
||||||
|
config_digest = manifest["config"]["digest"]
|
||||||
|
scope = "repository:%s:pull" % (repository,)
|
||||||
|
bear_token = self.getBearerTokenForScope(scope)
|
||||||
|
if bear_token is None:
|
||||||
|
return None
|
||||||
|
url = "%s/v2/%s/blobs/%s" % (self.registry_endpoint, repository, config_digest)
|
||||||
|
req = urllib2.Request(url)
|
||||||
|
req.get_method = lambda: 'GET'
|
||||||
|
req.add_header('Authorization', r'Bearer %s' % (bear_token,))
|
||||||
|
req.add_header('Accept', 'application/vnd.docker.distribution.manifest.v2+json')
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(req)
|
||||||
|
manifest["configContent"] = json.loads(response.read())
|
||||||
|
return manifest
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
@ -26,6 +26,9 @@ Guide to deploy Harbor on Kubenetes. (maintained by community)
|
|||||||
[Architecture Overview of Harbor](https://github.com/vmware/harbor/wiki/Architecture-Overview-of-Harbor)
|
[Architecture Overview of Harbor](https://github.com/vmware/harbor/wiki/Architecture-Overview-of-Harbor)
|
||||||
Developers read this first.
|
Developers read this first.
|
||||||
|
|
||||||
|
[Build Harbor from Source](compile_guide.md)
|
||||||
|
How to build Harbor from source code.
|
||||||
|
|
||||||
[Harbor API Specs by Swagger](configure_swagger.md)
|
[Harbor API Specs by Swagger](configure_swagger.md)
|
||||||
Use Swagger to find out the specs of Harbor API.
|
Use Swagger to find out the specs of Harbor API.
|
||||||
|
|
||||||
|
@ -1,65 +1,142 @@
|
|||||||
## makeing Harbor on Kubernetes
|
|
||||||
To deploy Harbor on Kubernetes, it requires some additional steps because
|
|
||||||
1. When Harbor registry uses https, so we need cert or workaround to avoid errors like this:
|
|
||||||
```
|
|
||||||
Error response from daemon: invalid registry endpoint https://{HOST}/v0/: unable to ping registry endpoint https://{HOST}/v0/
|
|
||||||
v2 ping attempt failed with error: Get https://{HOST}/v2/: EOF
|
|
||||||
v1 ping attempt failed with error: Get https://{HOST}/v1/_ping: EOF. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry {HOST}` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/{HOST}/ca.crt
|
|
||||||
```
|
|
||||||
There is a workaround if you don't have a cert. The workaround is to add the host into the list of insecure registry by editting the ```/etc/default/docker``` file:
|
|
||||||
```
|
|
||||||
sudo vi /etc/default/docker
|
|
||||||
```
|
|
||||||
add the line at the end of file:
|
|
||||||
```
|
|
||||||
DOCKER_OPTS="$DOCKER_OPTS --insecure-registry={HOST}"
|
|
||||||
```
|
|
||||||
restart docker service
|
|
||||||
```
|
|
||||||
sudo service docker restart
|
|
||||||
```
|
|
||||||
|
|
||||||
2. The registry config file needs to have the IP (or DNS name) of the registry, but on Kubernetes, you don't know the IP before the service is created. There are several workarounds to solve this problem for now:
|
## Integration with Kubernetes
|
||||||
- Use DNS name and link the DNS name with the IP after the service is created.
|
This Document decribes how to deploy Harbor on Kubernetes.
|
||||||
- Rebuild the registry image with the service IP after the service is created and use ```kubectl rolling-update``` to update to the new image.
|
|
||||||
|
|
||||||
|
### Prerequisite
|
||||||
|
* You need to download docker images of Harbor.
|
||||||
|
* Download the offline installer of Harbor from the [release](https://github.com/vmware/harbor/releases) page.
|
||||||
|
* Uncompress the offline installer and get the images tgz file harbor.*.tgz.
|
||||||
|
* Load the images into docker:
|
||||||
|
```
|
||||||
|
docker load -i harbor.*.tgz
|
||||||
|
```
|
||||||
|
* You should have domain knowledge about Kubernetes (Replication Controller, Service, Persistent Volume, Persistent Volume Claim, Config Map).
|
||||||
|
|
||||||
To start Harbor on Kubernetes, you first need to build the docker images. The docker images for deploying Harbor on Kubernetes depends on the docker images to deploy Harbor with docker-compose. So the first step is to build docker images with docker-compose. Before actually building the images, you need to first adjust the [configuration](https://github.com/vmware/harbor/blob/master/make/harbor.cfg):
|
### Configuration
|
||||||
- Change the [hostname](https://github.com/vmware/harbor/blob/master/make/harbor.cfg#L5) to ```localhost```
|
We provide a python script `make/kubernetes/prepare` to generate Kubernetes ConfigMap files.
|
||||||
- Adjust the [email settings](https://github.com/vmware/harbor/blob/master/make/harbor.cfg#L11) according to your needs.
|
The script is written in python, so you need a version of python in your deployment environment.
|
||||||
|
Also the script need `openssl` to generate private key and certification, make sure you have a workable `openssl`.
|
||||||
|
|
||||||
|
There are some args of the python script:
|
||||||
|
- -f: Default Value is `../harbor.cfg`. You can specify other config file of Harbor.
|
||||||
|
- -k: Path to https private key. This arg can overwrite the value of `ssl_cert_key` in `harbor.cfg`.
|
||||||
|
- -c: Path to https certification. This arg can overwrite the value of `ssl_cert` in `harbor.cfg`.
|
||||||
|
- -s: Path to secret key. Must be 16 characters. If you don't set it, the script will generate it automatically.
|
||||||
|
|
||||||
|
#### Basic Configuration
|
||||||
|
These Basic Configuration must be set. Otherwise you can't deploy Harbor on Kubernetes.
|
||||||
|
- `make/harbor.cfg`: Basic config of Harbor. Please refer to `harbor.cfg`.
|
||||||
|
- `make/kubernetes/**/*.rc.yaml`: Specify configs of containers.
|
||||||
|
You need to specify the path to your images in all `*.rc.yaml`. example:
|
||||||
|
|
||||||
|
```
|
||||||
|
containers:
|
||||||
|
- name: nginx-app
|
||||||
|
# it's very importent that you need modify the path of image.
|
||||||
|
image: harbor/nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
- `make/kubernetes/pv/*.pvc.yaml`: Persistent Volume Claim.
|
||||||
|
You can set capacity of storage in these files. example:
|
||||||
|
|
||||||
|
```
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
# you can set another value to adapt to your needs
|
||||||
|
storage: 100Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
- `make/kubernetes/pv/*.pv.yaml`: Persistent Volume. Be bound with `*.pvc.yaml`.
|
||||||
|
PVs and PVCs are one to one correspondence. If you changed capacity of PVC, you need to set capacity of PV together.
|
||||||
|
example:
|
||||||
|
|
||||||
|
```
|
||||||
|
capacity:
|
||||||
|
# same value with PVC
|
||||||
|
storage: 100Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
In PV, you should set another way to store data rather than `hostPath`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# it's default value, you should use others like nfs.
|
||||||
|
hostPath:
|
||||||
|
path: /data/registry
|
||||||
|
```
|
||||||
|
|
||||||
|
For more infomation about store ways, Please check [Kubernetes Document](http://kubernetes.io/docs/user-guide/persistent-volumes/)
|
||||||
|
|
||||||
|
Then you can generate ConfigMap files by :
|
||||||
|
|
||||||
Then you can run the following commends to build docker images:
|
|
||||||
```
|
```
|
||||||
cd make
|
python make/kubernetes/prepare
|
||||||
./prepare
|
|
||||||
docker-compose build
|
|
||||||
docker build -f kubernetes/dockerfiles/proxy-dockerfile -t {your_account}/proxy .
|
|
||||||
docker build -f kubernetes/dockerfiles/registry-dockerfile -t {your_account}/registry .
|
|
||||||
docker build -f kubernetes/dockerfiles/ui-dockerfile -t {your_account}/deploy_ui .
|
|
||||||
docker tag deploy_mysql {your_account}/deploy_mysql
|
|
||||||
docker push {your_account}/proxy
|
|
||||||
docker push {your_account}/registry
|
|
||||||
docker push {your_account}/deploy_ui
|
|
||||||
docker push {your_account}/deploy_mysql
|
|
||||||
```
|
```
|
||||||
|
|
||||||
where "your_account" is your own registry. Then you need to update the "image" field in the ```*-rc.yaml``` files at:
|
These files will be generated:
|
||||||
```
|
- make/kubernetes/jobservice/jobservice.cm.yaml
|
||||||
make/kubernetes/mysql-rc.yaml
|
- make/kubernetes/mysql/mysql.cm.yaml
|
||||||
make/kubernetes/proxy-rc.yaml
|
- make/kubernetes/nginx/nginx.cm.yaml
|
||||||
make/kubernetes/registry-rc.yaml
|
- make/kubernetes/registry/registry.cm.yaml
|
||||||
make/kubernetes/ui-rc.yaml
|
- make/kubernetes/ui/ui.cm.yaml
|
||||||
```
|
|
||||||
|
|
||||||
Further more, the following configuration could be changed according to your need:
|
#### Advanced Configuration
|
||||||
- **harbor_admin_password**: The password for the administrator of Harbor, by default the password is Harbor12345. You can changed it [here](https://github.com/vmware/harbor/blob/master/make/kubernetes/ui-rc.yaml#L36).
|
If Basic Configuration was not covering your requirements, you can read this section for more details.
|
||||||
- **auth_mode**: The authentication mode of Harbor. By default it is *db_auth*, i.e. the credentials are stored in a database. Please set it to *ldap_auth* if you want to verify user's credentials against an LDAP server. You can change the configuration [here](https://github.com/vmware/harbor/blob/master/make/kubernetes/ui-rc.yaml#L40).
|
|
||||||
- **ldap_url**: The URL for LDAP endpoint, for example ldaps://ldap.mydomain.com. It is only used when **auth_mode** is set to *ldap_auth*. It could be changed [here](https://github.com/vmware/harbor/blob/master/make/kubernetes/ui-rc.yaml#L42).
|
|
||||||
- **ldap_basedn**: The basedn template for verifying the user's credentials against LDAP, for example uid=%s,ou=people,dc=mydomain,dc=com. It is only used when **auth_mode** is set to *ldap_auth*. It could be changed [here](https://github.com/vmware/harbor/blob/master/make/kubernetes/ui-rc.yaml#L44).
|
|
||||||
- **db_password**: The password of root user of mySQL database. Change this password for any production use. You need to change both [here](https://github.com/vmware/harbor/blob/master/make/kubernetes/ui-rc.yaml#L28) and [here](https://github.com/vmware/harbor/blob/master/make/harbor.cfg#L32) to make the change. Please note, you need to change the ```harbor.cfg``` before building the docker images.
|
|
||||||
|
|
||||||
Finally you can start the jobs by running:
|
`./prepare` has a specify format of placeholder:
|
||||||
```
|
- `{{key}}`: It means we should replace the placeholder with the value in `config.cfg` which name is `key`.
|
||||||
kubectl create -f make/kubernetes
|
- `{{num key}}`: It's used for multiple lines text. It will add `num` spaces to the leading of every line in text.
|
||||||
```
|
|
||||||
|
|
||||||
|
You can find all configs of Harbor in `make/kubernetes/templates/`. There are specifications of these files:
|
||||||
|
- `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.
|
||||||
|
- `registry.cm.yaml`: Token service certification and registry config
|
||||||
|
Registry use filesystem to store data of images. You can find it like:
|
||||||
|
|
||||||
|
```
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
rootdirectory: /storage
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want use another storage backend, please see [Docker Doc](https://docs.docker.com/datacenter/dtr/2.1/guides/configure/configure-storage/)
|
||||||
|
- `ui.cm.yaml`: Token service private key, ENV and web config of ui
|
||||||
|
|
||||||
|
`ui` and `jobservice` are powered by beego. If you are fimiliar with beego, you can modify configs in `jobservice.cm.yaml` and `ui.cm.yaml`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Running
|
||||||
|
When you finished your configuring and generated ConfigMap files, you can run Harbor on kubernetes with these commands:
|
||||||
|
```
|
||||||
|
# create pv & pvc
|
||||||
|
kubectl apply -f make/kubernetes/pv/log.pv.yaml
|
||||||
|
kubectl apply -f make/kubernetes/pv/registry.pv.yaml
|
||||||
|
kubectl apply -f make/kubernetes/pv/storage.pv.yaml
|
||||||
|
kubectl apply -f make/kubernetes/pv/log.pvc.yaml
|
||||||
|
kubectl apply -f make/kubernetes/pv/registry.pvc.yaml
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```
|
||||||
|
@ -9,23 +9,21 @@ When upgrading your existing Habor instance to a newer version, you may need to
|
|||||||
|
|
||||||
### Upgrading Harbor and migrating data
|
### Upgrading Harbor and migrating data
|
||||||
|
|
||||||
1. Log in to the machine that Harbor runs on, stop and remove existing Harbor service if it is still running:
|
1. Log in to the host that Harbor runs on, stop and remove existing Harbor instance if it is still running:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd make/
|
cd harbor
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Back up Harbor's current source code so that you can roll back to the current version when it is necessary.
|
2. Back up Harbor's current files so that you can roll back to the current version when it is necessary.
|
||||||
```sh
|
```sh
|
||||||
cd ../..
|
cd ..
|
||||||
mv harbor /tmp/harbor
|
mv harbor /tmp/harbor
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Get the lastest source code from Github:
|
3. Get the lastest Harbor release package from Github:
|
||||||
```sh
|
https://github.com/vmware/harbor/releases
|
||||||
git clone https://github.com/vmware/harbor
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Before upgrading Harbor, perform database migration first. The migration tool is delivered as a docker image, so you should pull the image from docker hub:
|
4. Before upgrading Harbor, perform database migration first. The migration tool is delivered as a docker image, so you should pull the image from docker hub:
|
||||||
|
|
||||||
@ -45,16 +43,13 @@ When upgrading your existing Habor instance to a newer version, you may need to
|
|||||||
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql vmware/harbor-db-migrator up head
|
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql vmware/harbor-db-migrator up head
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Change to `make/` directory, configure Harbor by modifying the file `harbor.cfg`, you may need to refer to the configuration files you've backed up during step 2. Refer to [Installation & Configuration Guide ](../docs/installation_guide.md) for more info.
|
7. Unzip the new Harbor package and change to `./harbor` as the working directory. Configure Harbor by modifying the file `harbor.cfg`,
|
||||||
|
you may need to refer to the configuration files you've backed up during step 2.
|
||||||
|
Refer to [Installation & Configuration Guide ](../docs/installation_guide.md) for more information.
|
||||||
|
Since the content and format of `harbor.cfg` may have been changed in the new release, **DO NOT directly copy `harbor.cfg` from previous version of Harbor.**
|
||||||
|
|
||||||
|
|
||||||
8. Under the directory `make/`, run the `./prepare` script to generate necessary config files.
|
8. Under the directory `./harbor`, run the `./install.sh` script to install the new Harbor instance.
|
||||||
|
|
||||||
9. Rebuild Harbor and restart the registry service
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Roll back from an upgrade
|
### Roll back from an upgrade
|
||||||
For any reason, if you want to roll back to the previous version of Harbor, follow the below steps:
|
For any reason, if you want to roll back to the previous version of Harbor, follow the below steps:
|
||||||
@ -62,7 +57,7 @@ For any reason, if you want to roll back to the previous version of Harbor, foll
|
|||||||
1. Stop and remove the current Harbor service if it is still running.
|
1. Stop and remove the current Harbor service if it is still running.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd make/
|
cd harbor
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
2. Restore database from backup file in `/path/to/backup` .
|
2. Restore database from backup file in `/path/to/backup` .
|
||||||
@ -71,19 +66,26 @@ For any reason, if you want to roll back to the previous version of Harbor, foll
|
|||||||
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup vmware/harbor-db-migrator restore
|
docker run -ti --rm -e DB_USR=root -e DB_PWD=xxxx -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup vmware/harbor-db-migrator restore
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove current source code of Harbor.
|
3. Remove current Harbor instance.
|
||||||
```
|
```
|
||||||
rm -rf harbor
|
rm -rf harbor
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Restore the source code of an older version of Harbor.
|
4. Restore the older version package of Harbor.
|
||||||
```sh
|
```sh
|
||||||
mv /tmp/harbor harbor
|
mv /tmp/harbor harbor
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Restart Harbor service using the previous configuration.
|
5. Restart Harbor service using the previous configuration.
|
||||||
|
If previous version of Harbor was installed by a release build:
|
||||||
```sh
|
```sh
|
||||||
cd make/
|
cd harbor
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If your previous version of Harbor was installed from source code:
|
||||||
|
```sh
|
||||||
|
cd harbor
|
||||||
docker-compose up --build -d
|
docker-compose up --build -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
FROM library/nginx:1.11.5
|
|
||||||
|
|
||||||
ADD ./config/nginx /etc/nginx
|
|
@ -1,33 +0,0 @@
|
|||||||
version: 0.1
|
|
||||||
log:
|
|
||||||
level: debug
|
|
||||||
fields:
|
|
||||||
service: registry
|
|
||||||
storage:
|
|
||||||
cache:
|
|
||||||
layerinfo: inmemory
|
|
||||||
filesystem:
|
|
||||||
rootdirectory: /storage
|
|
||||||
maintenance:
|
|
||||||
uploadpurging:
|
|
||||||
enabled: false
|
|
||||||
http:
|
|
||||||
addr: :5000
|
|
||||||
secret: placeholder
|
|
||||||
debug:
|
|
||||||
addr: localhost:5001
|
|
||||||
auth:
|
|
||||||
token:
|
|
||||||
issuer: registry-token-issuer
|
|
||||||
realm: http://harbor.caicloud.io/service/token
|
|
||||||
rootcertbundle: /etc/registry/root.crt
|
|
||||||
service: token-service
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
endpoints:
|
|
||||||
- name: harbor
|
|
||||||
disabled: false
|
|
||||||
url: http://harbor.caicloud.io/service/notifications
|
|
||||||
timeout: 500
|
|
||||||
threshold: 5
|
|
||||||
backoff: 1000
|
|
@ -1,6 +0,0 @@
|
|||||||
FROM library/registry:2.3.0
|
|
||||||
|
|
||||||
ADD ./config/registry/ /etc/registry/
|
|
||||||
ADD ./kubernetes/dockerfiles/registry-config.yml /etc/registry/config.yml
|
|
||||||
|
|
||||||
CMD ["/etc/registry/config.yml"]
|
|
@ -1,4 +0,0 @@
|
|||||||
FROM deploy_ui
|
|
||||||
|
|
||||||
ADD ./config/ui/app.conf /etc/ui/app.conf
|
|
||||||
ADD ./config/ui/private_key.pem /etc/ui/private_key.pem
|
|
112
make/kubernetes/jobservice/jobservice.rc.yaml
Normal file
112
make/kubernetes/jobservice/jobservice.rc.yaml
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ReplicationController
|
||||||
|
metadata:
|
||||||
|
name: jobservice-rc
|
||||||
|
labels:
|
||||||
|
name: jobservice-rc
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
name: jobservice-apps
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
name: jobservice-apps
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: jobservice-app
|
||||||
|
image: harbor/jobservice
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: MYSQL_HOST
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: MYSQL_HOST
|
||||||
|
- name: MYSQL_PORT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: MYSQL_PORT
|
||||||
|
- name: MYSQL_USR
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: MYSQL_USR
|
||||||
|
- name: MYSQL_PWD
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: MYSQL_PWD
|
||||||
|
- name: UI_SECRET
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: UI_SECRET
|
||||||
|
- name: SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: SECRET_KEY
|
||||||
|
- name: CONFIG_PATH
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: CONFIG_PATH
|
||||||
|
- name: REGISTRY_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: REGISTRY_URL
|
||||||
|
- name: VERIFY_REMOTE_CERT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: VERIFY_REMOTE_CERT
|
||||||
|
- name: MAX_JOB_WORKERS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: MAX_JOB_WORKERS
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: LOG_LEVEL
|
||||||
|
- name: LOG_DIR
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: LOG_DIR
|
||||||
|
- name: GODEBUG
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: GODEBUG
|
||||||
|
- name: EXT_ENDPOINT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: EXT_ENDPOINT
|
||||||
|
- name: TOKEN_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
key: TOKEN_URL
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/jobservice
|
||||||
|
- name: logs
|
||||||
|
mountPath: /var/log/jobs
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
items:
|
||||||
|
- key: config
|
||||||
|
path: app.conf
|
||||||
|
- name: logs
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: log-pvc
|
9
make/kubernetes/jobservice/jobservice.svc.yaml
Normal file
9
make/kubernetes/jobservice/jobservice.svc.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: jobservice
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
selector:
|
||||||
|
name: jobservice-apps
|
@ -1,30 +1,34 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ReplicationController
|
kind: ReplicationController
|
||||||
metadata:
|
metadata:
|
||||||
name: mysql
|
name: mysql-rc
|
||||||
labels:
|
labels:
|
||||||
name: mysql
|
name: mysql-rc
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
name: mysql
|
name: mysql-apps
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
name: mysql
|
name: mysql-apps
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: mysql
|
- name: mysql-app
|
||||||
image: caicloud/harbor_deploy_mysql:latest
|
image: harbor/mysql
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 3306
|
- containerPort: 3306
|
||||||
env:
|
env:
|
||||||
- name: MYSQL_ROOT_PASSWORD
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
value: root123
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-mysql-config
|
||||||
|
key: MYSQL_ROOT_PASSWORD
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: mysql-storage
|
- name: mysql-storage
|
||||||
mountPath: /var/lib/mysql
|
mountPath: /var/lib/mysql
|
||||||
volumes:
|
volumes:
|
||||||
- name: mysql-storage
|
- name: mysql-storage
|
||||||
emptyDir: {}
|
persistentVolumeClaim:
|
||||||
|
claimName: storage-pvc
|
@ -2,10 +2,8 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: mysql
|
name: mysql
|
||||||
labels:
|
|
||||||
name: mysql
|
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- port: 3306
|
- port: 3306
|
||||||
selector:
|
selector:
|
||||||
name: mysql
|
name: mysql-apps
|
36
make/kubernetes/nginx/nginx.rc.yaml
Normal file
36
make/kubernetes/nginx/nginx.rc.yaml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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: harbor/nginx
|
||||||
|
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
|
@ -1,15 +1,12 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: proxy
|
name: nginx
|
||||||
labels:
|
|
||||||
name: proxy
|
|
||||||
spec:
|
spec:
|
||||||
type: LoadBalancer
|
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
port: 80
|
port: 80
|
||||||
- name: https
|
- name: https
|
||||||
port: 443
|
port: 443
|
||||||
selector:
|
selector:
|
||||||
name: proxy
|
name: nginx-apps
|
201
make/kubernetes/prepare
Normal file
201
make/kubernetes/prepare
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import print_function, unicode_literals # We require Python 2.6 or later
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
if sys.version_info[:3][0] == 2:
|
||||||
|
import ConfigParser as configparser
|
||||||
|
import StringIO as io
|
||||||
|
|
||||||
|
if sys.version_info[:3][0] == 3:
|
||||||
|
import configparser as configparser
|
||||||
|
import io as io
|
||||||
|
|
||||||
|
|
||||||
|
# prepare base dir
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Generate *.cm.yaml')
|
||||||
|
parser.add_argument('-f', default=os.path.join(base_dir, '../harbor.cfg'),
|
||||||
|
dest='config_file', help='[Optional] path of harbor config file')
|
||||||
|
parser.add_argument('-k', default='',
|
||||||
|
dest='private_key', help='[Optional] path of harbor https private key(pem)')
|
||||||
|
parser.add_argument('-c', default='',
|
||||||
|
dest='cert', help='[Optional] harbor path of https cert(pem)')
|
||||||
|
parser.add_argument('-s', default='',
|
||||||
|
dest='secret_key', help="[Optional] path of harbor secret key(16 characters)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# read config file
|
||||||
|
config_str = ''
|
||||||
|
if os.path.isfile(args.config_file):
|
||||||
|
with open(args.config_file) as conf:
|
||||||
|
config_str = conf.read()
|
||||||
|
else:
|
||||||
|
raise Exception('Error: No such file(' + args.config_file + ')')
|
||||||
|
|
||||||
|
config_str = '[harbor]\n' + config_str
|
||||||
|
fp = io.StringIO()
|
||||||
|
fp.write(config_str)
|
||||||
|
fp.seek(0, os.SEEK_SET)
|
||||||
|
config = configparser.RawConfigParser()
|
||||||
|
config.readfp(fp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(key):
|
||||||
|
"""get value by key
|
||||||
|
"""
|
||||||
|
if config.has_option('harbor', key):
|
||||||
|
return config.get('harbor', key)
|
||||||
|
print('Warning: Key(' + key + ') is not existing. Use empty string as default')
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def set_config(key, value):
|
||||||
|
"""set key & value
|
||||||
|
"""
|
||||||
|
config.set('harbor', key, value)
|
||||||
|
|
||||||
|
# relative path with config file
|
||||||
|
def rel_path(p):
|
||||||
|
if p[0] == '/':
|
||||||
|
return p
|
||||||
|
config_path = args.config_file
|
||||||
|
if config_path[0] != '/':
|
||||||
|
config_path = os.path.join(os.getcwd(), config_path)
|
||||||
|
return os.path.join(os.path.dirname(config_path), p)
|
||||||
|
|
||||||
|
# path of private key
|
||||||
|
pk_path = args.private_key
|
||||||
|
if pk_path == '':
|
||||||
|
pk_path = get_config('ssl_cert_key')
|
||||||
|
if pk_path != '':
|
||||||
|
pk_path = rel_path(pk_path)
|
||||||
|
|
||||||
|
# path of cert
|
||||||
|
cert_path = args.cert
|
||||||
|
if cert_path == '':
|
||||||
|
cert_path = get_config('ssl_cert')
|
||||||
|
if cert_path != '':
|
||||||
|
cert_path = rel_path(cert_path)
|
||||||
|
|
||||||
|
|
||||||
|
# validate
|
||||||
|
if get_config('ui_url_protocol') == 'https':
|
||||||
|
if pk_path == '':
|
||||||
|
raise Exception("Error: The protocol is https but attribute ssl_cert_key is not set")
|
||||||
|
if cert_path == '':
|
||||||
|
raise Exception("Error: The protocol is https but attribute ssl_cert is not set")
|
||||||
|
else:
|
||||||
|
pk_path = ''
|
||||||
|
cert_path = ''
|
||||||
|
|
||||||
|
|
||||||
|
# read secret key
|
||||||
|
if args.secret_key != '':
|
||||||
|
if os.path.isfile(args.secret_key):
|
||||||
|
key = ''
|
||||||
|
with open(args.secret_key, 'r') as skey:
|
||||||
|
key = skey.read()
|
||||||
|
if len(key) != 16:
|
||||||
|
raise Exception('Error: The length of secret key has to be 16 characters!')
|
||||||
|
set_config('secret_key', key)
|
||||||
|
else:
|
||||||
|
set_config('secret_key', ''.join(random.choice(
|
||||||
|
string.ascii_letters + string.digits) for i in range(16)))
|
||||||
|
|
||||||
|
# read https pkey & cert
|
||||||
|
if pk_path != '':
|
||||||
|
if os.path.isfile(pk_path):
|
||||||
|
with open(pk_path, 'r') as pkey:
|
||||||
|
set_config('https_pkey', pkey.read())
|
||||||
|
else:
|
||||||
|
raise Exception('Error: https private key is not existing')
|
||||||
|
else:
|
||||||
|
set_config('https_pkey', 'USE_HTTP')
|
||||||
|
|
||||||
|
if cert_path != '':
|
||||||
|
if os.path.isfile(cert_path):
|
||||||
|
with open(cert_path, 'r') as cert:
|
||||||
|
set_config('https_cert', cert.read())
|
||||||
|
else:
|
||||||
|
raise Exception('Error: https cert is not existing')
|
||||||
|
else:
|
||||||
|
set_config('https_cert', 'USE_HTTP')
|
||||||
|
|
||||||
|
|
||||||
|
# add configs
|
||||||
|
set_config('ui_url', get_config('ui_url_protocol') +
|
||||||
|
'://' + get_config('hostname'))
|
||||||
|
set_config('ui_secret', ''.join(random.choice(
|
||||||
|
string.ascii_letters + string.digits) for i in range(16)))
|
||||||
|
|
||||||
|
# generate auth pkey & cert
|
||||||
|
with open(os.devnull, 'w') as devnull:
|
||||||
|
openssl = subprocess.call(['which','openssl'], stdout=devnull, stderr=devnull)
|
||||||
|
if openssl == 0:
|
||||||
|
pkey = subprocess.check_output(['openssl','genrsa','4096'], stderr=devnull)
|
||||||
|
subj = '/C={0}/ST={1}/L={2}/O={3}/OU={4}/CN={5}/emailAddress={6}'.format(get_config('crt_country'),
|
||||||
|
get_config('crt_state'), get_config('crt_location'), get_config('crt_organization'),
|
||||||
|
get_config('crt_organizationalunit'), get_config('crt_commonname'), get_config('crt_email'))
|
||||||
|
openssl = subprocess.Popen(['openssl', 'req', '-new', '-x509', '-key', '/dev/stdin', '-days', '3650', '-subj', subj],
|
||||||
|
stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=devnull)
|
||||||
|
cert = openssl.communicate(input=pkey)[0]
|
||||||
|
set_config('auth_pkey', pkey.decode())
|
||||||
|
set_config('auth_cert', cert.decode())
|
||||||
|
else:
|
||||||
|
set_config('auth_pkey', 'NEED_SET')
|
||||||
|
set_config('auth_cert', 'NEED_SET')
|
||||||
|
print('Warning: auth_pkey and auth_cert cannot be generated automatically without openssl. Please set it manually')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
variable = re.compile(r'{{.+?}}')
|
||||||
|
detail = re.compile(r'((\d+) )?([a-zA-Z_0-9-]+)')
|
||||||
|
def render_template(tmpl):
|
||||||
|
"""render template
|
||||||
|
replace {{(number of leading spaces)name}} with config
|
||||||
|
examples:
|
||||||
|
config:
|
||||||
|
hostname='test\ntest'
|
||||||
|
|
||||||
|
{{hostname}} -> 'test\ntest'
|
||||||
|
{{4 hostname}} -> 'test\n test'
|
||||||
|
"""
|
||||||
|
matches = variable.findall(tmpl)
|
||||||
|
for match in matches:
|
||||||
|
segs = detail.search(match)
|
||||||
|
if segs.group() == '':
|
||||||
|
raise Exception('Error: Invalid template item(' + match + ')')
|
||||||
|
value = get_config(segs.group(3))
|
||||||
|
spaces = segs.group(2)
|
||||||
|
if spaces != '' and spaces != None:
|
||||||
|
leading = ''.join(' ' for i in range(int(spaces)))
|
||||||
|
value = str(value).replace('\n', '\n' + leading)
|
||||||
|
tmpl = tmpl.replace(match, value)
|
||||||
|
return tmpl
|
||||||
|
|
||||||
|
|
||||||
|
def generate_template(tmpl, dest):
|
||||||
|
"""generate file
|
||||||
|
"""
|
||||||
|
with open(tmpl) as tmpl:
|
||||||
|
with open(dest, 'w') as dest:
|
||||||
|
dest.write(render_template(tmpl.read()))
|
||||||
|
|
||||||
|
|
||||||
|
template_dir = os.path.join(base_dir, 'templates')
|
||||||
|
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'))
|
@ -1,22 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ReplicationController
|
|
||||||
metadata:
|
|
||||||
name: proxy
|
|
||||||
labels:
|
|
||||||
name: proxy
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
name: proxy
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
name: proxy
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: proxy
|
|
||||||
image: caicloud/harbor_proxy:latest
|
|
||||||
imagePullPolicy: Always
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
- containerPort: 443
|
|
14
make/kubernetes/pv/log.pv.yaml
Normal file
14
make/kubernetes/pv/log.pv.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: log-pv
|
||||||
|
labels:
|
||||||
|
type: log
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 5Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
hostPath:
|
||||||
|
path: /data/logs
|
13
make/kubernetes/pv/log.pvc.yaml
Normal file
13
make/kubernetes/pv/log.pvc.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: log-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
type: log
|
14
make/kubernetes/pv/registry.pv.yaml
Normal file
14
make/kubernetes/pv/registry.pv.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: registry-pv
|
||||||
|
labels:
|
||||||
|
type: registry
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 100Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
hostPath:
|
||||||
|
path: /data/registry
|
13
make/kubernetes/pv/registry.pvc.yaml
Normal file
13
make/kubernetes/pv/registry.pvc.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: registry-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 100Gi
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
type: registry
|
14
make/kubernetes/pv/storage.pv.yaml
Normal file
14
make/kubernetes/pv/storage.pv.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: storage-pv
|
||||||
|
labels:
|
||||||
|
type: storage
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: 5Gi
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
hostPath:
|
||||||
|
path: /data/storage
|
13
make/kubernetes/pv/storage.pvc.yaml
Normal file
13
make/kubernetes/pv/storage.pvc.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: storage-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
type: storage
|
@ -1,28 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ReplicationController
|
|
||||||
metadata:
|
|
||||||
name: registry
|
|
||||||
labels:
|
|
||||||
name: registry
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
name: registry
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
name: registry
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry
|
|
||||||
image: caicloud/harbor_registry:2.3.0
|
|
||||||
imagePullPolicy: Always
|
|
||||||
ports:
|
|
||||||
- containerPort: 5000
|
|
||||||
- containerPort: 5001
|
|
||||||
volumeMounts:
|
|
||||||
- name: storage
|
|
||||||
mountPath: /storage
|
|
||||||
volumes:
|
|
||||||
- name: storage
|
|
||||||
emptyDir: {}
|
|
39
make/kubernetes/registry/registry.rc.yaml
Normal file
39
make/kubernetes/registry/registry.rc.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ReplicationController
|
||||||
|
metadata:
|
||||||
|
name: registry-rc
|
||||||
|
labels:
|
||||||
|
name: registry-rc
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
name: registry-apps
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
name: registry-apps
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: registry-app
|
||||||
|
image: harbor/registry
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
- containerPort: 5001
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/docker/registry
|
||||||
|
- name: storage
|
||||||
|
mountPath: /storage
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: harbor-registry-config
|
||||||
|
items:
|
||||||
|
- key: config
|
||||||
|
path: config.yml
|
||||||
|
- key: cert
|
||||||
|
path: root.crt
|
||||||
|
- name: storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: registry-pvc
|
@ -2,13 +2,11 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: registry
|
name: registry
|
||||||
labels:
|
|
||||||
name: registry
|
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- name: internal
|
- name: repo
|
||||||
port: 5000
|
port: 5000
|
||||||
- name: external
|
- name: debug
|
||||||
port: 5001
|
port: 5001
|
||||||
selector:
|
selector:
|
||||||
name: registry
|
name: registry-apps
|
25
make/kubernetes/templates/jobservice.cm.yaml
Normal file
25
make/kubernetes/templates/jobservice.cm.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: harbor-jobservice-config
|
||||||
|
data:
|
||||||
|
MYSQL_HOST: mysql
|
||||||
|
MYSQL_PORT: "3306"
|
||||||
|
MYSQL_USR: root
|
||||||
|
MYSQL_PWD: "{{db_password}}"
|
||||||
|
UI_SECRET: "{{ui_secret}}"
|
||||||
|
SECRET_KEY: "{{secret_key}}"
|
||||||
|
CONFIG_PATH: /etc/jobservice/app.conf
|
||||||
|
REGISTRY_URL: http://registry:5000
|
||||||
|
VERIFY_REMOTE_CERT: "{{verify_remote_cert}}"
|
||||||
|
MAX_JOB_WORKERS: "{{max_job_workers}}"
|
||||||
|
LOG_LEVEL: debug
|
||||||
|
LOG_DIR: /var/log/jobs
|
||||||
|
GODEBUG: netdns=cgo
|
||||||
|
EXT_ENDPOINT: "{{ui_url}}"
|
||||||
|
TOKEN_URL: http://ui
|
||||||
|
config: |
|
||||||
|
appname = jobservice
|
||||||
|
runmode = dev
|
||||||
|
[dev]
|
||||||
|
httpport = 80
|
6
make/kubernetes/templates/mysql.cm.yaml
Normal file
6
make/kubernetes/templates/mysql.cm.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: harbor-mysql-config
|
||||||
|
data:
|
||||||
|
MYSQL_ROOT_PASSWORD: "{{db_password}}"
|
155
make/kubernetes/templates/nginx.cm.yaml
Normal file
155
make/kubernetes/templates/nginx.cm.yaml
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
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 443 ssl;
|
||||||
|
server_name {{hostname}};
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
ssl_certificate /etc/nginx/https.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/https.key;
|
||||||
|
|
||||||
|
# Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||||
|
ssl_protocols TLSv1.1 TLSv1.2;
|
||||||
|
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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}}
|
44
make/kubernetes/templates/registry.cm.yaml
Normal file
44
make/kubernetes/templates/registry.cm.yaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: harbor-registry-config
|
||||||
|
data:
|
||||||
|
config: |
|
||||||
|
version: 0.1
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
fields:
|
||||||
|
service: registry
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
rootdirectory: /storage
|
||||||
|
cache:
|
||||||
|
layerinfo: inmemory
|
||||||
|
maintenance:
|
||||||
|
uploadpurging:
|
||||||
|
enabled: false
|
||||||
|
delete:
|
||||||
|
enabled: true
|
||||||
|
http:
|
||||||
|
addr: :5000
|
||||||
|
secret: placeholder
|
||||||
|
debug:
|
||||||
|
addr: localhost:5001
|
||||||
|
auth:
|
||||||
|
token:
|
||||||
|
issuer: registry-token-issuer
|
||||||
|
realm: {{ui_url}}/service/token
|
||||||
|
rootcertbundle: /etc/docker/registry/root.crt
|
||||||
|
service: token-service
|
||||||
|
notifications:
|
||||||
|
endpoints:
|
||||||
|
- name: harbor
|
||||||
|
disabled: false
|
||||||
|
url: http://ui/service/notifications
|
||||||
|
timeout: 3000ms
|
||||||
|
threshold: 5
|
||||||
|
backoff: 1s
|
||||||
|
|
||||||
|
cert: |
|
||||||
|
{{4 auth_cert}}
|
||||||
|
|
52
make/kubernetes/templates/ui.cm.yaml
Normal file
52
make/kubernetes/templates/ui.cm.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: harbor-ui-config
|
||||||
|
data:
|
||||||
|
MYSQL_HOST: mysql
|
||||||
|
MYSQL_PORT: "3306"
|
||||||
|
MYSQL_USR: root
|
||||||
|
MYSQL_PWD: "{{db_password}}"
|
||||||
|
REGISTRY_URL: http://registry:5000
|
||||||
|
CONFIG_PATH: /etc/ui/app.conf
|
||||||
|
HARBOR_REG_URL: "{{hostname}}"
|
||||||
|
HARBOR_ADMIN_PASSWORD: "{{harbor_admin_password}}"
|
||||||
|
HARBOR_URL: http://ui
|
||||||
|
AUTH_MODE: "{{auth_mode}}"
|
||||||
|
LDAP_URL: "{{ldap_url}}"
|
||||||
|
LDAP_SEARCH_DN: "{{ldap_searchdn}}"
|
||||||
|
LDAP_SEARCH_PWD: "{{ldap_search_pwd}}"
|
||||||
|
LDAP_BASE_DN: "{{ldap_basedn}}"
|
||||||
|
LDAP_FILTER: "{{ldap_filter}}"
|
||||||
|
LDAP_UID: "{{ldap_uid}}"
|
||||||
|
LDAP_SCOPE: "{{ldap_scope}}"
|
||||||
|
LOG_LEVEL: debug
|
||||||
|
UI_SECRET: "{{ui_secret}}"
|
||||||
|
SECRET_KEY: "{{secret_key}}"
|
||||||
|
GODEBUG: netdns=cgo
|
||||||
|
EXT_ENDPOINT: "{{ui_url}}"
|
||||||
|
TOKEN_URL: http://ui
|
||||||
|
SELF_REGISTRATION: "{{self_registration}}"
|
||||||
|
USE_COMPRESSED_JS: "{{use_compressed_js}}"
|
||||||
|
VERIFY_REMOTE_CERT: "{{verify_remote_cert}}"
|
||||||
|
TOKEN_EXPIRATION: "{{token_expiration}}"
|
||||||
|
EXT_REG_URL: "{{hostname}}"
|
||||||
|
config: |
|
||||||
|
appname = registry
|
||||||
|
runmode = dev
|
||||||
|
[lang]
|
||||||
|
types = en-US|zh-CN
|
||||||
|
names = en-US|zh-CN
|
||||||
|
[dev]
|
||||||
|
httpport = 80
|
||||||
|
[mail]
|
||||||
|
host = {{email_server}}
|
||||||
|
port = {{email_server_port}}
|
||||||
|
username = {{email_username}}
|
||||||
|
password = {{email_password}}
|
||||||
|
from = {{email_from}}
|
||||||
|
ssl = {{email_ssl}}
|
||||||
|
pkey: |
|
||||||
|
{{4 auth_pkey}}
|
||||||
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ReplicationController
|
|
||||||
metadata:
|
|
||||||
name: ui
|
|
||||||
labels:
|
|
||||||
name: ui
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
name: ui
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
name: ui
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: ui
|
|
||||||
image: caicloud/harbor_deploy_ui:latest
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: MYSQL_HOST
|
|
||||||
value: mysql
|
|
||||||
- name: MYSQL_PORT
|
|
||||||
value: "3306"
|
|
||||||
- name: MYSQL_USR
|
|
||||||
value: root
|
|
||||||
- name: MYSQL_PWD
|
|
||||||
value: root123
|
|
||||||
- name: REGISTRY_URL
|
|
||||||
value: http://registry:5000
|
|
||||||
- name: CONFIG_PATH
|
|
||||||
value: /etc/ui/app.conf
|
|
||||||
- name: HARBOR_REG_URL
|
|
||||||
value: localhost
|
|
||||||
- name: HARBOR_ADMIN_PASSWORD
|
|
||||||
value: Harbor12345
|
|
||||||
- name: HARBOR_URL
|
|
||||||
value: http://localhost
|
|
||||||
- name: AUTH_MODE
|
|
||||||
value: db_auth
|
|
||||||
- name: LDAP_URL
|
|
||||||
value: ldaps://ldap.mydomain.com
|
|
||||||
- name: LDAP_BASE_DN
|
|
||||||
value: uid=%s,ou=people,dc=mydomain,dc=com
|
|
||||||
- name: LOG_LEVEL
|
|
||||||
value: debug
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
|
|
174
make/kubernetes/ui/ui.rc.yaml
Normal file
174
make/kubernetes/ui/ui.rc.yaml
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ReplicationController
|
||||||
|
metadata:
|
||||||
|
name: ui-rc
|
||||||
|
labels:
|
||||||
|
name: ui-rc
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
name: ui-apps
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
name: ui-apps
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ui-app
|
||||||
|
image: harbor/ui
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: MYSQL_HOST
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: MYSQL_HOST
|
||||||
|
- name: MYSQL_PORT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: MYSQL_PORT
|
||||||
|
- name: MYSQL_USR
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: MYSQL_USR
|
||||||
|
- name: MYSQL_PWD
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: MYSQL_PWD
|
||||||
|
- name: REGISTRY_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: REGISTRY_URL
|
||||||
|
- name: CONFIG_PATH
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: CONFIG_PATH
|
||||||
|
- name: HARBOR_REG_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: HARBOR_REG_URL
|
||||||
|
- name: HARBOR_ADMIN_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: HARBOR_ADMIN_PASSWORD
|
||||||
|
- name: HARBOR_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: HARBOR_URL
|
||||||
|
- name: AUTH_MODE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: AUTH_MODE
|
||||||
|
- name: LDAP_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_URL
|
||||||
|
- name: LDAP_SEARCH_DN
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_SEARCH_DN
|
||||||
|
- name: LDAP_SEARCH_PWD
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_SEARCH_PWD
|
||||||
|
- name: LDAP_BASE_DN
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_BASE_DN
|
||||||
|
- name: LDAP_FILTER
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_FILTER
|
||||||
|
- name: LDAP_UID
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_UID
|
||||||
|
- name: LDAP_SCOPE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LDAP_SCOPE
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: LOG_LEVEL
|
||||||
|
- name: UI_SECRET
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: UI_SECRET
|
||||||
|
- name: SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: SECRET_KEY
|
||||||
|
- name: GODEBUG
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: GODEBUG
|
||||||
|
- name: EXT_ENDPOINT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: EXT_ENDPOINT
|
||||||
|
- name: TOKEN_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: TOKEN_URL
|
||||||
|
- name: SELF_REGISTRATION
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: SELF_REGISTRATION
|
||||||
|
- name: USE_COMPRESSED_JS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: USE_COMPRESSED_JS
|
||||||
|
- name: VERIFY_REMOTE_CERT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: VERIFY_REMOTE_CERT
|
||||||
|
- name: TOKEN_EXPIRATION
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: TOKEN_EXPIRATION
|
||||||
|
- name: EXT_REG_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: harbor-ui-config
|
||||||
|
key: EXT_REG_URL
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/ui
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: harbor-ui-config
|
||||||
|
items:
|
||||||
|
- key: config
|
||||||
|
path: app.conf
|
||||||
|
- key: pkey
|
||||||
|
path: private_key.pem
|
@ -2,10 +2,8 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: ui
|
name: ui
|
||||||
labels:
|
|
||||||
name: ui
|
|
||||||
spec:
|
spec:
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
selector:
|
selector:
|
||||||
name: ui
|
name: ui-apps
|
13
partners.md
Normal file
13
partners.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Community of Harbor
|
||||||
|
|
||||||
|
Below are some of the partners and users of Harbor projects ( update constantly ). If you wish to be listed on this page as reference partners or users, please contact us at harbor@ vmware.com .
|
||||||
|
|
||||||
|
## Partners
|
||||||
|
<a href="https://www.shurenyun.com/" border="0" target="_blank"><img alt="DataMan" src="docs/img/dataman.png"></a> <a href="http://www.slamtec.com" target="_blank" border="0"><img alt="SlamTec" src="docs/img/slamteclogo.png"></a>
|
||||||
|
<a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
|
||||||
|
|
||||||
|
## Users
|
||||||
|
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.dianrong.com/" border="0" target="_blank"><img alt="Dianrong" src="docs/img/dianrong.png"></a>
|
||||||
|
|
||||||
|
## Supporting Technologies
|
||||||
|
<img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>.
|
Loading…
Reference in New Issue
Block a user