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