github.com/argoproj/argo-cd@v1.8.7/util/helm/client.go (about) 1 package helm 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "crypto/x509" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "strings" 17 "time" 18 19 "github.com/Masterminds/semver" 20 "github.com/argoproj/pkg/sync" 21 log "github.com/sirupsen/logrus" 22 "gopkg.in/yaml.v2" 23 24 executil "github.com/argoproj/argo-cd/util/exec" 25 "github.com/argoproj/argo-cd/util/io" 26 ) 27 28 var ( 29 globalLock = sync.NewKeyLock() 30 ) 31 32 type Creds struct { 33 Username string 34 Password string 35 CAPath string 36 CertData []byte 37 KeyData []byte 38 InsecureSkipVerify bool 39 } 40 41 type Client interface { 42 CleanChartCache(chart string, version *semver.Version) error 43 ExtractChart(chart string, version *semver.Version) (string, io.Closer, error) 44 GetIndex() (*Index, error) 45 TestHelmOCI() (bool, error) 46 } 47 48 func NewClient(repoURL string, creds Creds, enableOci bool) Client { 49 return NewClientWithLock(repoURL, creds, globalLock, enableOci) 50 } 51 52 func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, enableOci bool) Client { 53 return &nativeHelmChart{ 54 repoURL: repoURL, 55 creds: creds, 56 repoPath: filepath.Join(os.TempDir(), strings.Replace(repoURL, "/", "_", -1)), 57 repoLock: repoLock, 58 enableOci: enableOci, 59 } 60 } 61 62 type nativeHelmChart struct { 63 repoPath string 64 repoURL string 65 creds Creds 66 repoLock sync.KeyLock 67 enableOci bool 68 } 69 70 func fileExist(filePath string) (bool, error) { 71 if _, err := os.Stat(filePath); err != nil { 72 if os.IsNotExist(err) { 73 return false, nil 74 } else { 75 return false, err 76 } 77 } 78 return true, nil 79 } 80 81 func (c *nativeHelmChart) ensureHelmChartRepoPath() error { 82 c.repoLock.Lock(c.repoPath) 83 defer c.repoLock.Unlock(c.repoPath) 84 85 err := os.Mkdir(c.repoPath, 0700) 86 if err != nil && !os.IsExist(err) { 87 return err 88 } 89 return nil 90 } 91 92 func (c *nativeHelmChart) CleanChartCache(chart string, version *semver.Version) error { 93 return os.RemoveAll(c.getCachedChartPath(chart, version)) 94 } 95 96 func (c *nativeHelmChart) ExtractChart(chart string, version *semver.Version) (string, io.Closer, error) { 97 err := c.ensureHelmChartRepoPath() 98 if err != nil { 99 return "", nil, err 100 } 101 102 // always use Helm V3 since we don't have chart content to determine correct Helm version 103 helmCmd, err := NewCmdWithVersion(c.repoPath, HelmV3, c.enableOci) 104 105 if err != nil { 106 return "", nil, err 107 } 108 defer helmCmd.Close() 109 110 _, err = helmCmd.Init() 111 if err != nil { 112 return "", nil, err 113 } 114 115 // throw away temp directory that stores extracted chart and should be deleted as soon as no longer needed by returned closer 116 tempDir, err := ioutil.TempDir("", "helm") 117 if err != nil { 118 return "", nil, err 119 } 120 121 cachedChartPath := c.getCachedChartPath(chart, version) 122 123 c.repoLock.Lock(cachedChartPath) 124 defer c.repoLock.Unlock(cachedChartPath) 125 126 // check if chart tar is already downloaded 127 exists, err := fileExist(cachedChartPath) 128 if err != nil { 129 return "", nil, err 130 } 131 132 if !exists { 133 // create empty temp directory to extract chart from the registry 134 tempDest, err := ioutil.TempDir("", "helm") 135 if err != nil { 136 return "", nil, err 137 } 138 defer func() { _ = os.RemoveAll(tempDest) }() 139 140 if c.enableOci { 141 if c.creds.Password != "" && c.creds.Username != "" { 142 _, err = helmCmd.Login(c.repoURL, c.creds) 143 if err != nil { 144 return "", nil, err 145 } 146 147 defer func() { 148 _, _ = helmCmd.Logout(c.repoURL, c.creds) 149 }() 150 } 151 152 // 'helm chart pull' ensures that chart is downloaded into local repository cache 153 _, err = helmCmd.ChartPull(c.repoURL, chart, version.String()) 154 if err != nil { 155 return "", nil, err 156 } 157 158 // 'helm chart export' copies cached chart into temp directory 159 _, err = helmCmd.ChartExport(c.repoURL, chart, version.String(), tempDest) 160 if err != nil { 161 return "", nil, err 162 } 163 164 // use downloaded chart content to produce tar file in expected cache location 165 cmd := exec.Command("tar", "-zcvf", cachedChartPath, normalizeChartName(chart)) 166 cmd.Dir = tempDest 167 _, err = executil.Run(cmd) 168 if err != nil { 169 return "", nil, err 170 } 171 } else { 172 _, err = helmCmd.Fetch(c.repoURL, chart, version.String(), tempDest, c.creds) 173 if err != nil { 174 return "", nil, err 175 } 176 177 // 'helm fetch' file downloads chart into the tgz file and we move that to where we want it 178 infos, err := ioutil.ReadDir(tempDest) 179 if err != nil { 180 return "", nil, err 181 } 182 if len(infos) != 1 { 183 return "", nil, fmt.Errorf("expected 1 file, found %v", len(infos)) 184 } 185 err = os.Rename(filepath.Join(tempDest, infos[0].Name()), cachedChartPath) 186 if err != nil { 187 return "", nil, err 188 } 189 } 190 191 } 192 193 cmd := exec.Command("tar", "-zxvf", cachedChartPath) 194 cmd.Dir = tempDir 195 _, err = executil.Run(cmd) 196 if err != nil { 197 _ = os.RemoveAll(tempDir) 198 return "", nil, err 199 } 200 return path.Join(tempDir, normalizeChartName(chart)), io.NewCloser(func() error { 201 return os.RemoveAll(tempDir) 202 }), nil 203 } 204 205 func (c *nativeHelmChart) GetIndex() (*Index, error) { 206 start := time.Now() 207 208 data, err := c.loadRepoIndex() 209 if err != nil { 210 return nil, err 211 } 212 213 index := &Index{} 214 err = yaml.NewDecoder(bytes.NewBuffer(data)).Decode(index) 215 if err != nil { 216 return nil, err 217 } 218 219 log.WithFields(log.Fields{"seconds": time.Since(start).Seconds()}).Info("took to get index") 220 221 return index, nil 222 } 223 224 func (c *nativeHelmChart) TestHelmOCI() (bool, error) { 225 start := time.Now() 226 227 tmpDir, err := ioutil.TempDir("", "helm") 228 if err != nil { 229 return false, err 230 } 231 defer func() { _ = os.RemoveAll(tmpDir) }() 232 233 helmCmd, err := NewCmdWithVersion(tmpDir, HelmV3, c.enableOci) 234 if err != nil { 235 return false, err 236 } 237 defer helmCmd.Close() 238 239 // Looks like there is no good way to test access to OCI repo if credentials are not provided 240 // just assume it is accessible 241 if c.creds.Username != "" && c.creds.Password != "" { 242 _, err = helmCmd.Login(c.repoURL, c.creds) 243 if err != nil { 244 return false, err 245 } 246 defer func() { 247 _, _ = helmCmd.Logout(c.repoURL, c.creds) 248 }() 249 250 log.WithFields(log.Fields{"seconds": time.Since(start).Seconds()}).Info("took to test helm oci repository") 251 } 252 return true, nil 253 } 254 255 func (c *nativeHelmChart) loadRepoIndex() ([]byte, error) { 256 repoURL, err := url.Parse(c.repoURL) 257 if err != nil { 258 return nil, err 259 } 260 repoURL.Path = path.Join(repoURL.Path, "index.yaml") 261 262 req, err := http.NewRequest("GET", repoURL.String(), nil) 263 if err != nil { 264 return nil, err 265 } 266 if c.creds.Username != "" || c.creds.Password != "" { 267 // only basic supported 268 req.SetBasicAuth(c.creds.Username, c.creds.Password) 269 } 270 271 tlsConf, err := newTLSConfig(c.creds) 272 if err != nil { 273 return nil, err 274 } 275 tr := &http.Transport{ 276 Proxy: http.ProxyFromEnvironment, 277 TLSClientConfig: tlsConf, 278 } 279 client := http.Client{Transport: tr} 280 resp, err := client.Do(req) 281 if err != nil { 282 return nil, err 283 } 284 defer func() { _ = resp.Body.Close() }() 285 286 if resp.StatusCode != 200 { 287 return nil, errors.New("failed to get index: " + resp.Status) 288 } 289 return ioutil.ReadAll(resp.Body) 290 } 291 292 func newTLSConfig(creds Creds) (*tls.Config, error) { 293 tlsConfig := &tls.Config{InsecureSkipVerify: creds.InsecureSkipVerify} 294 295 if creds.CAPath != "" { 296 caData, err := ioutil.ReadFile(creds.CAPath) 297 if err != nil { 298 return nil, err 299 } 300 caCertPool := x509.NewCertPool() 301 caCertPool.AppendCertsFromPEM(caData) 302 tlsConfig.RootCAs = caCertPool 303 } 304 305 // If a client cert & key is provided then configure TLS config accordingly. 306 if len(creds.CertData) > 0 && len(creds.KeyData) > 0 { 307 cert, err := tls.X509KeyPair(creds.CertData, creds.KeyData) 308 if err != nil { 309 return nil, err 310 } 311 tlsConfig.Certificates = []tls.Certificate{cert} 312 } 313 // nolint:staticcheck 314 tlsConfig.BuildNameToCertificate() 315 316 return tlsConfig, nil 317 } 318 319 // Normalize a chart name for file system use, that is, if chart name is foo/bar/baz, returns the last component as chart name. 320 func normalizeChartName(chart string) string { 321 strings.Join(strings.Split(chart, "/"), "_") 322 _, nc := path.Split(chart) 323 // We do not want to return the empty string or something else related to filesystem access 324 // Instead, return original string 325 if nc == "" || nc == "." || nc == ".." { 326 return chart 327 } 328 return nc 329 } 330 331 func (c *nativeHelmChart) getCachedChartPath(chart string, version *semver.Version) string { 332 return path.Join(c.repoPath, fmt.Sprintf("%s-%v.tgz", strings.ReplaceAll(chart, "/", "_"), version)) 333 } 334 335 // Only OCI registries support storing charts under sub-directories. 336 func IsHelmOciChart(chart string) bool { 337 return strings.Contains(chart, "/") 338 }