github.com/jmrodri/operator-sdk@v0.5.0/pkg/helm/release/manager.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 release 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "strings" 24 25 yaml "gopkg.in/yaml.v2" 26 apierrors "k8s.io/apimachinery/pkg/api/errors" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 apitypes "k8s.io/apimachinery/pkg/types" 30 "k8s.io/cli-runtime/pkg/genericclioptions/resource" 31 "k8s.io/client-go/rest" 32 "k8s.io/helm/pkg/chartutil" 33 "k8s.io/helm/pkg/kube" 34 cpb "k8s.io/helm/pkg/proto/hapi/chart" 35 rpb "k8s.io/helm/pkg/proto/hapi/release" 36 "k8s.io/helm/pkg/proto/hapi/services" 37 "k8s.io/helm/pkg/storage" 38 "k8s.io/helm/pkg/tiller" 39 40 "github.com/mattbaird/jsonpatch" 41 "github.com/operator-framework/operator-sdk/pkg/helm/internal/types" 42 ) 43 44 var ( 45 // ErrNotFound indicates the release was not found. 46 ErrNotFound = errors.New("release not found") 47 ) 48 49 // Manager manages a Helm release. It can install, update, reconcile, 50 // and uninstall a release. 51 type Manager interface { 52 ReleaseName() string 53 IsInstalled() bool 54 IsUpdateRequired() bool 55 Sync(context.Context) error 56 InstallRelease(context.Context) (*rpb.Release, error) 57 UpdateRelease(context.Context) (*rpb.Release, *rpb.Release, error) 58 ReconcileRelease(context.Context) (*rpb.Release, error) 59 UninstallRelease(context.Context) (*rpb.Release, error) 60 } 61 62 type manager struct { 63 storageBackend *storage.Storage 64 tillerKubeClient *kube.Client 65 chartDir string 66 67 tiller *tiller.ReleaseServer 68 releaseName string 69 namespace string 70 71 spec interface{} 72 status *types.HelmAppStatus 73 74 isInstalled bool 75 isUpdateRequired bool 76 deployedRelease *rpb.Release 77 chart *cpb.Chart 78 config *cpb.Config 79 } 80 81 // ReleaseName returns the name of the release. 82 func (m manager) ReleaseName() string { 83 return m.releaseName 84 } 85 86 func (m manager) IsInstalled() bool { 87 return m.isInstalled 88 } 89 90 func (m manager) IsUpdateRequired() bool { 91 return m.isUpdateRequired 92 } 93 94 // Sync ensures the Helm storage backend is in sync with the status of the 95 // custom resource. 96 func (m *manager) Sync(ctx context.Context) error { 97 if err := m.syncReleaseStatus(*m.status); err != nil { 98 return fmt.Errorf("failed to sync release status to storage backend: %s", err) 99 } 100 101 // Get release history for this release name 102 releases, err := m.storageBackend.History(m.releaseName) 103 if err != nil && !notFoundErr(err) { 104 return fmt.Errorf("failed to retrieve release history: %s", err) 105 } 106 107 // Cleanup non-deployed release versions. If all release versions are 108 // non-deployed, this will ensure that failed installations are correctly 109 // retried. 110 for _, rel := range releases { 111 if rel.GetInfo().GetStatus().GetCode() != rpb.Status_DEPLOYED { 112 _, err := m.storageBackend.Delete(rel.GetName(), rel.GetVersion()) 113 if err != nil && !notFoundErr(err) { 114 return fmt.Errorf("failed to delete stale release version: %s", err) 115 } 116 } 117 } 118 119 // Load the chart and config based on the current state of the custom resource. 120 chart, config, err := m.loadChartAndConfig() 121 if err != nil { 122 return fmt.Errorf("failed to load chart and config: %s", err) 123 } 124 m.chart = chart 125 m.config = config 126 127 // Load the most recently deployed release from the storage backend. 128 deployedRelease, err := m.getDeployedRelease() 129 if err == ErrNotFound { 130 return nil 131 } 132 if err != nil { 133 return fmt.Errorf("failed to get deployed release: %s", err) 134 } 135 m.deployedRelease = deployedRelease 136 m.isInstalled = true 137 138 // Get the next candidate release to determine if an update is necessary. 139 candidateRelease, err := m.getCandidateRelease(ctx, m.tiller, m.releaseName, chart, config) 140 if err != nil { 141 return fmt.Errorf("failed to get candidate release: %s", err) 142 } 143 if deployedRelease.GetManifest() != candidateRelease.GetManifest() { 144 m.isUpdateRequired = true 145 } 146 147 return nil 148 } 149 150 func (m manager) syncReleaseStatus(status types.HelmAppStatus) error { 151 var release *rpb.Release 152 for _, condition := range status.Conditions { 153 if condition.Type == types.ConditionDeployed && condition.Status == types.StatusTrue { 154 release = condition.Release 155 break 156 } 157 } 158 if release == nil { 159 return nil 160 } 161 162 name := release.GetName() 163 version := release.GetVersion() 164 _, err := m.storageBackend.Get(name, version) 165 if err == nil { 166 return nil 167 } 168 169 if !notFoundErr(err) { 170 return err 171 } 172 return m.storageBackend.Create(release) 173 } 174 175 func notFoundErr(err error) bool { 176 return strings.Contains(err.Error(), "not found") 177 } 178 179 func (m manager) loadChartAndConfig() (*cpb.Chart, *cpb.Config, error) { 180 // chart is mutated by the call to processRequirements, 181 // so we need to reload it from disk every time. 182 chart, err := chartutil.LoadDir(m.chartDir) 183 if err != nil { 184 return nil, nil, fmt.Errorf("failed to load chart: %s", err) 185 } 186 187 cr, err := yaml.Marshal(m.spec) 188 if err != nil { 189 return nil, nil, fmt.Errorf("failed to parse values: %s", err) 190 } 191 config := &cpb.Config{Raw: string(cr)} 192 193 err = processRequirements(chart, config) 194 if err != nil { 195 return nil, nil, fmt.Errorf("failed to process chart requirements: %s", err) 196 } 197 return chart, config, nil 198 } 199 200 // processRequirements will process the requirements file 201 // It will disable/enable the charts based on condition in requirements file 202 // Also imports the specified chart values from child to parent. 203 func processRequirements(chart *cpb.Chart, values *cpb.Config) error { 204 err := chartutil.ProcessRequirementsEnabled(chart, values) 205 if err != nil { 206 return err 207 } 208 err = chartutil.ProcessRequirementsImportValues(chart) 209 if err != nil { 210 return err 211 } 212 return nil 213 } 214 215 func (m manager) getDeployedRelease() (*rpb.Release, error) { 216 deployedRelease, err := m.storageBackend.Deployed(m.releaseName) 217 if err != nil { 218 if strings.Contains(err.Error(), "has no deployed releases") { 219 return nil, ErrNotFound 220 } 221 return nil, err 222 } 223 return deployedRelease, nil 224 } 225 226 func (m manager) getCandidateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) { 227 dryRunReq := &services.UpdateReleaseRequest{ 228 Name: name, 229 Chart: chart, 230 Values: config, 231 DryRun: true, 232 } 233 dryRunResponse, err := tiller.UpdateRelease(ctx, dryRunReq) 234 if err != nil { 235 return nil, err 236 } 237 return dryRunResponse.GetRelease(), nil 238 } 239 240 // InstallRelease performs a Helm release install. 241 func (m manager) InstallRelease(ctx context.Context) (*rpb.Release, error) { 242 return installRelease(ctx, m.tiller, m.namespace, m.releaseName, m.chart, m.config) 243 } 244 245 func installRelease(ctx context.Context, tiller *tiller.ReleaseServer, namespace, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) { 246 installReq := &services.InstallReleaseRequest{ 247 Namespace: namespace, 248 Name: name, 249 Chart: chart, 250 Values: config, 251 } 252 253 releaseResponse, err := tiller.InstallRelease(ctx, installReq) 254 if err != nil { 255 // Workaround for helm/helm#3338 256 if releaseResponse.GetRelease() != nil { 257 uninstallReq := &services.UninstallReleaseRequest{ 258 Name: releaseResponse.GetRelease().GetName(), 259 Purge: true, 260 } 261 _, uninstallErr := tiller.UninstallRelease(ctx, uninstallReq) 262 if uninstallErr != nil { 263 return nil, fmt.Errorf("failed to roll back failed installation: %s: %s", uninstallErr, err) 264 } 265 } 266 return nil, err 267 } 268 return releaseResponse.GetRelease(), nil 269 } 270 271 // UpdateRelease performs a Helm release update. 272 func (m manager) UpdateRelease(ctx context.Context) (*rpb.Release, *rpb.Release, error) { 273 updatedRelease, err := updateRelease(ctx, m.tiller, m.releaseName, m.chart, m.config) 274 return m.deployedRelease, updatedRelease, err 275 } 276 277 func updateRelease(ctx context.Context, tiller *tiller.ReleaseServer, name string, chart *cpb.Chart, config *cpb.Config) (*rpb.Release, error) { 278 updateReq := &services.UpdateReleaseRequest{ 279 Name: name, 280 Chart: chart, 281 Values: config, 282 } 283 284 releaseResponse, err := tiller.UpdateRelease(ctx, updateReq) 285 if err != nil { 286 // Workaround for helm/helm#3338 287 if releaseResponse.GetRelease() != nil { 288 rollbackReq := &services.RollbackReleaseRequest{ 289 Name: name, 290 Force: true, 291 } 292 _, rollbackErr := tiller.RollbackRelease(ctx, rollbackReq) 293 if rollbackErr != nil { 294 return nil, fmt.Errorf("failed to roll back failed update: %s: %s", rollbackErr, err) 295 } 296 } 297 return nil, err 298 } 299 return releaseResponse.GetRelease(), nil 300 } 301 302 // ReconcileRelease creates or patches resources as necessary to match the 303 // deployed release's manifest. 304 func (m manager) ReconcileRelease(ctx context.Context) (*rpb.Release, error) { 305 err := reconcileRelease(ctx, m.tillerKubeClient, m.namespace, m.deployedRelease.GetManifest()) 306 return m.deployedRelease, err 307 } 308 309 func reconcileRelease(ctx context.Context, tillerKubeClient *kube.Client, namespace string, expectedManifest string) error { 310 expectedInfos, err := tillerKubeClient.BuildUnstructured(namespace, bytes.NewBufferString(expectedManifest)) 311 if err != nil { 312 return err 313 } 314 return expectedInfos.Visit(func(expected *resource.Info, err error) error { 315 if err != nil { 316 return err 317 } 318 319 expectedClient := resource.NewClientWithOptions(expected.Client, func(r *rest.Request) { 320 *r = *r.Context(ctx) 321 }) 322 helper := resource.NewHelper(expectedClient, expected.Mapping) 323 324 existing, err := helper.Get(expected.Namespace, expected.Name, false) 325 if apierrors.IsNotFound(err) { 326 if _, err := helper.Create(expected.Namespace, true, expected.Object, &metav1.CreateOptions{}); err != nil { 327 return fmt.Errorf("create error: %s", err) 328 } 329 return nil 330 } else if err != nil { 331 return err 332 } 333 334 patch, err := generatePatch(existing, expected.Object) 335 if err != nil { 336 return fmt.Errorf("failed to marshal JSON patch: %s", err) 337 } 338 339 if patch == nil { 340 return nil 341 } 342 343 _, err = helper.Patch(expected.Namespace, expected.Name, apitypes.JSONPatchType, patch, &metav1.UpdateOptions{}) 344 if err != nil { 345 return fmt.Errorf("patch error: %s", err) 346 } 347 return nil 348 }) 349 } 350 351 func generatePatch(existing, expected runtime.Object) ([]byte, error) { 352 existingJSON, err := json.Marshal(existing) 353 if err != nil { 354 return nil, err 355 } 356 expectedJSON, err := json.Marshal(expected) 357 if err != nil { 358 return nil, err 359 } 360 361 ops, err := jsonpatch.CreatePatch(existingJSON, expectedJSON) 362 if err != nil { 363 return nil, err 364 } 365 366 // We ignore the "remove" operations from the full patch because they are 367 // fields added by Kubernetes or by the user after the existing release 368 // resource has been applied. The goal for this patch is to make sure that 369 // the fields managed by the Helm chart are applied. 370 patchOps := make([]jsonpatch.JsonPatchOperation, 0) 371 for _, op := range ops { 372 if op.Operation != "remove" { 373 patchOps = append(patchOps, op) 374 } 375 } 376 377 // If there are no patch operations, return nil. Callers are expected 378 // to check for a nil response and skip the patch operation to avoid 379 // unnecessary chatter with the API server. 380 if len(patchOps) == 0 { 381 return nil, nil 382 } 383 384 return json.Marshal(patchOps) 385 } 386 387 // UninstallRelease performs a Helm release uninstall. 388 func (m manager) UninstallRelease(ctx context.Context) (*rpb.Release, error) { 389 return uninstallRelease(ctx, m.storageBackend, m.tiller, m.releaseName) 390 } 391 392 func uninstallRelease(ctx context.Context, storageBackend *storage.Storage, tiller *tiller.ReleaseServer, releaseName string) (*rpb.Release, error) { 393 // Get history of this release 394 h, err := storageBackend.History(releaseName) 395 if err != nil { 396 return nil, fmt.Errorf("failed to get release history: %s", err) 397 } 398 399 // If there is no history, the release has already been uninstalled, 400 // so return ErrNotFound. 401 if len(h) == 0 { 402 return nil, ErrNotFound 403 } 404 405 uninstallResponse, err := tiller.UninstallRelease(ctx, &services.UninstallReleaseRequest{ 406 Name: releaseName, 407 Purge: true, 408 }) 409 return uninstallResponse.GetRelease(), err 410 }