github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/helm/chart.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helm
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    26  
    27  	"github.com/iancoleman/strcase"
    28  	log "github.com/sirupsen/logrus"
    29  	"k8s.io/helm/pkg/chartutil"
    30  	"k8s.io/helm/pkg/downloader"
    31  	"k8s.io/helm/pkg/getter"
    32  	"k8s.io/helm/pkg/helm/environment"
    33  	"k8s.io/helm/pkg/helm/helmpath"
    34  	"k8s.io/helm/pkg/proto/hapi/chart"
    35  	"k8s.io/helm/pkg/repo"
    36  )
    37  
    38  const (
    39  
    40  	// HelmChartsDir is the relative directory within an SDK project where Helm
    41  	// charts are stored.
    42  	HelmChartsDir string = "helm-charts"
    43  
    44  	// DefaultAPIVersion is the Kubernetes CRD API Version used for fetched
    45  	// charts when the --api-version flag is not specified
    46  	DefaultAPIVersion string = "charts.helm.k8s.io/v1alpha1"
    47  )
    48  
    49  // CreateChartOptions is used to configure how a Helm chart is scaffolded
    50  // for a new Helm operator project.
    51  type CreateChartOptions struct {
    52  	// ResourceAPIVersion defines the Kubernetes GroupVersion to be associated
    53  	// with the created chart.
    54  	ResourceAPIVersion string
    55  
    56  	// ResourceKind defines the Kubernetes Kind to be associated with the
    57  	// created chart.
    58  	ResourceKind string
    59  
    60  	// Chart is a chart reference for a local or remote chart.
    61  	Chart string
    62  
    63  	// Repo is a URL to a custom chart repository.
    64  	Repo string
    65  
    66  	// Version is the version of the chart to fetch.
    67  	Version string
    68  }
    69  
    70  // CreateChart scaffolds a new helm chart for the project rooted in projectDir
    71  // based on the passed opts.
    72  //
    73  // It returns a scaffold.Resource that can be used by the caller to create
    74  // other related files. opts.ResourceAPIVersion and opts.ResourceKind are
    75  // used to create the resource and must be specified if opts.Chart is empty.
    76  //
    77  // If opts.Chart is not empty, opts.ResourceAPIVersion and opts.Kind can be
    78  // left unset: opts.ResourceAPIVersion defaults to "charts.helm.k8s.io/v1alpha1"
    79  // and opts.ResourceKind is deduced from the specified opts.Chart.
    80  //
    81  // CreateChart also returns a chart.Chart that references the newly created
    82  // chart.
    83  //
    84  // If opts.Chart is empty, CreateChart scaffolds the default chart from helm's
    85  // default template.
    86  //
    87  // If opts.Chart is a local file, CreateChart verifies that it is a valid helm
    88  // chart archive and unpacks it into the project's helm charts directory.
    89  //
    90  // If opts.Chart is a local directory, CreateChart verifies that it is a valid
    91  // helm chart directory and copies it into the project's helm charts directory.
    92  //
    93  // For any other value of opts.Chart, CreateChart attempts to fetch the helm chart
    94  // from a remote repository.
    95  //
    96  // If opts.Repo is not specified, the following chart reference formats are supported:
    97  //
    98  //   - <repoName>/<chartName>: Fetch the helm chart named chartName from the helm
    99  //                             chart repository named repoName, as specified in the
   100  //                             $HELM_HOME/repositories/repositories.yaml file.
   101  //
   102  //   - <url>: Fetch the helm chart archive at the specified URL.
   103  //
   104  // If opts.Repo is specified, only one chart reference format is supported:
   105  //
   106  //   - <chartName>: Fetch the helm chart named chartName in the helm chart repository
   107  //                  specified by opts.Repo
   108  //
   109  // If opts.Version is not set, CreateChart will fetch the latest available version of
   110  // the helm chart. Otherwise, CreateChart will fetch the specified version.
   111  // opts.Version is not used when opts.Chart itself refers to a specific version, for
   112  // example when it is a local path or a URL.
   113  //
   114  // CreateChart returns an error if an error occurs creating the scaffold.Resource or
   115  // creating the chart.
   116  func CreateChart(projectDir string, opts CreateChartOptions) (*scaffold.Resource, *chart.Chart, error) {
   117  	chartsDir := filepath.Join(projectDir, HelmChartsDir)
   118  	err := os.MkdirAll(chartsDir, 0755)
   119  	if err != nil {
   120  		return nil, nil, fmt.Errorf("failed to create helm-charts directory: %s", err)
   121  	}
   122  
   123  	var (
   124  		r *scaffold.Resource
   125  		c *chart.Chart
   126  	)
   127  
   128  	// If we don't have a helm chart reference, scaffold the default chart
   129  	// from Helm's default template. Otherwise, fetch it.
   130  	if len(opts.Chart) == 0 {
   131  		r, c, err = scaffoldChart(chartsDir, opts.ResourceAPIVersion, opts.ResourceKind)
   132  		if err != nil {
   133  			return nil, nil, fmt.Errorf("failed to scaffold default chart: %s", err)
   134  		}
   135  	} else {
   136  		r, c, err = fetchChart(chartsDir, opts)
   137  		if err != nil {
   138  			return nil, nil, fmt.Errorf("failed to fetch chart: %s", err)
   139  		}
   140  	}
   141  
   142  	relChartPath := filepath.Join(HelmChartsDir, c.GetMetadata().GetName())
   143  	absChartPath := filepath.Join(projectDir, relChartPath)
   144  	if err := fetchChartDependencies(absChartPath); err != nil {
   145  		return nil, nil, fmt.Errorf("failed to fetch chart dependencies: %s", err)
   146  	}
   147  
   148  	// Reload chart in case dependencies changed
   149  	c, err = chartutil.Load(absChartPath)
   150  	if err != nil {
   151  		return nil, nil, fmt.Errorf("failed to load chart: %s", err)
   152  	}
   153  
   154  	log.Infof("Created %s", relChartPath)
   155  	return r, c, nil
   156  }
   157  
   158  func scaffoldChart(destDir, apiVersion, kind string) (*scaffold.Resource, *chart.Chart, error) {
   159  	r, err := scaffold.NewResource(apiVersion, kind)
   160  	if err != nil {
   161  		return nil, nil, err
   162  	}
   163  
   164  	chartfile := &chart.Metadata{
   165  		// Many helm charts use hyphenated names, but we chose not to because
   166  		// of the issues related to how hyphens are interpreted in templates.
   167  		// See https://github.com/helm/helm/issues/2192
   168  		Name:        r.LowerKind,
   169  		Description: "A Helm chart for Kubernetes",
   170  		Version:     "0.1.0",
   171  		AppVersion:  "1.0",
   172  		ApiVersion:  chartutil.ApiVersionV1,
   173  	}
   174  	chartPath, err := chartutil.Create(chartfile, destDir)
   175  	if err != nil {
   176  		return nil, nil, err
   177  	}
   178  
   179  	chart, err := chartutil.Load(chartPath)
   180  	if err != nil {
   181  		return nil, nil, err
   182  	}
   183  	return r, chart, nil
   184  }
   185  
   186  func fetchChart(destDir string, opts CreateChartOptions) (*scaffold.Resource, *chart.Chart, error) {
   187  	var (
   188  		stat  os.FileInfo
   189  		chart *chart.Chart
   190  		err   error
   191  	)
   192  
   193  	if stat, err = os.Stat(opts.Chart); err == nil {
   194  		chart, err = createChartFromDisk(destDir, opts.Chart, stat.IsDir())
   195  	} else {
   196  		chart, err = createChartFromRemote(destDir, opts)
   197  	}
   198  	if err != nil {
   199  		return nil, nil, err
   200  	}
   201  
   202  	chartName := chart.GetMetadata().GetName()
   203  	if len(opts.ResourceAPIVersion) == 0 {
   204  		opts.ResourceAPIVersion = DefaultAPIVersion
   205  	}
   206  	if len(opts.ResourceKind) == 0 {
   207  		opts.ResourceKind = strcase.ToCamel(chartName)
   208  	}
   209  
   210  	r, err := scaffold.NewResource(opts.ResourceAPIVersion, opts.ResourceKind)
   211  	if err != nil {
   212  		return nil, nil, err
   213  	}
   214  	return r, chart, nil
   215  }
   216  
   217  func createChartFromDisk(destDir, source string, isDir bool) (*chart.Chart, error) {
   218  	chart, err := chartutil.Load(source)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	// Save it into our project's helm-charts directory.
   224  	if err := chartutil.SaveDir(chart, destDir); err != nil {
   225  		return nil, err
   226  	}
   227  	return chart, nil
   228  }
   229  
   230  func createChartFromRemote(destDir string, opts CreateChartOptions) (*chart.Chart, error) {
   231  	helmHome, ok := os.LookupEnv(environment.HomeEnvVar)
   232  	if !ok {
   233  		helmHome = environment.DefaultHelmHome
   234  	}
   235  	getters := getter.All(environment.EnvSettings{})
   236  	c := downloader.ChartDownloader{
   237  		HelmHome: helmpath.Home(helmHome),
   238  		Out:      os.Stderr,
   239  		Getters:  getters,
   240  	}
   241  
   242  	if opts.Repo != "" {
   243  		chartURL, err := repo.FindChartInRepoURL(opts.Repo, opts.Chart, opts.Version, "", "", "", getters)
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  		opts.Chart = chartURL
   248  	}
   249  
   250  	tmpDir, err := ioutil.TempDir("", "osdk-helm-chart")
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  	defer func() {
   255  		if err := os.RemoveAll(tmpDir); err != nil {
   256  			log.Errorf("Failed to remove temporary directory %s: %s", tmpDir, err)
   257  		}
   258  	}()
   259  
   260  	chartArchive, _, err := c.DownloadTo(opts.Chart, opts.Version, tmpDir)
   261  	if err != nil {
   262  		// One of Helm's error messages directs users to run `helm init`, which
   263  		// installs tiller in a remote cluster. Since that's unnecessary and
   264  		// unhelpful, modify the error message to be relevant for operator-sdk.
   265  		if strings.Contains(err.Error(), "Couldn't load repositories file") {
   266  			return nil, fmt.Errorf("failed to load repositories file %s "+
   267  				"(you might need to run `helm init --client-only` "+
   268  				"to create and initialize it)", c.HelmHome.RepositoryFile())
   269  		}
   270  		return nil, err
   271  	}
   272  
   273  	return createChartFromDisk(destDir, chartArchive, false)
   274  }
   275  
   276  func fetchChartDependencies(chartPath string) error {
   277  	helmHome, ok := os.LookupEnv(environment.HomeEnvVar)
   278  	if !ok {
   279  		helmHome = environment.DefaultHelmHome
   280  	}
   281  	getters := getter.All(environment.EnvSettings{})
   282  
   283  	out := &bytes.Buffer{}
   284  	man := &downloader.Manager{
   285  		Out:       out,
   286  		ChartPath: chartPath,
   287  		HelmHome:  helmpath.Home(helmHome),
   288  		Getters:   getters,
   289  	}
   290  	if err := man.Build(); err != nil {
   291  		fmt.Println(out.String())
   292  		return err
   293  	}
   294  	return nil
   295  }