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  }