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