2018-07-02 13:34:04 +02:00
|
|
|
package chartserver
|
|
|
|
|
|
|
|
import (
|
2018-07-05 12:07:00 +02:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2018-07-02 13:34:04 +02:00
|
|
|
"net/http"
|
2018-07-05 12:07:00 +02:00
|
|
|
"net/url"
|
2018-07-14 09:49:38 +02:00
|
|
|
"path"
|
2018-07-05 12:07:00 +02:00
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ghodss/yaml"
|
|
|
|
"github.com/vmware/harbor/src/ui/filter"
|
|
|
|
helm_repo "k8s.io/helm/pkg/repo"
|
2018-07-14 09:49:38 +02:00
|
|
|
|
|
|
|
hlog "github.com/vmware/harbor/src/common/utils/log"
|
2018-07-05 12:07:00 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxWorkers = 10
|
2018-07-02 13:34:04 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
//RepositoryHandler defines all the handlers to handle the requests related with chart repository
|
|
|
|
//e.g: index.yaml and downloading chart objects
|
|
|
|
type RepositoryHandler struct {
|
|
|
|
//Proxy used to to transfer the traffic of requests
|
|
|
|
//It's mainly used to talk to the backend chart server
|
2018-07-03 15:36:20 +02:00
|
|
|
trafficProxy *ProxyEngine
|
2018-07-05 12:07:00 +02:00
|
|
|
|
|
|
|
//HTTP client used to call the realted APIs of the backend chart repositories
|
2018-07-06 14:53:13 +02:00
|
|
|
apiClient *ChartClient
|
2018-07-05 12:07:00 +02:00
|
|
|
|
|
|
|
//Point to the url of the backend server
|
|
|
|
backendServerAddress *url.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
//Pass work to the workers
|
|
|
|
//'index' is the location of processing namespace/project in the list
|
|
|
|
type workload struct {
|
|
|
|
index uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
//Result returned by worker
|
|
|
|
type processedResult struct {
|
|
|
|
namespace string
|
|
|
|
indexFileOfRepo *helm_repo.IndexFile
|
2018-07-02 13:34:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//GetIndexFileWithNS will read the index.yaml data under the specified namespace
|
|
|
|
func (rh *RepositoryHandler) GetIndexFileWithNS(w http.ResponseWriter, req *http.Request) {
|
2018-07-03 15:36:20 +02:00
|
|
|
rh.trafficProxy.ServeHTTP(w, req)
|
2018-07-02 13:34:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
|
|
|
//Please be aware that, to support this function, the backend chart repository server should
|
|
|
|
//enable multi-tenancies
|
2018-07-03 15:36:20 +02:00
|
|
|
func (rh *RepositoryHandler) GetIndexFile(w http.ResponseWriter, req *http.Request) {
|
2018-07-05 12:07:00 +02:00
|
|
|
//Get project manager references
|
|
|
|
projectMgr, err := filter.GetProjectManager(req)
|
|
|
|
if err != nil {
|
2018-07-23 11:41:40 +02:00
|
|
|
WriteInternalError(w, err)
|
2018-07-05 12:07:00 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get all the projects
|
|
|
|
results, err := projectMgr.List(nil)
|
|
|
|
if err != nil {
|
2018-07-23 11:41:40 +02:00
|
|
|
WriteInternalError(w, err)
|
2018-07-05 12:07:00 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//If no projects existing, return empty index.yaml content immediately
|
|
|
|
if results.Total == 0 {
|
|
|
|
w.Write(emptyIndexFile())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//The final merged index file
|
|
|
|
mergedIndexFile := &helm_repo.IndexFile{
|
|
|
|
APIVersion: "v1",
|
|
|
|
Entries: make(map[string]helm_repo.ChartVersions),
|
|
|
|
Generated: time.Now(),
|
|
|
|
PublicKeys: []string{},
|
|
|
|
}
|
|
|
|
|
|
|
|
//Retrieve index.yaml for repositories
|
|
|
|
workerPool := make(chan *workload, maxWorkers)
|
|
|
|
//Sync the output results from the retriever
|
|
|
|
resultChan := make(chan *processedResult, 1)
|
|
|
|
//Receive error
|
|
|
|
errorChan := make(chan error, 1)
|
|
|
|
//Signal chan for merging work
|
|
|
|
mergeDone := make(chan struct{}, 1)
|
|
|
|
//Total projects/namespaces
|
|
|
|
total := uint32(results.Total)
|
|
|
|
//Track all the background threads
|
|
|
|
waitGroup := new(sync.WaitGroup)
|
|
|
|
|
|
|
|
//Initialize
|
|
|
|
initialItemCount := maxWorkers
|
|
|
|
if total < maxWorkers {
|
|
|
|
initialItemCount = int(total)
|
|
|
|
}
|
|
|
|
for i := 0; i < initialItemCount; i++ {
|
|
|
|
workerPool <- &workload{uint32(i)}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Atomtic index
|
|
|
|
var indexRef uint32
|
|
|
|
atomic.AddUint32(&indexRef, uint32(initialItemCount-1))
|
|
|
|
|
|
|
|
//Start the index files merging thread
|
|
|
|
go func() {
|
|
|
|
defer func() {
|
|
|
|
mergeDone <- struct{}{}
|
|
|
|
}()
|
|
|
|
|
|
|
|
for res := range resultChan {
|
|
|
|
rh.mergeIndexFile(res.namespace, mergedIndexFile, res.indexFileOfRepo)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
//Retrieve the index files for the repositories
|
|
|
|
//and blocking here
|
|
|
|
LOOP:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case work := <-workerPool:
|
|
|
|
if work.index >= total {
|
|
|
|
break LOOP
|
|
|
|
}
|
|
|
|
//Process
|
|
|
|
//New one
|
|
|
|
waitGroup.Add(1)
|
|
|
|
namespace := results.Projects[work.index].Name
|
|
|
|
go func(ns string) {
|
|
|
|
//Return the worker back to the pool
|
|
|
|
defer func() {
|
|
|
|
waitGroup.Done() //done
|
|
|
|
|
|
|
|
//Put one. The invalid index will be treated as a signal to quit loop
|
|
|
|
nextOne := atomic.AddUint32(&indexRef, 1)
|
|
|
|
workerPool <- &workload{nextOne}
|
|
|
|
}()
|
|
|
|
|
|
|
|
indexFile, err := rh.getIndexYamlWithNS(ns)
|
|
|
|
if err != nil {
|
|
|
|
errorChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//Output
|
|
|
|
resultChan <- &processedResult{
|
|
|
|
namespace: ns,
|
|
|
|
indexFileOfRepo: indexFile,
|
|
|
|
}
|
|
|
|
}(namespace)
|
2018-07-11 11:31:34 +02:00
|
|
|
case err = <-errorChan:
|
|
|
|
//Quit earlier
|
|
|
|
break LOOP
|
2018-07-05 12:07:00 +02:00
|
|
|
case <-req.Context().Done():
|
2018-07-11 11:31:34 +02:00
|
|
|
//Quit earlier
|
|
|
|
err = errors.New("request of getting index yaml file is aborted")
|
|
|
|
break LOOP
|
2018-07-05 12:07:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Hold util all the retrieving work are done
|
|
|
|
waitGroup.Wait()
|
|
|
|
|
|
|
|
//close consumer channel
|
|
|
|
close(resultChan)
|
|
|
|
|
|
|
|
//Wait until merging thread quit
|
|
|
|
<-mergeDone
|
|
|
|
|
2018-07-11 11:31:34 +02:00
|
|
|
//All the threads are done
|
|
|
|
//Met an error
|
|
|
|
if err != nil {
|
2018-07-23 11:41:40 +02:00
|
|
|
WriteInternalError(w, err)
|
2018-07-11 11:31:34 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-07-05 12:07:00 +02:00
|
|
|
//Remove duplicated keys in public key list
|
|
|
|
hash := make(map[string]string)
|
|
|
|
for _, key := range mergedIndexFile.PublicKeys {
|
|
|
|
hash[key] = key
|
|
|
|
}
|
|
|
|
mergedIndexFile.PublicKeys = []string{}
|
|
|
|
for k := range hash {
|
|
|
|
mergedIndexFile.PublicKeys = append(mergedIndexFile.PublicKeys, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes, err := yaml.Marshal(mergedIndexFile)
|
|
|
|
if err != nil {
|
2018-07-23 11:41:40 +02:00
|
|
|
WriteInternalError(w, err)
|
2018-07-05 12:07:00 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write(bytes)
|
2018-07-03 15:36:20 +02:00
|
|
|
}
|
2018-07-02 13:34:04 +02:00
|
|
|
|
|
|
|
//DownloadChartObject will download the stored chart object to the client
|
|
|
|
//e.g: helm install
|
2018-07-03 15:36:20 +02:00
|
|
|
func (rh *RepositoryHandler) DownloadChartObject(w http.ResponseWriter, req *http.Request) {
|
|
|
|
rh.trafficProxy.ServeHTTP(w, req)
|
|
|
|
}
|
2018-07-05 12:07:00 +02:00
|
|
|
|
|
|
|
//Get the index yaml file under the specified namespace from the backend server
|
|
|
|
func (rh *RepositoryHandler) getIndexYamlWithNS(namespace string) (*helm_repo.IndexFile, error) {
|
|
|
|
//Join url path
|
2018-07-14 09:49:38 +02:00
|
|
|
url := path.Join(namespace, "index.yaml")
|
|
|
|
url = fmt.Sprintf("%s/%s", rh.backendServerAddress.String(), url)
|
|
|
|
hlog.Debugf("Getting index.yaml from '%s'", url)
|
2018-07-05 12:07:00 +02:00
|
|
|
|
2018-07-06 14:53:13 +02:00
|
|
|
content, err := rh.apiClient.GetContent(url)
|
2018-07-05 12:07:00 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-07-06 14:53:13 +02:00
|
|
|
//Traverse to index file object for merging
|
2018-07-05 12:07:00 +02:00
|
|
|
indexFile := helm_repo.NewIndexFile()
|
|
|
|
if err := yaml.Unmarshal(content, indexFile); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return indexFile, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
//Merge the content of mergingIndexFile to the baseIndex
|
|
|
|
//The chart url should be without --chart-url prefix
|
|
|
|
func (rh *RepositoryHandler) mergeIndexFile(namespace string,
|
|
|
|
baseIndex *helm_repo.IndexFile,
|
|
|
|
mergingIndexFile *helm_repo.IndexFile) {
|
|
|
|
//Append entries
|
|
|
|
for chartName, chartVersions := range mergingIndexFile.Entries {
|
|
|
|
nameWithNS := fmt.Sprintf("%s/%s", namespace, chartName)
|
|
|
|
for _, version := range chartVersions {
|
|
|
|
version.Name = nameWithNS
|
|
|
|
//Currently there is only one url
|
|
|
|
for index, url := range version.URLs {
|
2018-07-14 09:49:38 +02:00
|
|
|
version.URLs[index] = path.Join(namespace, url)
|
2018-07-05 12:07:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Appended
|
|
|
|
baseIndex.Entries[nameWithNS] = chartVersions
|
|
|
|
}
|
|
|
|
|
|
|
|
//Update generated time
|
|
|
|
if mergingIndexFile.Generated.After(baseIndex.Generated) {
|
|
|
|
baseIndex.Generated = mergingIndexFile.Generated
|
|
|
|
}
|
|
|
|
|
|
|
|
//Merge public keys
|
|
|
|
baseIndex.PublicKeys = append(baseIndex.PublicKeys, mergingIndexFile.PublicKeys...)
|
|
|
|
}
|
|
|
|
|
|
|
|
//Generate empty index file
|
|
|
|
func emptyIndexFile() []byte {
|
|
|
|
emptyIndexFile := &helm_repo.IndexFile{}
|
|
|
|
emptyIndexFile.Generated = time.Now()
|
|
|
|
|
|
|
|
//Ignore the error
|
|
|
|
rawData, _ := json.Marshal(emptyIndexFile)
|
|
|
|
|
|
|
|
return rawData
|
|
|
|
}
|