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