cli-utils/pkg/manifestreader/common.go

163 lines
4.9 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package manifestreader
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/fluxcd/cli-utils/pkg/inventory"
"github.com/fluxcd/cli-utils/pkg/object"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// UnknownTypesError captures information about unknown types encountered.
type UnknownTypesError struct {
GroupVersionKinds []schema.GroupVersionKind
}
func (e *UnknownTypesError) Error() string {
var gvks []string
for _, gvk := range e.GroupVersionKinds {
gvks = append(gvks, fmt.Sprintf("%s/%s/%s",
gvk.Group, gvk.Version, gvk.Kind))
}
return fmt.Sprintf("unknown resource types: %s", strings.Join(gvks, ","))
}
// NamespaceMismatchError is returned if all resources must be in a specific
// namespace, and resources are found using other namespaces.
type NamespaceMismatchError struct {
RequiredNamespace string
Namespace string
}
func (e *NamespaceMismatchError) Error() string {
return fmt.Sprintf("found namespace %q, but all resources must be in namespace %q",
e.Namespace, e.RequiredNamespace)
}
// SetNamespaces verifies that every namespaced resource has the namespace
// set, and if one does not, it will set the namespace to the provided
// defaultNamespace.
// This implementation will check each resource (that doesn't already have
// the namespace set) on whether it is namespace or cluster scoped. It does
// this by first checking the RESTMapper, and it there is not match there,
// it will look for CRDs in the provided Unstructureds.
func SetNamespaces(mapper meta.RESTMapper, objs []*unstructured.Unstructured,
defaultNamespace string, enforceNamespace bool) error {
var crdObjs []*unstructured.Unstructured
// find any crds in the set of resources.
for _, obj := range objs {
if object.IsCRD(obj) {
crdObjs = append(crdObjs, obj)
}
}
var unknownGVKs []schema.GroupVersionKind
for _, obj := range objs {
// Exclude any inventory objects here since we don't want to change
// their namespace.
if inventory.IsInventoryObject(obj) {
continue
}
// Look up the scope of the resource so we know if the resource
// should have a namespace set or not.
scope, err := object.LookupResourceScope(obj, crdObjs, mapper)
if err != nil {
var unknownTypeError *object.UnknownTypeError
if errors.As(err, &unknownTypeError) {
// If no scope was found, just add the resource type to the list
// of unknown types.
unknownGVKs = append(unknownGVKs, unknownTypeError.GroupVersionKind)
continue
}
// If something went wrong when looking up the scope, just
// give up.
return err
}
switch scope {
case meta.RESTScopeNamespace:
if obj.GetNamespace() == "" {
obj.SetNamespace(defaultNamespace)
} else {
ns := obj.GetNamespace()
if enforceNamespace && ns != defaultNamespace {
return &NamespaceMismatchError{
Namespace: ns,
RequiredNamespace: defaultNamespace,
}
}
}
case meta.RESTScopeRoot:
if ns := obj.GetNamespace(); ns != "" {
return fmt.Errorf("resource is cluster-scoped but has a non-empty namespace %q", ns)
}
default:
return fmt.Errorf("unknown RESTScope %q", scope.Name())
}
}
if len(unknownGVKs) > 0 {
return &UnknownTypesError{
GroupVersionKinds: unknownGVKs,
}
}
return nil
}
// FilterLocalConfig returns a new slice of Unstructured where all resources
// with the LocalConfig annotation is filtered out.
func FilterLocalConfig(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
var filteredObjs []*unstructured.Unstructured
for _, obj := range objs {
// Ignoring the value of the LocalConfigAnnotation here. This is to be
// consistent with the behavior in the kyaml library:
// https://github.com/kubernetes-sigs/kustomize/blob/30b58e90a39485bc5724b2278651c5d26b815cb2/kyaml/kio/filters/local.go#L29
if _, found := obj.GetAnnotations()[filters.LocalConfigAnnotation]; !found {
filteredObjs = append(filteredObjs, obj)
}
}
return filteredObjs
}
// RemoveAnnotations removes the specified kioutil annotations from the resource.
func RemoveAnnotations(n *yaml.RNode, annotations ...kioutil.AnnotationKey) error {
for _, a := range annotations {
err := n.PipeE(yaml.ClearAnnotation(a))
if err != nil {
return err
}
}
return nil
}
// KyamlNodeToUnstructured take a resource represented as a kyaml RNode and
// turns it into an Unstructured object.
func KyamlNodeToUnstructured(n *yaml.RNode) (*unstructured.Unstructured, error) {
b, err := n.MarshalJSON()
if err != nil {
return nil, err
}
var m map[string]interface{}
err = json.Unmarshal(b, &m)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{
Object: m,
}, nil
}