github.com/metaprov/modela-operator@v0.0.0-20240118193048-f378be8b74d2/pkg/helm/helm_chart.go (about) 1 /* 2 * Copyright (c) 2021. 3 * 4 * Metaprov.com 5 */ 6 7 package helm 8 9 import ( 10 "bufio" 11 "bytes" 12 "context" 13 "fmt" 14 "github.com/metaprov/modela-operator/pkg/kube" 15 "k8s.io/cli-runtime/pkg/genericclioptions" 16 "regexp" 17 ctrl "sigs.k8s.io/controller-runtime" 18 "sigs.k8s.io/kustomize/kyaml/kio" 19 20 "k8s.io/klog/v2" 21 "sigs.k8s.io/controller-runtime/pkg/log" 22 23 "github.com/pkg/errors" 24 helmaction "helm.sh/helm/v3/pkg/action" 25 helmchart "helm.sh/helm/v3/pkg/chart" 26 helmloader "helm.sh/helm/v3/pkg/chart/loader" 27 helmcli "helm.sh/helm/v3/pkg/cli" 28 helmrelease "helm.sh/helm/v3/pkg/release" 29 ) 30 31 var settings = helmcli.New() 32 33 type LabelPostRenderer struct { 34 Labels map[string]string 35 } 36 37 func (lb LabelPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { 38 var output bytes.Buffer 39 var writer = bufio.NewWriter(&output) 40 rw := &kio.ByteReadWriter{ 41 Reader: bytes.NewReader(renderedManifests.Bytes()), 42 Writer: writer, 43 OmitReaderAnnotations: true, 44 KeepReaderAnnotations: true, 45 } 46 p := kio.Pipeline{ 47 Inputs: []kio.Reader{rw}, 48 Filters: []kio.Filter{kube.LabelFilter{Labels: lb.Labels}}, 49 Outputs: []kio.Writer{rw}, 50 } 51 52 if err := p.Execute(); err != nil { 53 return nil, err 54 } 55 56 _ = writer.Flush() 57 return bytes.NewBuffer(output.Bytes()), nil 58 } 59 60 type HelmChart struct { 61 Name string // chart name 62 Namespace string // chart namespace 63 ReleaseName string // release name 64 ChartVersion string // chart version 65 DryRun bool 66 CreateNamespace bool 67 Values map[string]interface{} 68 chart *helmchart.Chart 69 } 70 71 func NewHelmChart(name, namespace, releaseName string, dryRun bool) *HelmChart { 72 return &HelmChart{ 73 Name: name, 74 Namespace: namespace, 75 ReleaseName: releaseName, 76 DryRun: dryRun, 77 CreateNamespace: false, 78 Values: make(map[string]interface{}), 79 } 80 } 81 82 func (chart *HelmChart) GetConfig() (*helmaction.Configuration, error) { 83 var kubeConfig *genericclioptions.ConfigFlags 84 config := ctrl.GetConfigOrDie() 85 kubeConfig = genericclioptions.NewConfigFlags(false) 86 kubeConfig.APIServer = &config.Host 87 kubeConfig.BearerToken = &config.BearerToken 88 kubeConfig.CAFile = &config.CAFile 89 kubeConfig.Namespace = &chart.Namespace 90 91 actionConfig := new(helmaction.Configuration) 92 if err := actionConfig.Init(kubeConfig, chart.Namespace, "secret", klog.Infof); err != nil { 93 klog.Error(err, "Unable to initialize Helm") 94 return nil, err 95 } 96 97 return actionConfig, nil 98 } 99 100 // Load the chart, and assign it to the chart field 101 func (chart *HelmChart) Load(ctx context.Context) error { 102 logger := log.FromContext(ctx) 103 config, err := chart.GetConfig() 104 if err != nil { 105 logger.Error(err, "Failed to get config") 106 return err 107 } 108 109 client := helmaction.NewInstall(config) 110 client.Namespace = chart.Namespace 111 client.ReleaseName = chart.ReleaseName 112 name := "assets/charts/" + chart.Name 113 114 result, err := helmloader.Load(name) 115 if err != nil { 116 logger.Error(err, "Failed to load Helm Chart") 117 return errors.Wrapf(err, "Failed to load resources from %s", name) 118 } 119 chart.chart = result 120 return nil 121 } 122 123 func (chart *HelmChart) Version() string { 124 chartPackageSplit := chart.parsePackageName() 125 chartVersion := chartPackageSplit[1] 126 if chartPackageSplit[2] != "" { 127 chartVersion = fmt.Sprintf("%s-%s", chartVersion, chartPackageSplit[2]) 128 } 129 return chartVersion 130 } 131 132 func (chart *HelmChart) parsePackageName() []string { 133 packageNameRegexp := regexp.MustCompile(`([a-z\-]+)-([0-9\.]*[0-9]+)(-([0-9]+))?`) 134 packageSubstringSubmatch := packageNameRegexp.FindStringSubmatch(chart.Name) 135 parsedOutput := []string{"", "", ""} 136 if len(packageSubstringSubmatch) > 2 { 137 parsedOutput[0] = packageSubstringSubmatch[1] 138 parsedOutput[1] = packageSubstringSubmatch[2] 139 } 140 if len(packageSubstringSubmatch) > 4 { 141 parsedOutput[2] = packageSubstringSubmatch[4] 142 } 143 144 return parsedOutput 145 } 146 147 func (chart *HelmChart) CanInstall(ctx context.Context) (bool, error) { 148 err := chart.Load(ctx) 149 if err != nil { 150 return false, err 151 } 152 switch chart.chart.Metadata.Type { 153 case "", "application": 154 return true, err 155 } 156 return false, err 157 } 158 159 func (chart *HelmChart) Get(ctx context.Context) (*helmrelease.Release, error) { 160 logger := log.FromContext(ctx) 161 162 config, err := chart.GetConfig() 163 if err != nil { 164 logger.Error(err, "failed to get config") 165 return nil, err 166 } 167 // Check if the Release Exists 168 aList := helmaction.NewList(config) // NewGet provides bad error message if release doesn't exist 169 aList.All = true 170 charts, err := aList.Run() 171 if err != nil { 172 logger.Error(err, "failed to get config") 173 return nil, errors.Wrap(err, "failed to run get") 174 } 175 for _, release := range charts { 176 if release.Name == chart.ReleaseName && release.Namespace == chart.Namespace { 177 return release, nil 178 } 179 } 180 return nil, errors.Errorf("unable to find release '%s' in namespace '%s'", chart.ReleaseName, chart.Namespace) 181 } 182 183 // check if the chart is already installed 184 func (chart *HelmChart) IsInstalled(ctx context.Context) (bool, error) { 185 logger := log.FromContext(ctx) 186 err := chart.Load(ctx) 187 if err != nil { 188 logger.Error(err, "failed to load chart") 189 return false, errors.Wrapf(err, "failed to load chart") 190 } 191 existingRelease, _ := chart.Get(ctx) 192 /*if err != nil { 193 logger.Error(err, "failed to get chart") 194 return false, err 195 }*/ 196 if existingRelease != nil { 197 return true, nil 198 } 199 return false, nil 200 } 201 202 func (chart *HelmChart) GetStatus(ctx context.Context) (helmrelease.Status, error) { 203 err := chart.Load(ctx) 204 if err != nil { 205 return helmrelease.StatusUnknown, errors.Wrapf(err, "failed to load chart") 206 } 207 existingRelease, err := chart.Get(ctx) 208 if err != nil { 209 return helmrelease.StatusUnknown, errors.Wrapf(err, "chart does not exist") 210 } 211 212 return existingRelease.Info.Status, nil 213 214 } 215 216 func (chart *HelmChart) Install(ctx context.Context) error { 217 logger := log.FromContext(ctx) 218 logger.Info("Installing Helm Chart", "release", chart.ReleaseName, "namespace", chart.Namespace, "name", chart.Name) 219 err := chart.Load(ctx) 220 if err != nil { 221 logger.Error(err, "Failed to load chart") 222 return errors.Wrapf(err, "Failed to load chart") 223 } 224 // Check if resource already exists 225 existingRelease, _ := chart.Get(ctx) 226 if existingRelease != nil { 227 logger.Error(err, fmt.Sprintf("Release \"%s\" already exists in namespace \"%s\"", existingRelease.Name, existingRelease.Namespace)) 228 return errors.Wrapf(err, "Release '%s' already exists in namespace '%s'", existingRelease.Name, existingRelease.Namespace) 229 } 230 231 can, err := chart.CanInstall(ctx) 232 if err != nil { 233 logger.Error(err, "Failed to check if Helm Chart is installed", "namespace", existingRelease.Namespace) 234 return errors.Wrapf(err, "Failed to check if Helm Chart is installed (namespace=%s)", existingRelease.Namespace) 235 } 236 if !can { 237 return errors.Wrapf(err, "release at '%s' is not installable", chart.Name) 238 } 239 240 config, err := chart.GetConfig() 241 if err != nil { 242 logger.Error(err, "failed to get config") 243 return errors.Wrap(err, "failed to get config") 244 } 245 246 inst := helmaction.NewInstall(config) 247 if inst.Version == "" && inst.Devel { 248 inst.Version = ">0.0.0-0" 249 } 250 inst.ReleaseName = chart.ReleaseName 251 inst.Namespace = chart.Namespace 252 inst.DryRun = chart.DryRun 253 inst.CreateNamespace = chart.CreateNamespace 254 inst.Version = chart.ChartVersion 255 inst.PostRenderer = LabelPostRenderer{map[string]string{"app.kubernetes.io/created-by": "modela-operator"}} 256 inst.Replace = true 257 inst.ClientOnly = false 258 259 _, err = inst.Run(chart.chart, chart.Values) 260 if err != nil { 261 logger.Error(err, "failed to install") 262 return fmt.Errorf("failed to run install due to %s", err) 263 } 264 return nil 265 266 } 267 268 func (chart *HelmChart) Upgrade(ctx context.Context) error { 269 logger := log.FromContext(ctx) 270 271 logger.Info("Enter upgrade") 272 273 err := chart.Load(ctx) 274 if err != nil { 275 logger.Error(err, "failed to load chart") 276 return errors.Wrapf(err, "failed to load chart") 277 } 278 // Check if resource already exists 279 existingRelease, err := chart.Get(ctx) 280 if existingRelease != nil { 281 return errors.Wrapf(err, "release '%s' already exists in namespace '%s'", existingRelease.Name, existingRelease.Namespace) 282 } 283 284 can, err := chart.CanInstall(ctx) 285 if err != nil { 286 return errors.Wrapf(err, "failed to check if chart is installed '%s'", existingRelease.Namespace) 287 } 288 if !can { 289 return errors.Wrapf(err, "release at '%s' is not installable", chart.Name) 290 } 291 292 config, err := chart.GetConfig() 293 if err != nil { 294 return errors.Wrap(err, "failed to get config") 295 } 296 297 isInstalled, err := chart.IsInstalled(ctx) 298 if err != nil { 299 return fmt.Errorf("failed to get installed state %s", err) 300 } 301 302 if !isInstalled { 303 inst := helmaction.NewInstall(config) 304 if inst.Version == "" && inst.Devel { 305 inst.Version = ">0.0.0-0" 306 } 307 inst.ReleaseName = chart.ReleaseName 308 inst.Namespace = chart.Namespace 309 inst.DryRun = chart.DryRun 310 inst.CreateNamespace = chart.CreateNamespace 311 inst.Version = chart.ChartVersion 312 313 _, err = inst.Run(chart.chart, chart.Values) 314 if err != nil { 315 return fmt.Errorf("failed to run install due to %s", err) 316 } 317 return nil 318 } else { 319 inst := helmaction.NewUpgrade(config) 320 if inst.Version == "" && inst.Devel { 321 inst.Version = ">0.0.0-0" 322 } 323 inst.DryRun = chart.DryRun 324 inst.Version = chart.ChartVersion 325 326 _, err = inst.Run(chart.ReleaseName, chart.chart, chart.Values) 327 if err != nil { 328 return fmt.Errorf("failed to run install due to %s", err) 329 } 330 return nil 331 } 332 333 } 334 335 func (chart *HelmChart) Uninstall(ctx context.Context) error { 336 err := chart.Load(ctx) 337 if err != nil { 338 return errors.Wrapf(err, "failed to load chart") 339 } 340 // Check if resource already exists 341 existingRelease, _ := chart.Get(ctx) 342 if existingRelease == nil { 343 return nil 344 } 345 346 config, err := chart.GetConfig() 347 if err != nil { 348 return errors.Wrap(err, "failed to get config") 349 } 350 351 inst := helmaction.NewUninstall(config) 352 inst.DryRun = chart.DryRun 353 354 _, err = inst.Run(chart.ReleaseName) 355 if err != nil { 356 return fmt.Errorf("failed to run uninstall due to %s", err) 357 } 358 return nil 359 } 360 361 func InstallChart(ctx context.Context, name, ns, releaseName string, values map[string]interface{}) error { 362 chart := NewHelmChart(name, ns, releaseName, false) 363 chart.ReleaseName = releaseName 364 chart.Namespace = ns 365 chart.Values = values 366 367 canInstall, err := chart.CanInstall(ctx) 368 if err != nil { 369 return errors.Errorf("Failed to check if chart is installed ,err: %s", err) 370 } 371 if canInstall { 372 err = chart.Install(ctx) 373 if err != nil { 374 return errors.Wrapf(err, "Error installing chart %s", name) 375 } 376 } 377 return nil 378 } 379 380 func UninstallChart(ctx context.Context, name, ns, releaseName string, values map[string]interface{}) error { 381 chart := NewHelmChart(name, ns, releaseName, false) 382 chart.ReleaseName = releaseName 383 chart.Namespace = ns 384 chart.Values = values 385 386 if installed, err := chart.IsInstalled(ctx); err != nil { 387 return err 388 } else if !installed { 389 return nil 390 } 391 392 if err := chart.Uninstall(ctx); err != nil { 393 return errors.Wrapf(err, "Error uninstalling chart %s", name) 394 } 395 396 return nil 397 } 398 399 func IsChartInstalled(ctx context.Context, name, ns, releaseName string) (bool, error) { 400 // TODO(liam): Refactor these methods into the helm chart struct 401 chart := NewHelmChart(name, ns, releaseName, false) 402 403 chartStatus, _ := chart.GetStatus(ctx) 404 if chartStatus == helmrelease.StatusUnknown { 405 return false, nil 406 } 407 if chartStatus != helmrelease.StatusDeployed { 408 return false, nil 409 } 410 return true, nil 411 }