github.com/oam-dev/kubevela@v1.9.11/pkg/addon/push.go (about) 1 /* 2 Copyright 2022 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package addon 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "os" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 31 cm "github.com/chartmuseum/helm-push/pkg/chartmuseum" 32 cmhelm "github.com/chartmuseum/helm-push/pkg/helm" 33 "github.com/fatih/color" 34 helmrepo "helm.sh/helm/v3/pkg/repo" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 ) 37 38 // PushCmd is the command object to initiate a push command to ChartMuseum 39 type PushCmd struct { 40 ChartName string 41 AppVersion string 42 ChartVersion string 43 RepoName string 44 Username string 45 Password string 46 AccessToken string 47 AuthHeader string 48 ContextPath string 49 ForceUpload bool 50 UseHTTP bool 51 CaFile string 52 CertFile string 53 KeyFile string 54 InsecureSkipVerify bool 55 Out io.Writer 56 Timeout int64 57 KeepChartMetadata bool 58 // We need it to search in addon registries. 59 // If you use URL, instead of registry names, then it is not needed. 60 Client client.Client 61 } 62 63 // Push pushes addons (i.e. Helm Charts) to ChartMuseum. 64 // It will package the addon into a Helm Chart if necessary. 65 func (p *PushCmd) Push(ctx context.Context) error { 66 var repo *cmhelm.Repo 67 var err error 68 69 // Get the user specified Helm repo 70 repo, err = GetHelmRepo(ctx, p.Client, p.RepoName) 71 if err != nil { 72 return err 73 } 74 75 // Make the addon dir a Helm Chart 76 // The user can decide if they want Chart.yaml be in sync with addon metadata.yaml 77 // By default, it will recreate Chart.yaml according to addon metadata.yaml 78 err = MakeChartCompatible(p.ChartName, !p.KeepChartMetadata) 79 // `Not a directory` errors are ignored, that's fine, 80 // since .tgz files are also supported. 81 if err != nil && !strings.Contains(err.Error(), "is not a directory") { 82 return err 83 } 84 85 // Get chart from a directory or .tgz package 86 chart, err := cmhelm.GetChartByName(p.ChartName) 87 if err != nil { 88 return err 89 } 90 91 // Override chart version using specified version 92 if p.ChartVersion != "" { 93 chart.SetVersion(p.ChartVersion) 94 } 95 96 // Override app version using specified version 97 if p.AppVersion != "" { 98 chart.SetAppVersion(p.AppVersion) 99 } 100 101 // Override username and password using specified values 102 username := repo.Config.Username 103 password := repo.Config.Password 104 if p.Username != "" { 105 username = p.Username 106 } 107 if p.Password != "" { 108 password = p.Password 109 } 110 111 // Unset accessToken if repo credentials are provided 112 if username != "" && password != "" { 113 p.AccessToken = "" 114 } 115 116 // In case the repo is stored with cm:// protocol, 117 // (if that's somehow possible with KubeVela addon registries) 118 // use http instead, 119 // otherwise keep as it-is. 120 var url string 121 if p.UseHTTP { 122 url = strings.Replace(repo.Config.URL, "cm://", "http://", 1) 123 } else { 124 url = strings.Replace(repo.Config.URL, "cm://", "https://", 1) 125 } 126 127 cmClient, err := cm.NewClient( 128 cm.URL(url), 129 cm.Username(username), 130 cm.Password(password), 131 cm.AccessToken(p.AccessToken), 132 cm.AuthHeader(p.AuthHeader), 133 cm.ContextPath(p.ContextPath), 134 cm.CAFile(p.CaFile), 135 cm.CertFile(p.CertFile), 136 cm.KeyFile(p.KeyFile), 137 cm.InsecureSkipVerify(p.InsecureSkipVerify), 138 cm.Timeout(p.Timeout), 139 ) 140 141 if err != nil { 142 return err 143 } 144 145 // Use a temporary dir to hold packaged .tgz Charts 146 tmp, err := os.MkdirTemp("", "helm-push-") 147 if err != nil { 148 return err 149 } 150 defer func(path string) { 151 _ = os.RemoveAll(path) 152 }(tmp) 153 154 // Package Chart into .tgz packages for uploading to ChartMuseum 155 chartPackagePath, err := cmhelm.CreateChartPackage(chart, tmp) 156 if err != nil { 157 return err 158 } 159 160 _, _ = fmt.Fprintf(os.Stderr, "Pushing %s to %s... ", 161 color.New(color.Bold).Sprintf(filepath.Base(chartPackagePath)), 162 formatRepoNameAndURL(p.RepoName, repo.Config.URL), 163 ) 164 165 // Push Chart to ChartMuseum 166 resp, err := cmClient.UploadChartPackage(chartPackagePath, p.ForceUpload) 167 if err != nil { 168 return err 169 } 170 defer func() { 171 _ = resp.Body.Close() 172 }() 173 return handlePushResponse(resp) 174 } 175 176 // GetHelmRepo searches for a Helm repo by name. 177 // By saying name, it can actually be a URL or a name. 178 // If a URL is provided, a temp repo object is returned. 179 // If a name is provided, we will try to find it in local addon registries (only Helm type). 180 func GetHelmRepo(ctx context.Context, c client.Client, repoName string) (*cmhelm.Repo, error) { 181 var repo *cmhelm.Repo 182 var err error 183 184 // If RepoName looks like a URL (https / http), just create a temp repo object. 185 // We do not look for it in local addon registries. 186 if regexp.MustCompile(`^https?://`).MatchString(repoName) { 187 repo, err = cmhelm.TempRepoFromURL(repoName) 188 if err != nil { 189 return nil, err 190 } 191 return repo, nil 192 } 193 194 // Otherwise, search for in it in the local addon registries. 195 ds := NewRegistryDataStore(c) 196 registries, err := ds.ListRegistries(ctx) 197 if err != nil { 198 return nil, err 199 } 200 201 var matchedEntry *helmrepo.Entry 202 203 // Search for the target repo name in addon registries 204 for _, reg := range registries { 205 // We are only interested in Helm registries. 206 if reg.Helm == nil { 207 continue 208 } 209 210 if reg.Name == repoName { 211 matchedEntry = &helmrepo.Entry{ 212 Name: reg.Name, 213 URL: reg.Helm.URL, 214 Username: reg.Helm.Username, 215 Password: reg.Helm.Password, 216 } 217 break 218 } 219 } 220 221 if matchedEntry == nil { 222 return nil, fmt.Errorf("we cannot find Helm repository %s. Make sure you hava added it using `vela addon registry add` and it is a Helm repository", repoName) 223 } 224 225 // Use the repo found locally. 226 repo = &cmhelm.Repo{ChartRepository: &helmrepo.ChartRepository{Config: matchedEntry}} 227 228 return repo, nil 229 } 230 231 // SetFieldsFromEnv sets fields in PushCmd from environment variables 232 func (p *PushCmd) SetFieldsFromEnv() { 233 if v, ok := os.LookupEnv("HELM_REPO_USERNAME"); ok && p.Username == "" { 234 p.Username = v 235 } 236 if v, ok := os.LookupEnv("HELM_REPO_PASSWORD"); ok && p.Password == "" { 237 p.Password = v 238 } 239 if v, ok := os.LookupEnv("HELM_REPO_ACCESS_TOKEN"); ok && p.AccessToken == "" { 240 p.AccessToken = v 241 } 242 if v, ok := os.LookupEnv("HELM_REPO_AUTH_HEADER"); ok && p.AuthHeader == "" { 243 p.AuthHeader = v 244 } 245 if v, ok := os.LookupEnv("HELM_REPO_CONTEXT_PATH"); ok && p.ContextPath == "" { 246 p.ContextPath = v 247 } 248 if v, ok := os.LookupEnv("HELM_REPO_USE_HTTP"); ok { 249 p.UseHTTP, _ = strconv.ParseBool(v) 250 } 251 if v, ok := os.LookupEnv("HELM_REPO_CA_FILE"); ok && p.CaFile == "" { 252 p.CaFile = v 253 } 254 if v, ok := os.LookupEnv("HELM_REPO_CERT_FILE"); ok && p.CertFile == "" { 255 p.CertFile = v 256 } 257 if v, ok := os.LookupEnv("HELM_REPO_KEY_FILE"); ok && p.KeyFile == "" { 258 p.KeyFile = v 259 } 260 if v, ok := os.LookupEnv("HELM_REPO_INSECURE"); ok { 261 p.InsecureSkipVerify, _ = strconv.ParseBool(v) 262 } 263 } 264 265 // handlePushResponse checks response from ChartMuseum 266 func handlePushResponse(resp *http.Response) error { 267 if resp.StatusCode != 201 && resp.StatusCode != 202 { 268 _, _ = fmt.Fprintf(os.Stderr, "%s\n", color.RedString("Failed")) 269 b, err := io.ReadAll(resp.Body) 270 if err != nil { 271 return err 272 } 273 return getChartMuseumError(b, resp.StatusCode) 274 } 275 _, _ = fmt.Fprintf(os.Stderr, "%s\n", color.GreenString("Done")) 276 return nil 277 } 278 279 // getChartMuseumError checks error messages from the response 280 func getChartMuseumError(b []byte, code int) error { 281 var er struct { 282 Error string `json:"error"` 283 } 284 err := json.Unmarshal(b, &er) 285 if err != nil || er.Error == "" { 286 return fmt.Errorf("%d: could not properly parse response JSON: %s", code, string(b)) 287 } 288 return fmt.Errorf("%d: %s", code, er.Error) 289 } 290 291 func formatRepoNameAndURL(name, url string) string { 292 if name == "" || regexp.MustCompile(`^https?://`).MatchString(name) { 293 return color.BlueString(url) 294 } 295 296 return fmt.Sprintf("%s(%s)", 297 color.New(color.Bold).Sprintf(name), 298 color.BlueString(url), 299 ) 300 }