2020-08-10 17:43:49 +00:00
/ *
Copyright 2020 The Kubernetes 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 .
* /
package fieldmanager
import (
"encoding/json"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
type lastAppliedManager struct {
fieldManager Manager
2020-12-01 01:06:26 +00:00
typeConverter TypeConverter
2020-08-10 17:43:49 +00:00
objectConverter runtime . ObjectConvertor
groupVersion schema . GroupVersion
}
var _ Manager = & lastAppliedManager { }
// NewLastAppliedManager converts the client-side apply annotation to
// server-side apply managed fields
2020-12-01 01:06:26 +00:00
func NewLastAppliedManager ( fieldManager Manager , typeConverter TypeConverter , objectConverter runtime . ObjectConvertor , groupVersion schema . GroupVersion ) Manager {
2020-08-10 17:43:49 +00:00
return & lastAppliedManager {
fieldManager : fieldManager ,
typeConverter : typeConverter ,
objectConverter : objectConverter ,
groupVersion : groupVersion ,
}
}
// Update implements Manager.
func ( f * lastAppliedManager ) Update ( liveObj , newObj runtime . Object , managed Managed , manager string ) ( runtime . Object , Managed , error ) {
return f . fieldManager . Update ( liveObj , newObj , managed , manager )
}
// Apply will consider the last-applied annotation
// for upgrading an object managed by client-side apply to server-side apply
// without conflicts.
func ( f * lastAppliedManager ) Apply ( liveObj , newObj runtime . Object , managed Managed , manager string , force bool ) ( runtime . Object , Managed , error ) {
newLiveObj , newManaged , newErr := f . fieldManager . Apply ( liveObj , newObj , managed , manager , force )
// Upgrade the client-side apply annotation only from kubectl server-side-apply.
// To opt-out of this behavior, users may specify a different field manager.
if manager != "kubectl" {
return newLiveObj , newManaged , newErr
}
// Check if we have conflicts
if newErr == nil {
return newLiveObj , newManaged , newErr
}
conflicts , ok := newErr . ( merge . Conflicts )
if ! ok {
return newLiveObj , newManaged , newErr
}
conflictSet := conflictsToSet ( conflicts )
// Check if conflicts are allowed due to client-side apply,
// and if so, then force apply
allowedConflictSet , err := f . allowedConflictsFromLastApplied ( liveObj )
if err != nil {
return newLiveObj , newManaged , newErr
}
if ! conflictSet . Difference ( allowedConflictSet ) . Empty ( ) {
newConflicts := conflictsDifference ( conflicts , allowedConflictSet )
return newLiveObj , newManaged , newConflicts
}
return f . fieldManager . Apply ( liveObj , newObj , managed , manager , true )
}
func ( f * lastAppliedManager ) allowedConflictsFromLastApplied ( liveObj runtime . Object ) ( * fieldpath . Set , error ) {
var accessor , err = meta . Accessor ( liveObj )
if err != nil {
panic ( fmt . Sprintf ( "couldn't get accessor: %v" , err ) )
}
// If there is no client-side apply annotation, then there is nothing to do
var annotations = accessor . GetAnnotations ( )
if annotations == nil {
return nil , fmt . Errorf ( "no last applied annotation" )
}
var lastApplied , ok = annotations [ corev1 . LastAppliedConfigAnnotation ]
if ! ok || lastApplied == "" {
return nil , fmt . Errorf ( "no last applied annotation" )
}
liveObjVersioned , err := f . objectConverter . ConvertToVersion ( liveObj , f . groupVersion )
if err != nil {
return nil , fmt . Errorf ( "failed to convert live obj to versioned: %v" , err )
}
liveObjTyped , err := f . typeConverter . ObjectToTyped ( liveObjVersioned )
if err != nil {
return nil , fmt . Errorf ( "failed to convert live obj to typed: %v" , err )
}
var lastAppliedObj = & unstructured . Unstructured { Object : map [ string ] interface { } { } }
err = json . Unmarshal ( [ ] byte ( lastApplied ) , lastAppliedObj )
if err != nil {
return nil , fmt . Errorf ( "failed to decode last applied obj: %v in '%s'" , err , lastApplied )
}
if lastAppliedObj . GetAPIVersion ( ) != f . groupVersion . String ( ) {
return nil , fmt . Errorf ( "expected version of last applied to match live object '%s', but got '%s': %v" , f . groupVersion . String ( ) , lastAppliedObj . GetAPIVersion ( ) , err )
}
lastAppliedObjTyped , err := f . typeConverter . ObjectToTyped ( lastAppliedObj )
if err != nil {
return nil , fmt . Errorf ( "failed to convert last applied to typed: %v" , err )
}
lastAppliedObjFieldSet , err := lastAppliedObjTyped . ToFieldSet ( )
if err != nil {
return nil , fmt . Errorf ( "failed to create fieldset for last applied object: %v" , err )
}
comparison , err := lastAppliedObjTyped . Compare ( liveObjTyped )
if err != nil {
return nil , fmt . Errorf ( "failed to compare last applied object and live object: %v" , err )
}
// Remove fields in last applied that are different, added, or missing in
// the live object.
// Because last-applied fields don't match the live object fields,
// then we don't own these fields.
lastAppliedObjFieldSet = lastAppliedObjFieldSet .
Difference ( comparison . Modified ) .
Difference ( comparison . Added ) .
Difference ( comparison . Removed )
return lastAppliedObjFieldSet , nil
}
// TODO: replace with merge.Conflicts.ToSet()
func conflictsToSet ( conflicts merge . Conflicts ) * fieldpath . Set {
conflictSet := fieldpath . NewSet ( )
for _ , conflict := range [ ] merge . Conflict ( conflicts ) {
conflictSet . Insert ( conflict . Path )
}
return conflictSet
}
func conflictsDifference ( conflicts merge . Conflicts , s * fieldpath . Set ) merge . Conflicts {
newConflicts := [ ] merge . Conflict { }
for _ , conflict := range [ ] merge . Conflict ( conflicts ) {
if ! s . Has ( conflict . Path ) {
newConflicts = append ( newConflicts , conflict )
}
}
return newConflicts
}