This repository has been archived by the owner on Feb 23, 2023. It is now read-only.
/
agent.go
184 lines (154 loc) · 4.46 KB
/
agent.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package repo
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"time"
"github.com/redsailtechnologies/boatswain/pkg/logger"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
)
// Agent is the interface we use to talk to helm packages
type Agent interface {
CheckIndex(name, endpoint, token string) bool
GetChart(name, version, endpoint, token string) ([]byte, error)
GetChartFromOCIV2(name, version, endpoint, username, password string) ([]byte, error)
}
// DefaultAgent is the default implementation of the Agent interface
type DefaultAgent struct{}
// CheckIndex checks the index.yaml file at the repo's endpoint
func (a DefaultAgent) CheckIndex(name, endpoint, token string) bool {
r, err := toChartRepo(name, endpoint, token)
if err != nil {
return false
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan error, 1)
// FIXME AdamP - This is really a hack, and leaves goroutines lingering, but the underlying problem
// is that the DownloadIndexFile call has a timeout, but no context. So while we can see if it took
// a while to get a response, we can't stop it from taking as long as it wants. My big problem with
// this approach is that we just leave goroutines out there to finish and we'd rather cancel them.
// Maybe we should consider just skipping repo status checks altogether, depending on their use.
go func() {
_, err := r.DownloadIndexFile()
if err != nil {
logger.Warn("error downloading repo index", "error", err)
}
select {
default:
ch <- err
case <-ctx.Done():
return
}
}()
select {
case err := <-ch:
return err == nil
case <-time.After(500 * time.Millisecond):
return false
}
}
// GetChart downloads a single chart from a particular chart repo
func (a DefaultAgent) GetChart(name, version, endpoint, token string) ([]byte, error) {
out := os.TempDir()
pull := action.NewPull()
pull.ChartPathOptions = action.ChartPathOptions{
RepoURL: endpoint,
}
pull.Settings = cli.New()
pull.RepoURL = endpoint
pull.Version = version
pull.DestDir = out
_, err := pull.Run(name)
if err != nil {
return nil, err
}
path := path.Join(out, fmt.Sprintf("%s-%s.tgz", name, version))
file, err := os.Open(path)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
// TODO - some better dir management here could be good as if this fails we just eventually overload the filesystem
if err = os.Remove(path); err != nil {
logger.Warn("could not remove temporary remove temporary file which could lead to disk overusage", "error", err)
}
return bytes, nil
}
const (
v2manifest string = "%s/v2/%s/manifests/%s"
v2blob string = "%s/v2/%s/blobs/%s"
)
type manifest struct {
Layers []struct {
Digest string `json:"digest"`
} `json:"layers"`
}
// GetChartFromOCIV2 gets a chart from a V2 compliant OCI registry
func (a DefaultAgent) GetChartFromOCIV2(name, version, endpoint, username, password string) ([]byte, error) {
u := fmt.Sprintf(v2manifest, endpoint, name, version)
manifestRequest, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
manifestRequest.SetBasicAuth(username, password)
manifestRequest.Header.Add("accept", "application/vnd.oci.image.manifest.v1+json")
client := &http.Client{}
resp, err := client.Do(manifestRequest)
if err != nil {
return nil, err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
m := &manifest{}
json.Unmarshal(b, m)
if len(m.Layers) != 1 {
return nil, errors.New("manifest has too many layers")
}
u = fmt.Sprintf(v2blob, endpoint, name, m.Layers[0].Digest)
digestRequest, err := http.NewRequest("GET", u, nil)
digestRequest.SetBasicAuth(username, password)
digestRequest.Header.Add("accept", "application/tar+gzip")
resp, err = client.Do(digestRequest)
if err != nil {
return nil, err
}
file, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return file, nil
}
func toChartRepo(name, endpoint, token string) (*repo.ChartRepository, error) {
providers := []getter.Provider{
{
Schemes: []string{"http", "https"},
New: getter.NewHTTPGetter,
},
}
// set the username to anything if the token is set
un := ""
if token != "" {
un = "boatswain"
}
entry := &repo.Entry{
Name: name,
URL: endpoint,
Username: un,
Password: token,
}
return repo.NewChartRepository(entry, providers)
}