diff --git a/go.mod b/go.mod index fbfc6482f4..a4ba9d075f 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/openshift/api v0.0.0-20211209173311-a19f3b9052a6 github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3 github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3 - github.com/openshift/library-go v0.0.0-20211214141842-7bcd82848eb9 + github.com/openshift/library-go v0.0.0-20211217155025-d48a1fb9b7c2 github.com/pkg/profile v1.5.0 // indirect github.com/prometheus-operator/prometheus-operator/pkg/client v0.45.0 github.com/prometheus/client_golang v1.11.0 diff --git a/go.sum b/go.sum index 8474990447..b91fcfc206 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,7 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2/go.mod h1:XyjUkMA8GN+tOOPXvnbi3XuRxWFvTJntqvTFnjmhzbk= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -535,8 +536,8 @@ github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3 h1:65 github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3 h1:SG1aqwleU6bGD0X4mhkTNupjVnByMYYuW4XbnCPavQU= github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3/go.mod h1:cwhyki5lqBmrT0m8Im+9I7PGFaraOzcYPtEz93RcsGY= -github.com/openshift/library-go v0.0.0-20211214141842-7bcd82848eb9 h1:jw/1BmTcEaqC9HMEAUzHA0S78g1M8Dyi0qh+Sxgg1DM= -github.com/openshift/library-go v0.0.0-20211214141842-7bcd82848eb9/go.mod h1:M/Gi/GUUrMdSS07nrYtTiK43J6/VUAyk/+IfN4ZqUY4= +github.com/openshift/library-go v0.0.0-20211217155025-d48a1fb9b7c2 h1:XIc3BF59OalAmDZ2YTYiUvRo1LMAGoZbdK01VBPduXU= +github.com/openshift/library-go v0.0.0-20211217155025-d48a1fb9b7c2/go.mod h1:hz4rpghzCaE+vejl9v04JdmrujaZA8BBpajosXfIGl8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/core.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/core.go index 318d2dc06f..88c252036e 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/core.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/core.go @@ -3,479 +3,105 @@ package resourceapply import ( "bytes" "context" - "crypto/md5" - "errors" "fmt" - "io" "sort" "strings" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/klog/v2" - - "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" ) -type CachedVersionKey struct { - name, kind, namespace string -} - -// record of resource metadata used to determine if its safe to return early from an ApplyFoo -// resourceHash is an ms5 hash of the required in an ApplyFoo that is computed in case the input changes -// resourceVersion is the received resourceVersion from the apiserver in response to an update that is comparable to the GET -type CachedResource struct { - resourceHash, resourceVersion string -} - -func getResourceMetadata(obj runtime.Object) (string, string, string, string, error) { - var name, kind, namespace, resourceHash string - switch r := obj.(type) { +// TODO find way to create a registry of these based on struct mapping or some such that forces users to get this right +// for creating an ApplyGeneric +// Perhaps a struct containing the apply function and the getKind +func getCoreGroupKind(obj runtime.Object) *schema.GroupKind { + switch obj.(type) { case *corev1.Namespace: - name = r.Name - kind = r.Kind - case *corev1.Service: - name = r.Name - kind = r.Kind - namespace = r.Namespace - case *corev1.Pod: - name = r.Name - kind = r.Kind - namespace = r.Namespace - case *corev1.ServiceAccount: - name = r.Name - kind = r.Kind - namespace = r.Namespace - case *corev1.ConfigMap: - name = r.Name - kind = r.Kind - namespace = r.Namespace - case *corev1.Secret: - name = r.Name - kind = r.Kind - namespace = r.Namespace - default: - return name, kind, namespace, resourceHash, errors.New("attempted to access metadata for unsupported type") - } - resourceHash = hashOfResourceStruct(obj) - return name, kind, namespace, resourceHash, nil -} - -func getResourceVersion(obj runtime.Object) (string, error) { - var resourceVersion string - switch r := obj.(type) { - case *corev1.Namespace: - resourceVersion = r.ResourceVersion + return &schema.GroupKind{ + Kind: "Namespace", + } case *corev1.Service: - resourceVersion = r.ResourceVersion + return &schema.GroupKind{ + Kind: "Service", + } case *corev1.Pod: - resourceVersion = r.ResourceVersion + return &schema.GroupKind{ + Kind: "Pod", + } case *corev1.ServiceAccount: - resourceVersion = r.ResourceVersion + return &schema.GroupKind{ + Kind: "ServiceAccount", + } case *corev1.ConfigMap: - resourceVersion = r.ResourceVersion + return &schema.GroupKind{ + Kind: "ConfigMap", + } case *corev1.Secret: - resourceVersion = r.ResourceVersion - default: - return resourceVersion, errors.New("attempted to access metadata for unsupported type") - } - return resourceVersion, nil -} - -func updateCachedResourceMetadata(cache map[CachedVersionKey]CachedResource, required runtime.Object, actual runtime.Object) { - if cache == nil || required == nil || actual == nil { - return - } - name, kind, namespace, resourceHash, err := getResourceMetadata(required) - if err != nil { - return - } - - resourceVersion, err := getResourceVersion(actual) - if err != nil { - return - } - if resourceVersion == "" { - klog.V(4).Infof("resourceVersion has empty string %s:%s:%s %s", name, kind, namespace, resourceVersion) - return - } - - cache[CachedVersionKey{name, kind, namespace}] = CachedResource{resourceHash, resourceVersion} - klog.V(7).Infof("updated resourceVersion of %s:%s:%s %s", name, kind, namespace, resourceVersion) - -} - -// in the circumstance that an ApplyFoo's 'required' is the same one which was previously -// applied for a given (name, kind, namespace) and the existing resource (if any), -// hasn't been modified since the ApplyFoo last updated that resource, then return true (we don't -// need to reapply the resource). Otherwise return false. -func safeToSkipApply(cache map[CachedVersionKey]CachedResource, required runtime.Object, existing runtime.Object) bool { - if cache == nil || required == nil || existing == nil { - return false - } - name, kind, namespace, resourceHash, err := getResourceMetadata(required) - if err != nil { - return false - } - - resourceVersion, err := getResourceVersion(existing) - if err != nil { - return false - } - - var versionMatch, hashMatch bool - if cached, exists := cache[CachedVersionKey{name, kind, namespace}]; exists { - versionMatch = cached.resourceVersion == resourceVersion - hashMatch = cached.resourceHash == resourceHash - if versionMatch && hashMatch { - klog.V(4).Infof("found matching resourceVersion & manifest hash") - return true + return &schema.GroupKind{ + Kind: "Secret", } + default: + return nil } - - return false -} - -// detect changes in a resource by caching a hash of the string representation of the resource -// note: some changes in a resource e.g. nil vs empty, will not be detected this way -func hashOfResourceStruct(o interface{}) string { - oString := fmt.Sprintf("%v", o) - h := md5.New() - io.WriteString(h, oString) - rval := fmt.Sprintf("%x", h.Sum(nil)) - return rval } // ApplyNamespace merges objectmeta, does not worry about anything else func ApplyNamespace(ctx context.Context, client coreclientv1.NamespacesGetter, recorder events.Recorder, required *corev1.Namespace) (*corev1.Namespace, bool, error) { - existing, err := client.Namespaces().Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.Namespaces(). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Namespace), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - modified := resourcemerge.BoolPtr(false) - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - if !*modified { - return existingCopy, false, nil - } - - if klog.V(4).Enabled() { - klog.Infof("Namespace %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy)) - } - - actual, err := client.Namespaces().Update(ctx, existingCopy, metav1.UpdateOptions{}) - reportUpdateEvent(recorder, required, err) - return actual, true, err + return ApplyNamespaceImproved(ctx, client, recorder, required, noCache) } // ApplyService merges objectmeta and requires // TODO, since this cannot determine whether changes are due to legitimate actors (api server) or illegitimate ones (users), we cannot update // TODO I've special cased the selector for now func ApplyService(ctx context.Context, client coreclientv1.ServicesGetter, recorder events.Recorder, required *corev1.Service) (*corev1.Service, bool, error) { - existing, err := client.Services(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.Services(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Service), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - modified := resourcemerge.BoolPtr(false) - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - selectorSame := equality.Semantic.DeepEqual(existingCopy.Spec.Selector, required.Spec.Selector) - - typeSame := false - requiredIsEmpty := len(required.Spec.Type) == 0 - existingCopyIsCluster := existingCopy.Spec.Type == corev1.ServiceTypeClusterIP - if (requiredIsEmpty && existingCopyIsCluster) || equality.Semantic.DeepEqual(existingCopy.Spec.Type, required.Spec.Type) { - typeSame = true - } - - if selectorSame && typeSame && !*modified { - return existingCopy, false, nil - } - - existingCopy.Spec.Selector = required.Spec.Selector - existingCopy.Spec.Type = required.Spec.Type // if this is different, the update will fail. Status will indicate it. - - if klog.V(4).Enabled() { - klog.Infof("Service %q changes: %v", required.Namespace+"/"+required.Name, JSONPatchNoError(existing, required)) - } - - actual, err := client.Services(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) - reportUpdateEvent(recorder, required, err) - return actual, true, err + return ApplyServiceImproved(ctx, client, recorder, required, noCache) } // ApplyPod merges objectmeta, does not worry about anything else func ApplyPod(ctx context.Context, client coreclientv1.PodsGetter, recorder events.Recorder, required *corev1.Pod) (*corev1.Pod, bool, error) { - existing, err := client.Pods(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.Pods(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Pod), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - modified := resourcemerge.BoolPtr(false) - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - if !*modified { - return existingCopy, false, nil - } - - if klog.V(4).Enabled() { - klog.Infof("Pod %q changes: %v", required.Namespace+"/"+required.Name, JSONPatchNoError(existing, required)) - } - - actual, err := client.Pods(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) - reportUpdateEvent(recorder, required, err) - return actual, true, err + return ApplyPodImproved(ctx, client, recorder, required, noCache) } // ApplyServiceAccount merges objectmeta, does not worry about anything else func ApplyServiceAccount(ctx context.Context, client coreclientv1.ServiceAccountsGetter, recorder events.Recorder, required *corev1.ServiceAccount) (*corev1.ServiceAccount, bool, error) { - existing, err := client.ServiceAccounts(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.ServiceAccounts(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.ServiceAccount), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - modified := resourcemerge.BoolPtr(false) - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - if !*modified { - return existingCopy, false, nil - } - if klog.V(4).Enabled() { - klog.Infof("ServiceAccount %q changes: %v", required.Namespace+"/"+required.Name, JSONPatchNoError(existing, required)) - } - actual, err := client.ServiceAccounts(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) - reportUpdateEvent(recorder, required, err) - return actual, true, err + return ApplyServiceAccountImproved(ctx, client, recorder, required, noCache) } // ApplyConfigMap merges objectmeta, requires data func ApplyConfigMap(ctx context.Context, client coreclientv1.ConfigMapsGetter, recorder events.Recorder, required *corev1.ConfigMap) (*corev1.ConfigMap, bool, error) { - existing, err := client.ConfigMaps(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.ConfigMaps(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.ConfigMap), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - modified := resourcemerge.BoolPtr(false) - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - - caBundleInjected := required.Labels["config.openshift.io/inject-trusted-cabundle"] == "true" - _, newCABundleRequired := required.Data["ca-bundle.crt"] - - var modifiedKeys []string - for existingCopyKey, existingCopyValue := range existingCopy.Data { - // if we're injecting a ca-bundle and the required isn't forcing the value, then don't use the value of existing - // to drive a diff detection. If required has set the value then we need to force the value in order to have apply - // behave predictably. - if caBundleInjected && !newCABundleRequired && existingCopyKey == "ca-bundle.crt" { - continue - } - if requiredValue, ok := required.Data[existingCopyKey]; !ok || (existingCopyValue != requiredValue) { - modifiedKeys = append(modifiedKeys, "data."+existingCopyKey) - } - } - for existingCopyKey, existingCopyBinValue := range existingCopy.BinaryData { - if requiredBinValue, ok := required.BinaryData[existingCopyKey]; !ok || !bytes.Equal(existingCopyBinValue, requiredBinValue) { - modifiedKeys = append(modifiedKeys, "binaryData."+existingCopyKey) - } - } - for requiredKey := range required.Data { - if _, ok := existingCopy.Data[requiredKey]; !ok { - modifiedKeys = append(modifiedKeys, "data."+requiredKey) - } - } - for requiredBinKey := range required.BinaryData { - if _, ok := existingCopy.BinaryData[requiredBinKey]; !ok { - modifiedKeys = append(modifiedKeys, "binaryData."+requiredBinKey) - } - } - - dataSame := len(modifiedKeys) == 0 - if dataSame && !*modified { - return existingCopy, false, nil - } - existingCopy.Data = required.Data - existingCopy.BinaryData = required.BinaryData - // if we're injecting a cabundle, and we had a previous value, and the required object isn't setting the value, then set back to the previous - if existingCABundle, existedBefore := existing.Data["ca-bundle.crt"]; caBundleInjected && existedBefore && !newCABundleRequired { - if existingCopy.Data == nil { - existingCopy.Data = map[string]string{} - } - existingCopy.Data["ca-bundle.crt"] = existingCABundle - } - - actual, err := client.ConfigMaps(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) - - var details string - if !dataSame { - sort.Sort(sort.StringSlice(modifiedKeys)) - details = fmt.Sprintf("cause by changes in %v", strings.Join(modifiedKeys, ",")) - } - if klog.V(4).Enabled() { - klog.Infof("ConfigMap %q changes: %v", required.Namespace+"/"+required.Name, JSONPatchNoError(existing, required)) - } - reportUpdateEvent(recorder, required, err, details) - return actual, true, err + return ApplyConfigMapImproved(ctx, client, recorder, required, noCache) } // ApplySecret merges objectmeta, requires data -func ApplySecret(ctx context.Context, client coreclientv1.SecretsGetter, recorder events.Recorder, requiredInput *corev1.Secret) (*corev1.Secret, bool, error) { - // copy the stringData to data. Error on a data content conflict inside required. This is usually a bug. - required := requiredInput.DeepCopy() - if required.Data == nil { - required.Data = map[string][]byte{} - } - for k, v := range required.StringData { - if dataV, ok := required.Data[k]; ok { - if string(dataV) != v { - return nil, false, fmt.Errorf("Secret.stringData[%q] conflicts with Secret.data[%q]", k, k) - } - } - required.Data[k] = []byte(v) - } - required.StringData = nil - - existing, err := client.Secrets(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := required.DeepCopy() - actual, err := client.Secrets(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Secret), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - return actual, true, err - } - if err != nil { - return nil, false, err - } - - existingCopy := existing.DeepCopy() - - resourcemerge.EnsureObjectMeta(resourcemerge.BoolPtr(false), &existingCopy.ObjectMeta, required.ObjectMeta) - - switch required.Type { - case corev1.SecretTypeServiceAccountToken: - // Secrets for ServiceAccountTokens will have data injected by kube controller manager. - // We will apply only the explicitly set keys. - if existingCopy.Data == nil { - existingCopy.Data = map[string][]byte{} - } - - for k, v := range required.Data { - existingCopy.Data[k] = v - } - - default: - existingCopy.Data = required.Data - } - - existingCopy.Type = required.Type - - // Server defaults some values and we need to do it as well or it will never equal. - if existingCopy.Type == "" { - existingCopy.Type = corev1.SecretTypeOpaque - } - - if equality.Semantic.DeepEqual(existingCopy, existing) { - return existing, false, nil - } - - if klog.V(4).Enabled() { - klog.Infof("Secret %s/%s changes: %v", required.Namespace, required.Name, JSONPatchSecretNoError(existing, existingCopy)) - } - - var actual *corev1.Secret - /* - * Kubernetes validation silently hides failures to update secret type. - * https://github.com/kubernetes/kubernetes/blob/98e65951dccfd40d3b4f31949c2ab8df5912d93e/pkg/apis/core/validation/validation.go#L5048 - * We need to explicitly opt for delete+create in that case. - */ - if existingCopy.Type == existing.Type { - actual, err = client.Secrets(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) - reportUpdateEvent(recorder, existingCopy, err) - - if err == nil { - return actual, true, err - } - if !strings.Contains(err.Error(), "field is immutable") { - return actual, true, err - } - } - - // if the field was immutable on a secret, we're going to be stuck until we delete it. Try to delete and then create - deleteErr := client.Secrets(required.Namespace).Delete(ctx, existingCopy.Name, metav1.DeleteOptions{}) - reportDeleteEvent(recorder, existingCopy, deleteErr) - - // clear the RV and track the original actual and error for the return like our create value. - existingCopy.ResourceVersion = "" - actual, err = client.Secrets(required.Namespace).Create(ctx, existingCopy, metav1.CreateOptions{}) - reportCreateEvent(recorder, existingCopy, err) - - return actual, true, err +func ApplySecret(ctx context.Context, client coreclientv1.SecretsGetter, recorder events.Recorder, required *corev1.Secret) (*corev1.Secret, bool, error) { + return ApplySecretImproved(ctx, client, recorder, required, noCache) } // ApplyNamespace merges objectmeta, does not worry about anything else -func ApplyNamespaceImproved(ctx context.Context, client coreclientv1.NamespacesGetter, recorder events.Recorder, required *corev1.Namespace, cache map[CachedVersionKey]CachedResource) (*corev1.Namespace, bool, error) { +func ApplyNamespaceImproved(ctx context.Context, client coreclientv1.NamespacesGetter, recorder events.Recorder, required *corev1.Namespace, cache ResourceCache) (*corev1.Namespace, bool, error) { existing, err := client.Namespaces().Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.Namespaces(). Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Namespace), metav1.CreateOptions{}) reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } - if safeToSkipApply(cache, required, existing) { + if cache.SafeToSkipApply(required, existing) { return existing, false, nil } @@ -484,7 +110,7 @@ func ApplyNamespaceImproved(ctx context.Context, client coreclientv1.NamespacesG resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) if !*modified { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } @@ -494,28 +120,28 @@ func ApplyNamespaceImproved(ctx context.Context, client coreclientv1.NamespacesG actual, err := client.Namespaces().Update(ctx, existingCopy, metav1.UpdateOptions{}) reportUpdateEvent(recorder, required, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } // ApplyService merges objectmeta and requires // TODO, since this cannot determine whether changes are due to legitimate actors (api server) or illegitimate ones (users), we cannot update // TODO I've special cased the selector for now -func ApplyServiceImproved(ctx context.Context, client coreclientv1.ServicesGetter, recorder events.Recorder, required *corev1.Service, cache map[CachedVersionKey]CachedResource) (*corev1.Service, bool, error) { +func ApplyServiceImproved(ctx context.Context, client coreclientv1.ServicesGetter, recorder events.Recorder, required *corev1.Service, cache ResourceCache) (*corev1.Service, bool, error) { existing, err := client.Services(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.Services(requiredCopy.Namespace). Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Service), metav1.CreateOptions{}) reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } - if safeToSkipApply(cache, required, existing) { + if cache.SafeToSkipApply(required, existing) { return existing, false, nil } @@ -533,7 +159,7 @@ func ApplyServiceImproved(ctx context.Context, client coreclientv1.ServicesGette } if selectorSame && typeSame && !*modified { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } @@ -546,26 +172,26 @@ func ApplyServiceImproved(ctx context.Context, client coreclientv1.ServicesGette actual, err := client.Services(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) reportUpdateEvent(recorder, required, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } // ApplyPod merges objectmeta, does not worry about anything else -func ApplyPodImproved(ctx context.Context, client coreclientv1.PodsGetter, recorder events.Recorder, required *corev1.Pod, cache map[CachedVersionKey]CachedResource) (*corev1.Pod, bool, error) { +func ApplyPodImproved(ctx context.Context, client coreclientv1.PodsGetter, recorder events.Recorder, required *corev1.Pod, cache ResourceCache) (*corev1.Pod, bool, error) { existing, err := client.Pods(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.Pods(requiredCopy.Namespace). Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Pod), metav1.CreateOptions{}) reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } - if safeToSkipApply(cache, required, existing) { + if cache.SafeToSkipApply(required, existing) { return existing, false, nil } @@ -574,7 +200,7 @@ func ApplyPodImproved(ctx context.Context, client coreclientv1.PodsGetter, recor resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) if !*modified { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } @@ -584,26 +210,26 @@ func ApplyPodImproved(ctx context.Context, client coreclientv1.PodsGetter, recor actual, err := client.Pods(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) reportUpdateEvent(recorder, required, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } // ApplyServiceAccount merges objectmeta, does not worry about anything else -func ApplyServiceAccountImproved(ctx context.Context, client coreclientv1.ServiceAccountsGetter, recorder events.Recorder, required *corev1.ServiceAccount, cache map[CachedVersionKey]CachedResource) (*corev1.ServiceAccount, bool, error) { +func ApplyServiceAccountImproved(ctx context.Context, client coreclientv1.ServiceAccountsGetter, recorder events.Recorder, required *corev1.ServiceAccount, cache ResourceCache) (*corev1.ServiceAccount, bool, error) { existing, err := client.ServiceAccounts(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.ServiceAccounts(requiredCopy.Namespace). Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.ServiceAccount), metav1.CreateOptions{}) reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } - if safeToSkipApply(cache, required, existing) { + if cache.SafeToSkipApply(required, existing) { return existing, false, nil } @@ -612,7 +238,7 @@ func ApplyServiceAccountImproved(ctx context.Context, client coreclientv1.Servic resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) if !*modified { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } if klog.V(4).Enabled() { @@ -620,26 +246,26 @@ func ApplyServiceAccountImproved(ctx context.Context, client coreclientv1.Servic } actual, err := client.ServiceAccounts(required.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) reportUpdateEvent(recorder, required, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } // ApplyConfigMap merges objectmeta, requires data -func ApplyConfigMapImproved(ctx context.Context, client coreclientv1.ConfigMapsGetter, recorder events.Recorder, required *corev1.ConfigMap, cache map[CachedVersionKey]CachedResource) (*corev1.ConfigMap, bool, error) { +func ApplyConfigMapImproved(ctx context.Context, client coreclientv1.ConfigMapsGetter, recorder events.Recorder, required *corev1.ConfigMap, cache ResourceCache) (*corev1.ConfigMap, bool, error) { existing, err := client.ConfigMaps(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.ConfigMaps(requiredCopy.Namespace). Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.ConfigMap), metav1.CreateOptions{}) reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } - if safeToSkipApply(cache, required, existing) { + if cache.SafeToSkipApply(required, existing) { return existing, false, nil } @@ -681,7 +307,7 @@ func ApplyConfigMapImproved(ctx context.Context, client coreclientv1.ConfigMapsG dataSame := len(modifiedKeys) == 0 if dataSame && !*modified { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } existingCopy.Data = required.Data @@ -705,30 +331,25 @@ func ApplyConfigMapImproved(ctx context.Context, client coreclientv1.ConfigMapsG klog.Infof("ConfigMap %q changes: %v", required.Namespace+"/"+required.Name, JSONPatchNoError(existing, required)) } reportUpdateEvent(recorder, required, err, details) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } // ApplySecret merges objectmeta, requires data -func ApplySecretImproved(ctx context.Context, client coreclientv1.SecretsGetter, recorder events.Recorder, requiredInput *corev1.Secret, cache map[CachedVersionKey]CachedResource) (*corev1.Secret, bool, error) { - existing, err := client.Secrets(requiredInput.Namespace).Get(ctx, requiredInput.Name, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - requiredCopy := requiredInput.DeepCopy() - actual, err := client.Secrets(requiredCopy.Namespace). - Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Secret), metav1.CreateOptions{}) - reportCreateEvent(recorder, requiredCopy, err) - updateCachedResourceMetadata(cache, requiredInput, actual) - return actual, true, err - } - if err != nil { - return nil, false, err - } +func ApplySecretImproved(ctx context.Context, client coreclientv1.SecretsGetter, recorder events.Recorder, requiredInput *corev1.Secret, cache ResourceCache) (*corev1.Secret, bool, error) { + // copy the stringData to data. Error on a data content conflict inside required. This is usually a bug. - if safeToSkipApply(cache, requiredInput, existing) { + existing, err := client.Secrets(requiredInput.Namespace).Get(ctx, requiredInput.Name, metav1.GetOptions{}) + switch{ + case !apierrors.IsNotFound(err): + // do nothing and fall through to create + case err != nil: + return nil, false, err + case cache.SafeToSkipApply(requiredInput, existing): return existing, false, nil + } - // copy the stringData to data. Error on a data content conflict inside required. This is usually a bug. required := requiredInput.DeepCopy() if required.Data == nil { required.Data = map[string][]byte{} @@ -743,6 +364,18 @@ func ApplySecretImproved(ctx context.Context, client coreclientv1.SecretsGetter, } required.StringData = nil + if apierrors.IsNotFound(err) { + requiredCopy := required.DeepCopy() + actual, err := client.Secrets(requiredCopy.Namespace). + Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.Secret), metav1.CreateOptions{}) + reportCreateEvent(recorder, requiredCopy, err) + cache.UpdateCachedResourceMetadata(requiredInput, actual) + return actual, true, err + } + if err != nil { + return nil, false, err + } + existingCopy := existing.DeepCopy() resourcemerge.EnsureObjectMeta(resourcemerge.BoolPtr(false), &existingCopy.ObjectMeta, required.ObjectMeta) @@ -771,7 +404,7 @@ func ApplySecretImproved(ctx context.Context, client coreclientv1.SecretsGetter, } if equality.Semantic.DeepEqual(existingCopy, existing) { - updateCachedResourceMetadata(cache, required, existingCopy) + cache.UpdateCachedResourceMetadata(requiredInput, existingCopy) return existing, false, nil } @@ -805,7 +438,7 @@ func ApplySecretImproved(ctx context.Context, client coreclientv1.SecretsGetter, existingCopy.ResourceVersion = "" actual, err = client.Secrets(required.Namespace).Create(ctx, existingCopy, metav1.CreateOptions{}) reportCreateEvent(recorder, existingCopy, err) - updateCachedResourceMetadata(cache, required, actual) + cache.UpdateCachedResourceMetadata(requiredInput, actual) return actual, true, err } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go index 3d5aad37fd..c0a9fc8f4b 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go @@ -101,7 +101,7 @@ func (c *ClientHolder) WithMigrationClient(client migrationclient.Interface) *Cl } // ApplyDirectly applies the given manifest files to API server. -func ApplyDirectly(ctx context.Context, clients *ClientHolder, recorder events.Recorder, cache map[CachedVersionKey]CachedResource, manifests AssetFunc, files ...string) []ApplyResult { +func ApplyDirectly(ctx context.Context, clients *ClientHolder, recorder events.Recorder, cache ResourceCache, manifests AssetFunc, files ...string) []ApplyResult { ret := []ApplyResult{} for _, file := range files { diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/resource_cache.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/resource_cache.go new file mode 100644 index 0000000000..908c4533e9 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/resource_cache.go @@ -0,0 +1,161 @@ +package resourceapply + +import ( + "crypto/md5" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" +) + +type cachedVersionKey struct { + name string + namespace string + kind schema.GroupKind +} + +// record of resource metadata used to determine if its safe to return early from an ApplyFoo +// resourceHash is an ms5 hash of the required in an ApplyFoo that is computed in case the input changes +// resourceVersion is the received resourceVersion from the apiserver in response to an update that is comparable to the GET +type cachedResource struct { + resourceHash, resourceVersion string +} + +type resourceCache struct { + cache map[cachedVersionKey]cachedResource +} + +type ResourceCache interface { + UpdateCachedResourceMetadata(required runtime.Object, actual runtime.Object) + SafeToSkipApply(required runtime.Object, existing runtime.Object) bool +} + +func NewResourceCache() *resourceCache { + return &resourceCache{ + cache: map[cachedVersionKey]cachedResource{}, + } +} + +var noCache *resourceCache + +func getResourceMetadata(obj runtime.Object) (schema.GroupKind, string, string, string, error) { + metadata, err := meta.Accessor(obj) + if err != nil { + return schema.GroupKind{}, "", "", "", err + } + resourceHash := hashOfResourceStruct(obj) + + // retrieve kind, sometimes this can be done via the accesor, sometimes not (depends on the type) + kind := schema.GroupKind{} + gvk := obj.GetObjectKind().GroupVersionKind() + if len(gvk.Kind) > 0 { + kind = gvk.GroupKind() + } else { + if currKind := getCoreGroupKind(obj); currKind != nil { + kind = *currKind + } + } + if len(kind.Kind) == 0 { + return schema.GroupKind{}, "", "", "", fmt.Errorf("unable to determine GroupKind of %T", obj) + } + + return kind, metadata.GetName(), metadata.GetNamespace(), resourceHash, nil +} + +func getResourceVersion(obj runtime.Object) (string, error) { + if obj == nil { + return "", fmt.Errorf("nil object has no resourceVersion") + } + metadata, err := meta.Accessor(obj) + if err != nil { + return "", err + } + if metadata == nil{ + return "", fmt.Errorf("object has no metadata") + } + rv := metadata.GetResourceVersion() + if len(rv) == 0 { + return "", fmt.Errorf("missing resourceVersion") + } + + return rv, nil +} + +func (c *resourceCache) UpdateCachedResourceMetadata(required runtime.Object, actual runtime.Object) { + if c == nil || c.cache == nil { + return + } + if required == nil || actual == nil { + return + } + kind, name, namespace, resourceHash, err := getResourceMetadata(required) + if err != nil { + return + } + cacheKey := cachedVersionKey{ + name: name, + namespace: namespace, + kind: kind, + } + + resourceVersion, err := getResourceVersion(actual) + if err != nil { + klog.V(4).Infof("error reading resourceVersion %s:%s:%s %s", name, kind, namespace, err) + return + } + + c.cache[cacheKey] = cachedResource{resourceHash, resourceVersion} + klog.V(7).Infof("updated resourceVersion of %s:%s:%s %s", name, kind, namespace, resourceVersion) +} + +// in the circumstance that an ApplyFoo's 'required' is the same one which was previously +// applied for a given (name, kind, namespace) and the existing resource (if any), +// hasn't been modified since the ApplyFoo last updated that resource, then return true (we don't +// need to reapply the resource). Otherwise return false. +func (c *resourceCache) SafeToSkipApply(required runtime.Object, existing runtime.Object) bool { + if c == nil || c.cache == nil { + return false + } + if required == nil || existing == nil { + return false + } + kind, name, namespace, resourceHash, err := getResourceMetadata(required) + if err != nil { + return false + } + cacheKey := cachedVersionKey{ + name: name, + namespace: namespace, + kind: kind, + } + + resourceVersion, err := getResourceVersion(existing) + if err != nil { + return false + } + + var versionMatch, hashMatch bool + if cached, exists := c.cache[cacheKey]; exists { + versionMatch = cached.resourceVersion == resourceVersion + hashMatch = cached.resourceHash == resourceHash + if versionMatch && hashMatch { + klog.V(4).Infof("found matching resourceVersion & manifest hash") + return true + } + } + + return false +} + +// detect changes in a resource by caching a hash of the string representation of the resource +// note: some changes in a resource e.g. nil vs empty, will not be detected this way +func hashOfResourceStruct(o interface{}) string { + oString := fmt.Sprintf("%v", o) + h := md5.New() + io.WriteString(h, oString) + rval := fmt.Sprintf("%x", h.Sum(nil)) + return rval +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/bindata/bindata.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/bindata/bindata.go new file mode 100644 index 0000000000..b346d1ec9a --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/bindata/bindata.go @@ -0,0 +1,313 @@ +// Code generated for package bindata by go-bindata DO NOT EDIT. (@generated) +// sources: +// pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml +// pkg/operator/staticpod/controller/guard/manifests/pdb.yaml +package bindata + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +// Name return file name +func (fi bindataFileInfo) Name() string { + return fi.name +} + +// Size return file size +func (fi bindataFileInfo) Size() int64 { + return fi.size +} + +// Mode return file mode +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} + +// Mode return file modify time +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} + +// IsDir return file whether a directory +func (fi bindataFileInfo) IsDir() bool { + return fi.mode&os.ModeDir != 0 +} + +// Sys return file is sys mode +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _pkgOperatorStaticpodControllerGuardManifestsGuardPodYaml = []byte(`apiVersion: v1 +kind: Pod +metadata: + namespace: # Value set by operator + name: # Value set by operator + labels: + app: guard + ownerReferences: # Value set by operator +spec: + affinity: # Value set by operator + priorityClassName: "system-cluster-critical" + terminationGracePeriodSeconds: 3 + tolerations: + - key: node-role.kubernetes.io/master + effect: NoSchedule + operator: Exists + - key: node.kubernetes.io/not-ready + effect: NoExecute + operator: Exists + - key: node.kubernetes.io/unreachable + effect: NoExecute + operator: Exists + - key: node-role.kubernetes.io/etcd + operator: Exists + effect: NoSchedule + containers: + - name: guard + image: # Value set by operator + imagePullPolicy: IfNotPresent + terminationMessagePolicy: FallbackToLogsOnError + command: + - /bin/bash + args: + - -c + - | + # properly handle TERM and exit as soon as it is signaled + set -euo pipefail + trap 'jobs -p | xargs -r kill; exit 0' TERM + sleep infinity & wait + readinessProbe: + failureThreshold: 3 + httpGet: + host: # Value set by operator + path: healthz + port: # Value set by operator + scheme: HTTPS + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + requests: + cpu: 10m + memory: 5Mi +`) + +func pkgOperatorStaticpodControllerGuardManifestsGuardPodYamlBytes() ([]byte, error) { + return _pkgOperatorStaticpodControllerGuardManifestsGuardPodYaml, nil +} + +func pkgOperatorStaticpodControllerGuardManifestsGuardPodYaml() (*asset, error) { + bytes, err := pkgOperatorStaticpodControllerGuardManifestsGuardPodYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _pkgOperatorStaticpodControllerGuardManifestsPdbYaml = []byte(`apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: # Value set by operator + namespace: # Value set by operator +spec: + minAvailable: 0 # Value set by operator + selector: + matchLabels: + app: guard +`) + +func pkgOperatorStaticpodControllerGuardManifestsPdbYamlBytes() ([]byte, error) { + return _pkgOperatorStaticpodControllerGuardManifestsPdbYaml, nil +} + +func pkgOperatorStaticpodControllerGuardManifestsPdbYaml() (*asset, error) { + bytes, err := pkgOperatorStaticpodControllerGuardManifestsPdbYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "pkg/operator/staticpod/controller/guard/manifests/pdb.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml": pkgOperatorStaticpodControllerGuardManifestsGuardPodYaml, + "pkg/operator/staticpod/controller/guard/manifests/pdb.yaml": pkgOperatorStaticpodControllerGuardManifestsPdbYaml, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "pkg": {nil, map[string]*bintree{ + "operator": {nil, map[string]*bintree{ + "staticpod": {nil, map[string]*bintree{ + "controller": {nil, map[string]*bintree{ + "guard": {nil, map[string]*bintree{ + "manifests": {nil, map[string]*bintree{ + "guard-pod.yaml": {pkgOperatorStaticpodControllerGuardManifestsGuardPodYaml, map[string]*bintree{}}, + "pdb.yaml": {pkgOperatorStaticpodControllerGuardManifestsPdbYaml, map[string]*bintree{}}, + }}, + }}, + }}, + }}, + }}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/guard_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/guard_controller.go new file mode 100644 index 0000000000..ca47c78d81 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/guard_controller.go @@ -0,0 +1,291 @@ +package guard + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strconv" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/informers" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + policyclientv1 "k8s.io/client-go/kubernetes/typed/policy/v1" + corelisterv1 "k8s.io/client-go/listers/core/v1" + policylisterv1 "k8s.io/client-go/listers/policy/v1" + "k8s.io/klog/v2" + + configv1 "github.com/openshift/api/config/v1" + configv1listers "github.com/openshift/client-go/config/listers/config/v1" + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + "github.com/openshift/library-go/pkg/operator/resource/resourceread" + "github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/bindata" + operatorv1helpers "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +// GuardController is a controller that watches amount of static pods on master nodes and +// renders guard pods with a pdb to keep maxUnavailable to be at most 1 +type GuardController struct { + targetNamespace, podResourcePrefix string + operatorName string + readyzPort string + + nodeLister corelisterv1.NodeLister + podLister corelisterv1.PodLister + podGetter corev1client.PodsGetter + pdbGetter policyclientv1.PodDisruptionBudgetsGetter + pdbLister policylisterv1.PodDisruptionBudgetLister + + // installerPodImageFn returns the image name for the installer pod + installerPodImageFn func() string + createConditionalFunc func() (bool, error) +} + +func NewGuardController( + targetNamespace, podResourcePrefix string, + operatorName string, + readyzPort string, + kubeInformersForTargetNamespace informers.SharedInformerFactory, + kubeInformersClusterScoped informers.SharedInformerFactory, + operatorClient operatorv1helpers.StaticPodOperatorClient, + podGetter corev1client.PodsGetter, + pdbGetter policyclientv1.PodDisruptionBudgetsGetter, + eventRecorder events.Recorder, + createConditionalFunc func() (bool, error), +) factory.Controller { + c := &GuardController{ + targetNamespace: targetNamespace, + podResourcePrefix: podResourcePrefix, + operatorName: operatorName, + readyzPort: readyzPort, + nodeLister: kubeInformersClusterScoped.Core().V1().Nodes().Lister(), + podLister: kubeInformersForTargetNamespace.Core().V1().Pods().Lister(), + podGetter: podGetter, + pdbGetter: pdbGetter, + pdbLister: kubeInformersForTargetNamespace.Policy().V1().PodDisruptionBudgets().Lister(), + installerPodImageFn: getInstallerPodImageFromEnv, + createConditionalFunc: createConditionalFunc, + } + + return factory.New().WithInformers( + kubeInformersForTargetNamespace.Core().V1().Pods().Informer(), + kubeInformersClusterScoped.Core().V1().Nodes().Informer(), + ).WithSync(c.sync).WithSyncDegradedOnError(operatorClient).ToController("GuardController", eventRecorder) +} + +func getInstallerPodImageFromEnv() string { + return os.Getenv("OPERATOR_IMAGE") +} + +func getGuardPodName(prefix, nodeName string) string { + return fmt.Sprintf("%s-guard-%s", prefix, nodeName) +} + +func getGuardPDBName(prefix string) string { + return fmt.Sprintf("%s-guard-pdb", prefix) +} + +func nodeConditionFinder(status *corev1.NodeStatus, condType corev1.NodeConditionType) *corev1.NodeCondition { + for i := range status.Conditions { + if status.Conditions[i].Type == condType { + return &status.Conditions[i] + } + } + + return nil +} + +func (c *GuardController) sync(ctx context.Context, syncCtx factory.SyncContext) error { + klog.V(5).Info("Syncing guards") + + if c.createConditionalFunc == nil { + return fmt.Errorf("create conditional not set") + } + + shouldCreate, err := c.createConditionalFunc() + if err != nil { + return fmt.Errorf("create conditional returns an error: %v", err) + } + + errs := []error{} + if !shouldCreate { + pdb := resourceread.ReadPodDisruptionBudgetV1OrDie(bindata.MustAsset(filepath.Join("pkg/operator/staticpod/controller/guard", "manifests/pdb.yaml"))) + pdb.ObjectMeta.Name = getGuardPDBName(c.podResourcePrefix) + pdb.ObjectMeta.Namespace = c.targetNamespace + + // List the pdb from the cache in case it does not exist and there's nothing to delete + // so no Delete request is executed. + pdbs, err := c.pdbLister.PodDisruptionBudgets(c.targetNamespace).List(labels.Everything()) + if err != nil { + klog.Errorf("Unable to list PodDisruptionBudgets: %v", err) + return err + } + + for _, pdbItem := range pdbs { + if pdbItem.Name == pdb.Name { + _, _, err := resourceapply.DeletePodDisruptionBudget(ctx, c.pdbGetter, syncCtx.Recorder(), pdb) + if err != nil { + klog.Errorf("Unable to delete PodDisruptionBudget: %v", err) + errs = append(errs, err) + } + break + } + } + + pods, err := c.podLister.Pods(c.targetNamespace).List(labels.SelectorFromSet(labels.Set{"app": "guard"})) + if err != nil { + errs = append(errs, err) + } else { + for _, pod := range pods { + _, _, err = resourceapply.DeletePod(ctx, c.podGetter, syncCtx.Recorder(), pod) + if err != nil { + klog.Errorf("Unable to delete Pod: %v", err) + errs = append(errs, err) + } + } + } + } else { + selector, err := labels.NewRequirement("node-role.kubernetes.io/master", selection.Equals, []string{""}) + if err != nil { + panic(err) + } + nodes, err := c.nodeLister.List(labels.NewSelector().Add(*selector)) + if err != nil { + return err + } + + pods, err := c.podLister.Pods(c.targetNamespace).List(labels.SelectorFromSet(labels.Set{"app": c.podResourcePrefix})) + if err != nil { + return err + } + + klog.V(5).Infof("Rendering guard pdb") + + pdb := resourceread.ReadPodDisruptionBudgetV1OrDie(bindata.MustAsset(filepath.Join("pkg/operator/staticpod/controller/guard", "manifests/pdb.yaml"))) + pdb.ObjectMeta.Name = getGuardPDBName(c.podResourcePrefix) + pdb.ObjectMeta.Namespace = c.targetNamespace + if len(nodes) > 1 { + minAvailable := intstr.FromInt(len(nodes) - 1) + pdb.Spec.MinAvailable = &minAvailable + } + + // List the pdb from the cache in case it exists and there's nothing to update + // so no Get request is executed. + pdbs, err := c.pdbLister.PodDisruptionBudgets(c.targetNamespace).List(labels.Everything()) + if err != nil { + klog.Errorf("Unable to list PodDisruptionBudgets: %v", err) + return err + } + + for _, pdbItem := range pdbs { + if pdbItem.Name == pdb.Name { + if pdbItem.Spec.MinAvailable != pdb.Spec.MinAvailable { + _, _, err = resourceapply.ApplyPodDisruptionBudget(ctx, c.pdbGetter, syncCtx.Recorder(), pdb) + if err != nil { + klog.Errorf("Unable to apply PodDisruptionBudget changes: %v", err) + return err + } + } + break + } + } + + operands := map[string]*corev1.Pod{} + for _, pod := range pods { + operands[pod.Spec.NodeName] = pod + } + + for _, node := range nodes { + if _, exists := operands[node.Name]; !exists { + // If the operand does not exist and the node is not ready, wait until the node becomes ready + nodeReadyCondition := nodeConditionFinder(&node.Status, corev1.NodeReady) + // If a "Ready" condition is not found, that node should be deemed as not Ready by default. + if nodeReadyCondition == nil || nodeReadyCondition.Status != corev1.ConditionTrue { + klog.Infof("Node %v not ready, skipping reconciling the guard pod", node.Name) + continue + } + + klog.Errorf("Missing operand on node %v", node.Name) + errs = append(errs, fmt.Errorf("Missing operand on node %v", node.Name)) + continue + } + + if operands[node.Name].Status.PodIP == "" { + klog.Errorf("Missing PodIP in operand %v on node %v", operands[node.Name].Name, node.Name) + errs = append(errs, fmt.Errorf("Missing PodIP in operand %v on node %v", operands[node.Name].Name, node.Name)) + continue + } + + klog.V(5).Infof("Rendering guard pod for operand %v on node %v", operands[node.Name].Name, node.Name) + + pod := resourceread.ReadPodV1OrDie(bindata.MustAsset(filepath.Join("pkg/operator/staticpod/controller/guard", "manifests/guard-pod.yaml"))) + + pod.ObjectMeta.Name = getGuardPodName(c.podResourcePrefix, node.Name) + pod.ObjectMeta.Namespace = c.targetNamespace + pod.Spec.NodeName = node.Name + pod.Spec.Containers[0].Image = c.installerPodImageFn() + pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Host = operands[node.Name].Status.PodIP + // The readyz port as string type is expected to be convertible into int!!! + readyzPort, err := strconv.Atoi(c.readyzPort) + if err != nil { + panic(err) + } + pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port = intstr.FromInt(readyzPort) + + actual, err := c.podGetter.Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) + if err == nil { + // Delete the pod so it can be re-created. ApplyPod only updates the metadata part of the manifests, ignores the rest + delete := false + if actual.Spec.Containers[0].Image != pod.Spec.Containers[0].Image { + klog.V(5).Infof("Guard Image changed, deleting %v so the guard can be re-created", pod.Name) + delete = true + } + if actual.Spec.Containers[0].ReadinessProbe.HTTPGet.Host != pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Host { + klog.V(5).Infof("Operand PodIP changed, deleting %v so the guard can be re-created", pod.Name) + delete = true + } + if delete { + _, _, err = resourceapply.DeletePod(ctx, c.podGetter, syncCtx.Recorder(), pod) + if err != nil { + klog.Errorf("Unable to delete Pod for immidiate re-creation: %v", err) + errs = append(errs, fmt.Errorf("Unable to delete Pod for immidiate re-creation: %v", err)) + continue + } + } + } else if !apierrors.IsNotFound(err) { + errs = append(errs, err) + continue + } + + _, _, err = resourceapply.ApplyPod(ctx, c.podGetter, syncCtx.Recorder(), pod) + if err != nil { + klog.Errorf("Unable to apply pod %v changes: %v", pod.Name, err) + errs = append(errs, fmt.Errorf("Unable to apply pod %v changes: %v", pod.Name, err)) + } + } + } + + return utilerrors.NewAggregate(errs) +} + +func IsSNOCheckFnc(infraLister configv1listers.InfrastructureLister) func() (bool, error) { + return func() (bool, error) { + infraData, err := infraLister.Get("cluster") + if err != nil { + return false, fmt.Errorf("Unable to list infrastructures.config.openshift.io/cluster object, unable to determine topology mode") + } + if infraData.Status.ControlPlaneTopology == "" { + return false, fmt.Errorf("ControlPlaneTopology was not set, unable to determine topology mode") + } + + return infraData.Status.ControlPlaneTopology == configv1.SingleReplicaTopologyMode, nil + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controllers.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controllers.go index 112fd0d366..00025e3758 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controllers.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controllers.go @@ -11,6 +11,7 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourceapply" "github.com/openshift/library-go/pkg/operator/revisioncontroller" "github.com/openshift/library-go/pkg/operator/staticpod/controller/backingresource" + "github.com/openshift/library-go/pkg/operator/staticpod/controller/guard" "github.com/openshift/library-go/pkg/operator/staticpod/controller/installer" "github.com/openshift/library-go/pkg/operator/staticpod/controller/installerstate" "github.com/openshift/library-go/pkg/operator/staticpod/controller/node" @@ -62,6 +63,12 @@ type staticPodOperatorControllerBuilder struct { pruneCommand []string // TODO de-dupe this. I think it's actually a directory name staticPodPrefix string + + // guard infomation + operatorName string + operatorNamespace string + readyzPort string + guardCreateConditionalFunc func() (bool, error) } func NewBuilder( @@ -91,6 +98,7 @@ type Builder interface { // the installer pod is created for a revision. WithCustomInstaller(command []string, installerPodMutationFunc installer.InstallerPodMutationFunc) Builder WithPruning(command []string, staticPodPrefix string) Builder + WithPodDisruptionBudgetGuard(operatorNamespace, operatorName, readyzPort string, createConditionalFunc func() (bool, error)) Builder ToControllers() (manager.ControllerManager, error) } @@ -153,6 +161,14 @@ func (b *staticPodOperatorControllerBuilder) WithPruning(command []string, stati return b } +func (b *staticPodOperatorControllerBuilder) WithPodDisruptionBudgetGuard(operatorNamespace, operatorName, readyzPort string, createConditionalFunc func() (bool, error)) Builder { + b.operatorNamespace = operatorNamespace + b.operatorName = operatorName + b.readyzPort = readyzPort + b.guardCreateConditionalFunc = createConditionalFunc + return b +} + func (b *staticPodOperatorControllerBuilder) ToControllers() (manager.ControllerManager, error) { manager := manager.NewControllerManager() @@ -169,6 +185,7 @@ func (b *staticPodOperatorControllerBuilder) ToControllers() (manager.Controller secretClient := v1helpers.CachedSecretGetter(b.kubeClient.CoreV1(), b.kubeInformers) podClient := b.kubeClient.CoreV1() eventsClient := b.kubeClient.CoreV1() + pdbClient := b.kubeClient.PolicyV1() operandInformers := b.kubeInformers.InformersFor(b.operandNamespace) clusterInformers := b.kubeInformers.InformersFor("") @@ -299,5 +316,21 @@ func (b *staticPodOperatorControllerBuilder) ToControllers() (manager.Controller manager.WithController(unsupportedconfigoverridescontroller.NewUnsupportedConfigOverridesController(b.staticPodOperatorClient, eventRecorder), 1) manager.WithController(loglevel.NewClusterOperatorLoggingController(b.staticPodOperatorClient, eventRecorder), 1) + if len(b.operatorNamespace) > 0 && len(b.operatorName) > 0 && len(b.readyzPort) > 0 { + manager.WithController(guard.NewGuardController( + b.operandNamespace, + b.staticPodName, + b.operatorName, + b.readyzPort, + operandInformers, + clusterInformers, + b.staticPodOperatorClient, + podClient, + pdbClient, + eventRecorder, + b.guardCreateConditionalFunc, + ), 1) + } + return manager, errors.NewAggregate(errs) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticresourcecontroller/static_resource_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/staticresourcecontroller/static_resource_controller.go index 0cb06f2160..7c3f3e6919 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticresourcecontroller/static_resource_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticresourcecontroller/static_resource_controller.go @@ -64,7 +64,7 @@ type StaticResourceController struct { factory *factory.Factory restMapper meta.RESTMapper categoryExpander restmapper.CategoryExpander - performanceCache map[resourceapply.CachedVersionKey]resourceapply.CachedResource + performanceCache resourceapply.ResourceCache } type conditionalManifests struct { @@ -102,7 +102,7 @@ func NewStaticResourceController( eventRecorder: eventRecorder.WithComponentSuffix(strings.ToLower(name)), factory: factory.New().WithInformers(operatorClient.Informer()).ResyncEvery(1 * time.Minute), - performanceCache: make(map[resourceapply.CachedVersionKey]resourceapply.CachedResource), + performanceCache: resourceapply.NewResourceCache(), } c.WithConditionalResources(manifests, files, nil, nil) diff --git a/vendor/modules.txt b/vendor/modules.txt index d98f1b7972..09f11202d2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -268,7 +268,7 @@ github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/i github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane/v1alpha1 github.com/openshift/client-go/operatorcontrolplane/listers/operatorcontrolplane/v1alpha1 -# github.com/openshift/library-go v0.0.0-20211214141842-7bcd82848eb9 +# github.com/openshift/library-go v0.0.0-20211217155025-d48a1fb9b7c2 ## explicit; go 1.17 github.com/openshift/library-go/pkg/assets github.com/openshift/library-go/pkg/authorization/hardcodedauthorizer @@ -331,6 +331,8 @@ github.com/openshift/library-go/pkg/operator/staticpod github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod github.com/openshift/library-go/pkg/operator/staticpod/controller/backingresource github.com/openshift/library-go/pkg/operator/staticpod/controller/backingresource/bindata +github.com/openshift/library-go/pkg/operator/staticpod/controller/guard +github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/bindata github.com/openshift/library-go/pkg/operator/staticpod/controller/installer github.com/openshift/library-go/pkg/operator/staticpod/controller/installer/bindata github.com/openshift/library-go/pkg/operator/staticpod/controller/installerstate