github.com/banzaicloud/operator-tools@v0.28.10/pkg/helm/templatereconciler/reconciler.go (about) 1 // Copyright © 2020 Banzai Cloud 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 templatereconciler 16 17 import ( 18 "context" 19 "net/http" 20 "time" 21 22 "emperror.dev/errors" 23 "github.com/go-logr/logr" 24 "helm.sh/helm/v3/pkg/action" 25 "helm.sh/helm/v3/pkg/chartutil" 26 v1 "k8s.io/api/core/v1" 27 "k8s.io/apimachinery/pkg/api/meta" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/client-go/discovery" 31 controllerruntime "sigs.k8s.io/controller-runtime" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 35 "github.com/banzaicloud/operator-tools/pkg/inventory" 36 "github.com/banzaicloud/operator-tools/pkg/logger" 37 "github.com/banzaicloud/operator-tools/pkg/reconciler" 38 "github.com/banzaicloud/operator-tools/pkg/resources" 39 "github.com/banzaicloud/operator-tools/pkg/types" 40 ) 41 42 type ReleaseData struct { 43 Chart http.FileSystem 44 Values map[string]interface{} 45 Namespace string 46 ChartName string 47 ReleaseName string 48 // Layers can be embedded into CRDs directly to provide flexible override mechanisms 49 Layers []resources.K8SResourceOverlay 50 // Modifiers can be used from client code to modify resources before being applied 51 Modifiers []resources.ObjectModifierFunc 52 // DesiredStateOverrides can be used to override desired states of certain objects 53 DesiredStateOverrides map[reconciler.ObjectKeyWithGVK]reconciler.DesiredState 54 } 55 56 type Component interface { 57 Name() string 58 Skipped(runtime.Object) bool 59 Enabled(runtime.Object) bool 60 PreChecks(runtime.Object) error 61 ReleaseData(runtime.Object) (*ReleaseData, error) 62 UpdateStatus(object runtime.Object, status types.ReconcileStatus, message string) error 63 } 64 65 type HelmReconciler struct { 66 client client.Client 67 scheme *runtime.Scheme 68 logger logr.Logger 69 inventory *inventory.Inventory 70 nativeReconcilerOpts []reconciler.NativeReconcilerOpt 71 genericReconcilerOpts []reconciler.ResourceReconcilerOption 72 objectParser *resources.ObjectParser 73 discovery discovery.DiscoveryInterface 74 manageNamespace bool 75 } 76 77 type preConditionsFatalErr struct { 78 error 79 } 80 81 func NewPreConditionsFatalErr(err error) error { 82 return &preConditionsFatalErr{err} 83 } 84 85 type HelmReconcilerOpt func(*HelmReconciler) 86 87 func WithGenericReconcilerOptions(opts ...reconciler.ResourceReconcilerOption) HelmReconcilerOpt { 88 return func(r *HelmReconciler) { 89 if r.genericReconcilerOpts == nil { 90 r.genericReconcilerOpts = make([]reconciler.ResourceReconcilerOption, 0) 91 } 92 r.genericReconcilerOpts = append(r.genericReconcilerOpts, opts...) 93 } 94 } 95 96 func WithNativeReconcilerOptions(opts ...reconciler.NativeReconcilerOpt) HelmReconcilerOpt { 97 return func(r *HelmReconciler) { 98 if r.nativeReconcilerOpts == nil { 99 r.nativeReconcilerOpts = make([]reconciler.NativeReconcilerOpt, 0) 100 } 101 r.nativeReconcilerOpts = append(r.nativeReconcilerOpts, opts...) 102 } 103 } 104 105 func ManageNamespace(manageNamespace bool) HelmReconcilerOpt { 106 return func(r *HelmReconciler) { 107 r.manageNamespace = manageNamespace 108 } 109 } 110 111 func NewHelmReconciler( 112 client client.Client, 113 scheme *runtime.Scheme, 114 logger logr.Logger, 115 discovery discovery.DiscoveryInterface, 116 nativeReconcilerOpts []reconciler.NativeReconcilerOpt, 117 ) *HelmReconciler { 118 return NewHelmReconcilerWith(client, scheme, logger, discovery, WithNativeReconcilerOptions(nativeReconcilerOpts...)) 119 } 120 121 func NewHelmReconcilerWith( 122 client client.Client, 123 scheme *runtime.Scheme, 124 logger logr.Logger, 125 discovery discovery.DiscoveryInterface, 126 opts ...HelmReconcilerOpt, 127 ) *HelmReconciler { 128 r := &HelmReconciler{ 129 client: client, 130 scheme: scheme, 131 logger: logger, 132 inventory: inventory.NewDiscoveryInventory(client, logger, discovery), 133 discovery: discovery, 134 objectParser: resources.NewObjectParser(scheme), 135 nativeReconcilerOpts: make([]reconciler.NativeReconcilerOpt, 0), 136 genericReconcilerOpts: make([]reconciler.ResourceReconcilerOption, 0), 137 manageNamespace: true, 138 } 139 140 for _, opt := range opts { 141 opt(r) 142 } 143 144 if len(r.genericReconcilerOpts) == 0 { 145 r.genericReconcilerOpts = append(r.genericReconcilerOpts, reconciler.WithEnableRecreateWorkload()) 146 } 147 148 return r 149 } 150 151 func (rec *HelmReconciler) GetClient() client.Client { 152 return rec.client 153 } 154 155 func (rec *HelmReconciler) Reconcile(object runtime.Object, component Component) (*reconcile.Result, error) { 156 var ok bool 157 var parent reconciler.ResourceOwner 158 if parent, ok = object.(reconciler.ResourceOwner); !ok { 159 return nil, errors.New("cannot convert object to ResourceOwner interface") 160 } 161 162 if component.Skipped(object) { 163 return &reconcile.Result{}, component.UpdateStatus(object, types.ReconcileStatusUnmanaged, "") 164 } 165 166 if err := component.UpdateStatus(object, types.ReconcileStatusReconciling, ""); err != nil { 167 rec.logger.Error(err, "status update failed") 168 } 169 rec.logger.Info("reconciling") 170 171 if component.Enabled(object) { 172 if err := component.PreChecks(object); err != nil { 173 if preCondErr, ok := err.(*preConditionsFatalErr); ok { 174 if err := component.UpdateStatus(object, types.ReconcileStatusFailed, preCondErr.Error()); err != nil { 175 rec.logger.Error(err, "status update failed") 176 } 177 178 return nil, preCondErr 179 } 180 if err := component.UpdateStatus(object, types.ReconcileStatusReconciling, "waiting for precondition checks to pass"); err != nil { 181 rec.logger.Error(err, "status update failed") 182 } 183 rec.logger.Error(err, "precondition checks failed") 184 return &reconcile.Result{ 185 RequeueAfter: time.Second * 5, 186 }, nil 187 188 } 189 } 190 191 defer logger.EnableGroupSession(rec.logger)() 192 193 rec.logger.Info("syncing resources") 194 195 releaseData, err := component.ReleaseData(object) 196 if err != nil { 197 return nil, errors.WrapIf(err, "failed to get release data") 198 } 199 200 result, err := rec.reconcile(parent, component, releaseData) 201 if err != nil { 202 uerr := component.UpdateStatus(object, types.ReconcileStatusFailed, err.Error()) 203 if uerr != nil { 204 rec.logger.Error(uerr, "status update failed") 205 } 206 return result, err 207 } else { 208 if component.Skipped(object) { 209 err = component.UpdateStatus(object, types.ReconcileStatusUnmanaged, "") 210 if err != nil { 211 return result, err 212 } 213 } else if component.Enabled(object) { 214 err = component.UpdateStatus(object, types.ReconcileStatusAvailable, "") 215 if err != nil { 216 return result, err 217 } 218 } else { 219 err = component.UpdateStatus(object, types.ReconcileStatusRemoved, "") 220 if err != nil { 221 return result, err 222 } 223 } 224 } 225 226 return result, err 227 } 228 229 func (rec *HelmReconciler) GetResourceBuilders(parent reconciler.ResourceOwner, component Component, releaseData *ReleaseData, doInventory bool) ([]reconciler.ResourceBuilder, error) { 230 var err error 231 resourceBuilders := make([]reconciler.ResourceBuilder, 0) 232 233 if rec.manageNamespace { 234 resourceBuilders, err = reconciler.GetResourceBuildersFromObjects([]runtime.Object{ 235 &v1.Namespace{ 236 TypeMeta: metav1.TypeMeta{ 237 Kind: "Namespace", 238 APIVersion: "v1", 239 }, 240 ObjectMeta: metav1.ObjectMeta{ 241 Name: releaseData.Namespace, 242 }, 243 }, 244 }, reconciler.StateCreated) 245 if err != nil { 246 return nil, err 247 } 248 } 249 250 if component.Enabled(parent) { 251 serverVersion, err := rec.discovery.ServerVersion() 252 if err != nil { 253 return nil, errors.Wrapf(err, "unable to detect server version") 254 } 255 256 apiVersions, err := action.GetVersionSet(rec.discovery) 257 if err != nil { 258 return nil, errors.Wrapf(err, "unable to detect supported API versions") 259 } 260 261 capabilities := chartutil.Capabilities{ 262 KubeVersion: chartutil.KubeVersion{ 263 Version: serverVersion.GitVersion, 264 Major: serverVersion.Major, 265 Minor: serverVersion.Minor, 266 }, 267 APIVersions: apiVersions, 268 } 269 270 objects, state, err := orderedChartObjectsWithState(releaseData, rec.scheme, capabilities) 271 if err != nil { 272 return nil, err 273 } 274 275 modifiers := releaseData.Modifiers 276 277 for _, layer := range releaseData.Layers { 278 modifier, err := resources.PatchYAMLModifier(layer, rec.objectParser) 279 if err != nil { 280 return nil, errors.WrapIf(err, "failed to create modifier from layer") 281 } 282 modifiers = append(modifiers, modifier) 283 } 284 285 chartResourceBuilders, err := reconciler.GetResourceBuildersFromObjects(objects, state, modifiers...) 286 if err != nil { 287 return nil, err 288 } 289 290 resourceBuilders = append(resourceBuilders, chartResourceBuilders...) 291 if doInventory { 292 if resourceBuilders, err = rec.inventory.Append(releaseData.Namespace, releaseData.ReleaseName, parent, resourceBuilders); err != nil { 293 return nil, err 294 } 295 } 296 } else if doInventory { 297 if resourceBuilders, err = rec.inventory.Append(releaseData.Namespace, releaseData.ReleaseName, parent, resourceBuilders); err != nil { 298 return nil, err 299 } 300 } 301 302 return rec.setDesiredStateOverrides(resourceBuilders, releaseData), nil 303 } 304 305 func (rec *HelmReconciler) reconcile(parent reconciler.ResourceOwner, component Component, releaseData *ReleaseData) (*reconcile.Result, error) { 306 resourceBuilders, err := rec.GetResourceBuilders(parent, component, releaseData, true) 307 if err != nil { 308 return nil, err 309 } 310 311 r := reconciler.NewNativeReconciler( 312 component.Name(), 313 reconciler.NewReconcilerWith( 314 rec.client, 315 append(rec.genericReconcilerOpts, reconciler.WithLog(rec.logger), reconciler.WithScheme(rec.scheme))..., 316 ).(*reconciler.GenericResourceReconciler), 317 rec.client, 318 reconciler.NewReconciledComponent( 319 func(_ reconciler.ResourceOwner, _ interface{}) []reconciler.ResourceBuilder { 320 return resourceBuilders 321 }, 322 nil, 323 rec.inventory.TypesToPurge, 324 ), 325 func(_ runtime.Object) (reconciler.ResourceOwner, interface{}) { 326 return nil, nil 327 }, 328 append(rec.nativeReconcilerOpts, reconciler.NativeReconcilerWithScheme(rec.scheme))..., 329 ) 330 331 result, err := r.Reconcile(parent) 332 if err != nil { 333 return result, err 334 } 335 336 if !component.Enabled(parent) { 337 // cleanup orphaned pods left from removed jobs 338 if err := rec.client.DeleteAllOf(context.TODO(), &v1.Pod{}, 339 client.MatchingLabels{"release": releaseData.ReleaseName}, 340 client.HasLabels{"job-name"}, 341 client.InNamespace(releaseData.Namespace), 342 ); err != nil { 343 return result, errors.WrapIf(err, "failed to remove pods left from the release") 344 } 345 } 346 347 rec.logger.Info("reconciled") 348 349 return result, nil 350 } 351 352 func (rec *HelmReconciler) setDesiredStateOverrides(resourceBuilders []reconciler.ResourceBuilder, releaseData *ReleaseData) []reconciler.ResourceBuilder { 353 resources := []reconciler.ResourceBuilder{} 354 355 for _, rb := range resourceBuilders { 356 rb := rb 357 resources = append(resources, func() (runtime.Object, reconciler.DesiredState, error) { 358 o, state, err := rb() 359 if err != nil { 360 return nil, nil, err 361 } 362 363 om, err := meta.Accessor(o) 364 if err != nil { 365 return nil, nil, err 366 } 367 368 var overrideState reconciler.DesiredState 369 370 gvk := o.GetObjectKind().GroupVersionKind() 371 372 if ds, ok := releaseData.DesiredStateOverrides[reconciler.ObjectKeyWithGVK{ 373 GVK: gvk, 374 }]; ok { 375 rec.logger.V(2).Info("override object desired state by gvk", "gvk", gvk.String(), "namespace", om.GetNamespace(), "name", om.GetName()) 376 overrideState = ds 377 } 378 379 if ds, ok := releaseData.DesiredStateOverrides[reconciler.ObjectKeyWithGVK{ 380 ObjectKey: client.ObjectKey{ 381 Name: om.GetName(), 382 Namespace: om.GetNamespace(), 383 }, 384 GVK: gvk, 385 }]; ok { 386 rec.logger.V(2).Info("override object desired state by gvk and object key", "gvk", gvk.String(), "namespace", om.GetNamespace(), "name", om.GetName()) 387 overrideState = ds 388 } 389 390 if overrideState != nil { 391 state = reconciler.MultipleDesiredStates{ 392 state, overrideState, 393 } 394 } 395 396 return o, state, nil 397 }) 398 } 399 400 return resources 401 } 402 403 func (rec HelmReconciler) RegisterWatches(_ *controllerruntime.Builder) {}