mirror of https://github.com/knative/client.git
269 lines
7.7 KiB
Go
269 lines
7.7 KiB
Go
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{})
|
|
}
|