mirror of https://github.com/kubernetes/kops.git
applylib: combine field managers
This ensures that we don't lose managed fields as we update them.
This commit is contained in:
parent
32cb151419
commit
8288515eb4
3
go.mod
3
go.mod
|
@ -57,6 +57,7 @@ require (
|
||||||
helm.sh/helm/v3 v3.10.2
|
helm.sh/helm/v3 v3.10.2
|
||||||
k8s.io/api v0.25.4
|
k8s.io/api v0.25.4
|
||||||
k8s.io/apimachinery v0.25.4
|
k8s.io/apimachinery v0.25.4
|
||||||
|
k8s.io/apiserver v0.25.4
|
||||||
k8s.io/cli-runtime v0.25.4
|
k8s.io/cli-runtime v0.25.4
|
||||||
k8s.io/client-go v0.25.4
|
k8s.io/client-go v0.25.4
|
||||||
k8s.io/cloud-provider-aws v1.25.1
|
k8s.io/cloud-provider-aws v1.25.1
|
||||||
|
@ -69,6 +70,7 @@ require (
|
||||||
k8s.io/mount-utils v0.25.4
|
k8s.io/mount-utils v0.25.4
|
||||||
k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2
|
k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2
|
||||||
sigs.k8s.io/controller-runtime v0.13.1
|
sigs.k8s.io/controller-runtime v0.13.1
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.2.3
|
||||||
sigs.k8s.io/yaml v1.3.0
|
sigs.k8s.io/yaml v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -248,5 +250,4 @@ require (
|
||||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||||
sigs.k8s.io/kustomize/api v0.12.1 // indirect
|
sigs.k8s.io/kustomize/api v0.12.1 // indirect
|
||||||
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
|
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1345,6 +1345,8 @@ k8s.io/apiextensions-apiserver v0.25.2 h1:8uOQX17RE7XL02ngtnh3TgifY7EhekpK+/piwz
|
||||||
k8s.io/apiextensions-apiserver v0.25.2/go.mod h1:iRwwRDlWPfaHhuBfQ0WMa5skdQfrE18QXJaJvIDLvE8=
|
k8s.io/apiextensions-apiserver v0.25.2/go.mod h1:iRwwRDlWPfaHhuBfQ0WMa5skdQfrE18QXJaJvIDLvE8=
|
||||||
k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc=
|
k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc=
|
||||||
k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=
|
k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=
|
||||||
|
k8s.io/apiserver v0.25.4 h1:/3TwZcgLqX7wUxq7TtXOUqXeBTwXIblVMQdhR5XZ7yo=
|
||||||
|
k8s.io/apiserver v0.25.4/go.mod h1:rPcm567XxjOnnd7jedDUnGJGmDGAo+cT6H7QHAN+xV0=
|
||||||
k8s.io/cli-runtime v0.25.4 h1:GTSBN7aKBrc2LqpdO30CmHQqJtRmotxV7XsMSP+QZIk=
|
k8s.io/cli-runtime v0.25.4 h1:GTSBN7aKBrc2LqpdO30CmHQqJtRmotxV7XsMSP+QZIk=
|
||||||
k8s.io/cli-runtime v0.25.4/go.mod h1:JGOw1CR8v4Mcz6cEKA7bFQe0bPrNn1l5sGAX1/Ke4Eg=
|
k8s.io/cli-runtime v0.25.4/go.mod h1:JGOw1CR8v4Mcz6cEKA7bFQe0bPrNn1l5sGAX1/Ke4Eg=
|
||||||
k8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8=
|
k8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8=
|
||||||
|
|
|
@ -22,11 +22,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
)
|
)
|
||||||
|
@ -102,6 +100,11 @@ func (a *ApplySet) ApplyOnce(ctx context.Context) (*ApplyResults, error) {
|
||||||
trackers := a.trackers
|
trackers := a.trackers
|
||||||
a.mutex.Unlock()
|
a.mutex.Unlock()
|
||||||
|
|
||||||
|
client := &UnstructuredClient{
|
||||||
|
client: a.client,
|
||||||
|
restMapper: a.restMapper,
|
||||||
|
}
|
||||||
|
|
||||||
results := &ApplyResults{total: len(trackers.items)}
|
results := &ApplyResults{total: len(trackers.items)}
|
||||||
|
|
||||||
for i := range trackers.items {
|
for i := range trackers.items {
|
||||||
|
@ -113,54 +116,28 @@ func (a *ApplySet) ApplyOnce(ctx context.Context) (*ApplyResults, error) {
|
||||||
gvk := expectedObject.GroupVersionKind()
|
gvk := expectedObject.GroupVersionKind()
|
||||||
nn := types.NamespacedName{Namespace: ns, Name: name}
|
nn := types.NamespacedName{Namespace: ns, Name: name}
|
||||||
|
|
||||||
restMapping, err := a.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
currentObj, err := client.Get(ctx, gvk, nn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results.applyError(gvk, nn, fmt.Errorf("error getting rest mapping for %v: %w", gvk, err))
|
if !apierrors.IsNotFound(err) {
|
||||||
|
results.applyError(gvk, nn, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gvr := restMapping.Resource
|
}
|
||||||
|
|
||||||
var dynamicResource dynamic.ResourceInterface
|
// If the object exists, we need to update any client-side-apply field-managers
|
||||||
|
// Otherwise we often end up with old and new objects combined, which
|
||||||
switch restMapping.Scope.Name() {
|
// is unexpected and can be invalid.
|
||||||
case meta.RESTScopeNameNamespace:
|
if currentObj != nil {
|
||||||
if ns == "" {
|
managedFields := &ManagedFieldsMigrator{
|
||||||
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
NewManager: "kops",
|
||||||
results.applyError(gvk, nn, fmt.Errorf("namespace was not provided for namespace-scoped object %v", gvk))
|
Client: client,
|
||||||
|
}
|
||||||
|
if err := managedFields.Migrate(ctx, currentObj); err != nil {
|
||||||
|
results.applyError(gvk, nn, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dynamicResource = a.client.Resource(gvr).Namespace(ns)
|
|
||||||
|
|
||||||
case meta.RESTScopeNameRoot:
|
|
||||||
if ns != "" {
|
|
||||||
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
|
||||||
results.applyError(gvk, nn, fmt.Errorf("namespace %q was provided for cluster-scoped object %v", expectedObject.GetNamespace(), gvk))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dynamicResource = a.client.Resource(gvr)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Internal error ... this is panic-level
|
|
||||||
return nil, fmt.Errorf("unknown scope for gvk %s: %q", gvk, restMapping.Scope.Name())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentObj, err := dynamicResource.Get(ctx, name, v1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
if !errors.IsNotFound(err) {
|
|
||||||
return nil, fmt.Errorf("could not get existing object: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonData, err := createManagedFieldPatch(currentObj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create json patch: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if jsonData != nil {
|
|
||||||
_, err := dynamicResource.Patch(ctx, name, types.MergePatchType, jsonData, v1.PatchOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to patch object %q: %w", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j, err := json.Marshal(expectedObject)
|
j, err := json.Marshal(expectedObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
||||||
|
@ -168,7 +145,7 @@ func (a *ApplySet) ApplyOnce(ctx context.Context) (*ApplyResults, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lastApplied, err := dynamicResource.Patch(ctx, name, types.ApplyPatchType, j, a.patchOptions)
|
lastApplied, err := client.Patch(ctx, gvk, nn, types.ApplyPatchType, j, a.patchOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results.applyError(gvk, nn, fmt.Errorf("error from apply: %w", err))
|
results.applyError(gvk, nn, fmt.Errorf("error from apply: %w", err))
|
||||||
continue
|
continue
|
||||||
|
@ -181,36 +158,3 @@ func (a *ApplySet) ApplyOnce(ctx context.Context) (*ApplyResults, error) {
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createManagedFieldPatch(currentObject *unstructured.Unstructured) ([]byte, error) {
|
|
||||||
if currentObject == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
fixedManagedFields := []v1.ManagedFieldsEntry{}
|
|
||||||
for _, managedField := range currentObject.GetManagedFields() {
|
|
||||||
fixedManagedField := managedField.DeepCopy()
|
|
||||||
if managedField.Manager == "kubectl-edit" || managedField.Manager == "kubectl-client-side-apply" {
|
|
||||||
fixedManagedField.Manager = "kops"
|
|
||||||
fixedManagedField.Operation = "Apply"
|
|
||||||
}
|
|
||||||
// In case we have a kops & Update manager
|
|
||||||
if fixedManagedField.Manager == "kops" && fixedManagedField.Operation == "Update" {
|
|
||||||
fixedManagedField.Operation = "Apply"
|
|
||||||
}
|
|
||||||
fixedManagedFields = append(fixedManagedFields, *fixedManagedField)
|
|
||||||
}
|
|
||||||
if len(fixedManagedFields) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
meta := &v1.ObjectMeta{}
|
|
||||||
meta.SetManagedFields(fixedManagedFields)
|
|
||||||
patchObject := map[string]interface{}{
|
|
||||||
"metadata": meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(patchObject)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marsal %q into json: %w", currentObject.GetName(), err)
|
|
||||||
}
|
|
||||||
return jsonData, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 applyset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManagedFieldsMigrator manages the migration of field managers from client-side managers to the server-side manager.
|
||||||
|
type ManagedFieldsMigrator struct {
|
||||||
|
Client *UnstructuredClient
|
||||||
|
NewManager string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate migrates from client-side field managers to the NewManager (with an Apply operation).
|
||||||
|
// This is needed to move from client-side apply to server-side apply.
|
||||||
|
func (m *ManagedFieldsMigrator) Migrate(ctx context.Context, obj *unstructured.Unstructured) error {
|
||||||
|
managedFieldPatch, err := m.createManagedFieldPatch(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create managed-fields patch: %w", err)
|
||||||
|
}
|
||||||
|
if managedFieldPatch != nil {
|
||||||
|
gvk := obj.GroupVersionKind()
|
||||||
|
nn := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}
|
||||||
|
_, err := m.Client.Patch(ctx, gvk, nn, types.MergePatchType, managedFieldPatch, metav1.PatchOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to patch object managed-fields for %q: %w", obj.GetName(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createManagedFieldPatch constructs a patch to combine managed fields.
|
||||||
|
// It returns nil if no patch is needed.
|
||||||
|
func (m *ManagedFieldsMigrator) createManagedFieldPatch(currentObject *unstructured.Unstructured) ([]byte, error) {
|
||||||
|
if currentObject == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
needPatch := false
|
||||||
|
fixedManagedFields := []metav1.ManagedFieldsEntry{}
|
||||||
|
for _, managedField := range currentObject.GetManagedFields() {
|
||||||
|
fixedManagedField := managedField.DeepCopy()
|
||||||
|
if managedField.Manager == "kubectl-edit" || managedField.Manager == "kubectl-client-side-apply" {
|
||||||
|
needPatch = true
|
||||||
|
fixedManagedField.Manager = m.NewManager
|
||||||
|
fixedManagedField.Operation = metav1.ManagedFieldsOperationApply
|
||||||
|
}
|
||||||
|
// In case we have an existing Update operation
|
||||||
|
if fixedManagedField.Manager == m.NewManager && fixedManagedField.Operation == "Update" {
|
||||||
|
needPatch = true
|
||||||
|
fixedManagedField.Operation = metav1.ManagedFieldsOperationApply
|
||||||
|
}
|
||||||
|
fixedManagedFields = append(fixedManagedFields, *fixedManagedField)
|
||||||
|
}
|
||||||
|
if !needPatch {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := mergeFieldManagers(fixedManagedFields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fixedManagedFields = merged
|
||||||
|
|
||||||
|
meta := &metav1.ObjectMeta{}
|
||||||
|
meta.SetManagedFields(fixedManagedFields)
|
||||||
|
patchObject := map[string]interface{}{
|
||||||
|
"metadata": meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(patchObject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marsal %q into json: %w", currentObject.GetName(), err)
|
||||||
|
}
|
||||||
|
return jsonData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldManagerKey is the primary key for a ManagedFieldEntry
|
||||||
|
type fieldManagerKey struct {
|
||||||
|
Manager string
|
||||||
|
Operation metav1.ManagedFieldsOperationType
|
||||||
|
Subresource string
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeFieldManagers merges the managed fields from identical field managers.
|
||||||
|
// If we don't do this, the apiserver will not currently construct the union for duplicate keys.
|
||||||
|
func mergeFieldManagers(managedFields []metav1.ManagedFieldsEntry) ([]metav1.ManagedFieldsEntry, error) {
|
||||||
|
byKey := make(map[fieldManagerKey][]metav1.ManagedFieldsEntry)
|
||||||
|
for _, f := range managedFields {
|
||||||
|
k := fieldManagerKey{
|
||||||
|
Manager: f.Manager,
|
||||||
|
Operation: f.Operation,
|
||||||
|
Subresource: f.Subresource,
|
||||||
|
}
|
||||||
|
|
||||||
|
byKey[k] = append(byKey[k], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []metav1.ManagedFieldsEntry
|
||||||
|
for k := range byKey {
|
||||||
|
managers := byKey[k]
|
||||||
|
if len(managers) > 1 {
|
||||||
|
fieldSet, err := mergeManagedFields(managers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encoded, err := fieldSet.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
managers[0].FieldsV1.Raw = encoded
|
||||||
|
}
|
||||||
|
result = append(result, managers[0])
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeManagedFields merges a set of ManagedFieldEntry managed fields, that are expected to have the same key.
|
||||||
|
func mergeManagedFields(managedFields []metav1.ManagedFieldsEntry) (*fieldpath.Set, error) {
|
||||||
|
if len(managedFields) == 0 {
|
||||||
|
return nil, fmt.Errorf("no managed fields supplied")
|
||||||
|
}
|
||||||
|
|
||||||
|
union, err := toFieldPathSet(&managedFields[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range managedFields {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := &managedFields[i]
|
||||||
|
if managedFields[0].APIVersion != m.APIVersion {
|
||||||
|
return nil, fmt.Errorf("cannot merge ManagedFieldsEntry apiVersion %q with apiVersion %q", managedFields[0].APIVersion, m.APIVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
set, err := toFieldPathSet(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
union = union.Union(set)
|
||||||
|
}
|
||||||
|
return union, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toFieldPathSet converts an encoded ManagedFieldsEntry to a set of managed fields (a fieldpath.Set)
|
||||||
|
func toFieldPathSet(fields *metav1.ManagedFieldsEntry) (*fieldpath.Set, error) {
|
||||||
|
decoded, err := fieldmanager.DecodeManagedFields([]metav1.ManagedFieldsEntry{*fields})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(decoded.Fields()) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected a single managed fields entry, but got %d", len(decoded.Fields()))
|
||||||
|
}
|
||||||
|
for _, fieldSet := range decoded.Fields() {
|
||||||
|
return fieldSet.Set(), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no fields were decoded")
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 applyset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnstructuredClient is a client that makes it easier to work with unstructured objects.
|
||||||
|
// It is similar to client.Client in controller-runtime, but is never cached.
|
||||||
|
type UnstructuredClient struct {
|
||||||
|
// client is the dynamic kubernetes client used to apply objects to the k8s cluster.
|
||||||
|
client dynamic.Interface
|
||||||
|
// restMapper is used to map object kind to resources, and to know if objects are cluster-scoped.
|
||||||
|
restMapper meta.RESTMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnstructuredClient constructs an UnstructuredClient
|
||||||
|
func NewUnstructuredClient(options Options) *UnstructuredClient {
|
||||||
|
return &UnstructuredClient{
|
||||||
|
client: options.Client,
|
||||||
|
restMapper: options.RESTMapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamicResource is a helper to get the resource for a gvk (with the namespace)
|
||||||
|
// It returns an error if a namespace is provided for a cluster-scoped resource,
|
||||||
|
// or no namespace is provided for a namespace-scoped resource.
|
||||||
|
func (c *UnstructuredClient) dynamicResource(ctx context.Context, gvk schema.GroupVersionKind, ns string) (dynamic.ResourceInterface, error) {
|
||||||
|
restMapping, err := c.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting rest mapping for %v: %w", gvk, err)
|
||||||
|
}
|
||||||
|
gvr := restMapping.Resource
|
||||||
|
|
||||||
|
switch restMapping.Scope.Name() {
|
||||||
|
case meta.RESTScopeNameNamespace:
|
||||||
|
if ns == "" {
|
||||||
|
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
||||||
|
return nil, fmt.Errorf("namespace was not provided for namespace-scoped object %v", gvk)
|
||||||
|
}
|
||||||
|
return c.client.Resource(gvr).Namespace(ns), nil
|
||||||
|
|
||||||
|
case meta.RESTScopeNameRoot:
|
||||||
|
if ns != "" {
|
||||||
|
// TODO: Differentiate between server-fixable vs client-fixable errors?
|
||||||
|
return nil, fmt.Errorf("namespace %q was provided for cluster-scoped object %v", ns, gvk)
|
||||||
|
}
|
||||||
|
return c.client.Resource(gvr), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Internal error ... this is panic-level
|
||||||
|
return nil, fmt.Errorf("unknown scope for gvk %s: %q", gvk, restMapping.Scope.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch performs a Patch operation, used for server-side apply and client-side patch.
|
||||||
|
func (c *UnstructuredClient) Patch(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName, patchType types.PatchType, data []byte, opt metav1.PatchOptions) (*unstructured.Unstructured, error) {
|
||||||
|
dynamicResource, err := c.dynamicResource(ctx, gvk, nn.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := nn.Name
|
||||||
|
patched, err := dynamicResource.Patch(ctx, name, patchType, data, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error patching object: %w", err)
|
||||||
|
}
|
||||||
|
return patched, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update performs an Update operation on the object. Generally we should prefer server-side-apply.
|
||||||
|
func (c *UnstructuredClient) Update(ctx context.Context, obj *unstructured.Unstructured, opt metav1.UpdateOptions) (*unstructured.Unstructured, error) {
|
||||||
|
gvk := obj.GroupVersionKind()
|
||||||
|
dynamicResource, err := c.dynamicResource(ctx, gvk, obj.GetNamespace())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := dynamicResource.Update(ctx, obj, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating object: %w", err)
|
||||||
|
}
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get reads the specified object.
|
||||||
|
func (c *UnstructuredClient) Get(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*unstructured.Unstructured, error) {
|
||||||
|
dynamicResource, err := c.dynamicResource(ctx, gvk, nn.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := dynamicResource.Get(ctx, nn.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get existing object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj, nil
|
||||||
|
}
|
Loading…
Reference in New Issue