diff --git a/applyconfigurations/meta/v1/unstructured.go b/applyconfigurations/meta/v1/unstructured.go new file mode 100644 index 00000000..040a6adb --- /dev/null +++ b/applyconfigurations/meta/v1/unstructured.go @@ -0,0 +1,137 @@ +/* +Copyright 2021 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 v1 + +import ( + "fmt" + "sync" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/managedfields" + "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/util/proto" + "sigs.k8s.io/structured-merge-diff/v4/typed" +) + +// openAPISchemaTTL is how frequently we need to check +// whether the open API schema has changed or not. +const openAPISchemaTTL = time.Minute + +// UnstructuredExtractor enables extracting the applied configuration state from object for fieldManager into an +// unstructured object type. +type UnstructuredExtractor interface { + Extract(object *unstructured.Unstructured, fieldManager string) (*unstructured.Unstructured, error) + ExtractStatus(object *unstructured.Unstructured, fieldManager string) (*unstructured.Unstructured, error) +} + +// gvkParserCache caches the GVKParser in order to prevent from having to repeatedly +// parse the models from the open API schema when the schema itself changes infrequently. +type gvkParserCache struct { + // discoveryClient is the client for retrieving the openAPI document and checking + // whether the document has changed recently + discoveryClient discovery.DiscoveryInterface + // mu protects the gvkParser + mu sync.Mutex + // gvkParser retrieves the objectType for a given gvk + gvkParser *managedfields.GvkParser + // lastChecked is the last time we checked if the openAPI doc has changed. + lastChecked time.Time +} + +// regenerateGVKParser builds the parser from the raw OpenAPI schema. +func regenerateGVKParser(dc discovery.DiscoveryInterface) (*managedfields.GvkParser, error) { + doc, err := dc.OpenAPISchema() + if err != nil { + return nil, err + } + + models, err := proto.NewOpenAPIData(doc) + if err != nil { + return nil, err + } + + return managedfields.NewGVKParser(models, false) +} + +// objectTypeForGVK retrieves the typed.ParseableType for a given gvk from the cache +func (c *gvkParserCache) objectTypeForGVK(gvk schema.GroupVersionKind) (*typed.ParseableType, error) { + c.mu.Lock() + defer c.mu.Unlock() + // if the ttl on the openAPISchema has expired, + // regenerate the gvk parser + if time.Since(c.lastChecked) > openAPISchemaTTL { + c.lastChecked = time.Now() + parser, err := regenerateGVKParser(c.discoveryClient) + if err != nil { + return nil, err + } + c.gvkParser = parser + } + return c.gvkParser.Type(gvk), nil +} + +type extractor struct { + cache *gvkParserCache +} + +// NewUnstructuredExtractor creates the extractor with which you can extract the applied configuration +// for a given manager from an unstructured object. +func NewUnstructuredExtractor(dc discovery.DiscoveryInterface) (UnstructuredExtractor, error) { + parser, err := regenerateGVKParser(dc) + if err != nil { + return nil, fmt.Errorf("failed generating initial GVK Parser: %v", err) + } + return &extractor{ + cache: &gvkParserCache{ + gvkParser: parser, + discoveryClient: dc, + }, + }, nil +} + +// Extract extracts the applied configuration owned by fiieldManager from an unstructured object. +// Note that the apply configuration itself is also an unstructured object. +func (e *extractor) Extract(object *unstructured.Unstructured, fieldManager string) (*unstructured.Unstructured, error) { + return e.extractUnstructured(object, fieldManager, "") +} + +// ExtractStatus is the same as ExtractUnstructured except +// that it extracts the status subresource applied configuration. +// Experimental! +func (e *extractor) ExtractStatus(object *unstructured.Unstructured, fieldManager string) (*unstructured.Unstructured, error) { + return e.extractUnstructured(object, fieldManager, "status") +} + +func (e *extractor) extractUnstructured(object *unstructured.Unstructured, fieldManager string, subresource string) (*unstructured.Unstructured, error) { + gvk := object.GetObjectKind().GroupVersionKind() + objectType, err := e.cache.objectTypeForGVK(gvk) + if err != nil { + return nil, fmt.Errorf("failed to fetch the objectType: %v", err) + } + result := &unstructured.Unstructured{} + err = managedfields.ExtractInto(object, *objectType, fieldManager, result, subresource) + if err != nil { + return nil, fmt.Errorf("failed calling ExtractInto for unstructured: %v", err) + } + result.SetName(object.GetName()) + result.SetNamespace(object.GetNamespace()) + result.SetKind(object.GetKind()) + result.SetAPIVersion(object.GetAPIVersion()) + return result, nil +} diff --git a/go.mod b/go.mod index 5f09d892..be154e9e 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,9 @@ require ( golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac google.golang.org/protobuf v1.26.0 k8s.io/api v0.0.0-20210720141931-aa30bdaf750c - k8s.io/apimachinery v0.0.0-20210712060818-a644435e2c13 + k8s.io/apimachinery v0.0.0-20210805051055-f7769293e6f1 k8s.io/klog/v2 v2.9.0 + k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9 sigs.k8s.io/structured-merge-diff/v4 v4.1.2 sigs.k8s.io/yaml v1.2.0 @@ -40,5 +41,5 @@ require ( replace ( k8s.io/api => k8s.io/api v0.0.0-20210720141931-aa30bdaf750c - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20210712060818-a644435e2c13 + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20210805051055-f7769293e6f1 ) diff --git a/go.sum b/go.sum index c5159c4a..af374795 100644 --- a/go.sum +++ b/go.sum @@ -446,8 +446,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.0.0-20210720141931-aa30bdaf750c h1:bQDI22QyjupmAGqhbVE2uIkAkAAVEVfTf/n8RG/Xtpo= k8s.io/api v0.0.0-20210720141931-aa30bdaf750c/go.mod h1:FtqZiusVhnyM5jUPPFkDCU91OKo0sOpX9b9hotVGbIk= -k8s.io/apimachinery v0.0.0-20210712060818-a644435e2c13 h1:Y40e5ho6n8KOvBCqdz/jm7ssKQyQLQdbZbC3lR9TaCU= -k8s.io/apimachinery v0.0.0-20210712060818-a644435e2c13/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.0.0-20210805051055-f7769293e6f1 h1:cVpwhaGeh/tNPBeYbFff3tjx5AxwG5zwImhz+eusG3k= +k8s.io/apimachinery v0.0.0-20210805051055-f7769293e6f1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM=