package v1 import ( "context" "strings" "time" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/jsonmergepatch" servingv1 "knative.dev/serving/pkg/apis/serving/v1" "knative.dev/client/pkg/util" ) // Copyright © 2020 The Knative Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Helper methods supporting Apply() // patch performs a 3-way merge and returns whether the original service has been changed // This method uses a simple JSON 3-way merge which has some severe limitations, like that arrays // can't be merged. Ideally a strategicpatch merge should be used, which allows a more fine grained // way for performing the merge (but this is not supported for custom resources) // See issue https://github.com/knative/client/issues/1073 for more details how this method should be // improved for a better merge strategy. func (cl *knServingClient) patch(modifiedService *servingv1.Service, currentService *servingv1.Service, uOriginalService []byte) (bool, error) { uModifiedService, err := getModifiedConfiguration(modifiedService, true) if err != nil { return false, err } hasChanged, err := cl.patchSimple(currentService, uModifiedService, uOriginalService) for i := 1; i <= 5 && apierrors.IsConflict(err); i++ { if i > 1 { time.Sleep(1 * time.Second) } currentService, err = cl.GetService(currentService.Name) if err != nil { return false, err } hasChanged, err = cl.patchSimple(currentService, uModifiedService, uOriginalService) } return hasChanged, err } func (cl *knServingClient) patchSimple(currentService *servingv1.Service, uModifiedService []byte, uOriginalService []byte) (bool, error) { // Serialize the current configuration of the object from the server. uCurrentService, err := encodeService(currentService) if err != nil { return false, err } patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(uOriginalService, uModifiedService, uCurrentService) if err != nil { return false, err } if string(patch) == "{}" { return false, nil } // Check if the generation has been counted up, only then the backend detected a change savedService, err := cl.patchService(currentService.Name, types.MergePatchType, patch) if err != nil { return false, err } return savedService.Generation != savedService.Status.ObservedGeneration, nil } // patchService patches the given service func (cl *knServingClient) patchService(name string, patchType types.PatchType, patch []byte) (*servingv1.Service, error) { service, err := cl.client.Services(cl.namespace).Patch(context.TODO(), name, patchType, patch, metav1.PatchOptions{}) if err != nil { return nil, err } err = updateServingGvk(service) return service, err } func getOriginalConfiguration(service *servingv1.Service) []byte { annots := service.Annotations if annots == nil { return nil } original, ok := annots[v1.LastAppliedConfigAnnotation] if !ok { return nil } return []byte(original) } func getModifiedConfiguration(service *servingv1.Service, annotate bool) ([]byte, error) { // First serialize the object without the annotation to prevent recursion, // then add that serialization to it as the annotation and serialize it again. var uModifiedService []byte // Otherwise, use the server side version of the object. // Get the current annotations from the object. annots := service.Annotations if annots == nil { annots = map[string]string{} } original := annots[v1.LastAppliedConfigAnnotation] delete(annots, v1.LastAppliedConfigAnnotation) service.Annotations = annots uModifiedService, err := encodeService(service) if err != nil { return nil, err } if annotate { annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(uModifiedService), "\n") service.Annotations = annots uModifiedService, err = encodeService(service) if err != nil { return nil, err } } // Restore the object to its original condition. annots[v1.LastAppliedConfigAnnotation] = original service.Annotations = annots return uModifiedService, nil } func updateLastAppliedAnnotation(service *servingv1.Service) error { annots := service.Annotations if annots == nil { annots = map[string]string{} } lastApplied, err := encodeService(service) if err != nil { return err } // Cleanup any trailing newlines annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(lastApplied), "\n") service.Annotations = annots return nil } func encodeService(service *servingv1.Service) ([]byte, error) { scheme := runtime.NewScheme() err := servingv1.AddToScheme(scheme) if err != nil { return nil, err } factory := serializer.NewCodecFactory(scheme) encoder := factory.EncoderForVersion(unstructured.UnstructuredJSONScheme, servingv1.SchemeGroupVersion) err = util.UpdateGroupVersionKindWithScheme(service, servingv1.SchemeGroupVersion, scheme) if err != nil { return nil, err } serviceUnstructured, err := util.ToUnstructured(service) if err != nil { return nil, err } // Remove/adapt service so that it can be used in the apply-annotation cleanupServiceUnstructured(serviceUnstructured) return runtime.Encode(encoder, serviceUnstructured) } func cleanupServiceUnstructured(uService *unstructured.Unstructured) { clearCreationTimestamps(uService.Object) removeStatus(uService.Object) removeContainerNameAndResourcesIfNotSet(uService.Object) } func removeContainerNameAndResourcesIfNotSet(uService map[string]interface{}) { uContainer := extractUserContainer(uService) if uContainer == nil { return } name, ok := uContainer["name"] if ok && name != "" { delete(uContainer, "name") } resources := uContainer["resources"] if resources == nil { return } resourcesMap := resources.(map[string]interface{}) if len(resourcesMap) == 0 { delete(uContainer, "resources") } } func extractUserContainer(uService map[string]interface{}) map[string]interface{} { tSpec := extractTemplateSpec(uService) if tSpec == nil { return nil } containers := tSpec["containers"] if len(containers.([]interface{})) == 0 { return nil } return containers.([]interface{})[0].(map[string]interface{}) } func removeStatus(uService map[string]interface{}) { delete(uService, "status") } func clearCreationTimestamps(uService map[string]interface{}) { meta := uService["metadata"] if meta != nil { delete(meta.(map[string]interface{}), "creationTimestamp") } template := extractTemplate(uService) if template != nil { meta = template["metadata"] if meta != nil { delete(meta.(map[string]interface{}), "creationTimestamp") } } } func extractTemplateSpec(uService map[string]interface{}) map[string]interface{} { templ := extractTemplate(uService) if templ == nil { return nil } templSpec := templ["spec"] if templSpec == nil { return nil } return templSpec.(map[string]interface{}) } func extractTemplate(uService map[string]interface{}) map[string]interface{} { spec := uService["spec"] if spec == nil { return nil } templ := spec.(map[string]interface{})["template"] if templ == nil { return nil } return templ.(map[string]interface{}) }