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 }