github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/pkg/live/load.go (about)

     1  // Copyright 2021 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package live
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io"
    21  
    22  	"github.com/GoogleContainerTools/kpt/internal/errors"
    23  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    24  	"github.com/GoogleContainerTools/kpt/internal/util/pathutil"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/strings"
    26  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    27  	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/klog/v2"
    31  	"k8s.io/kubectl/pkg/cmd/util"
    32  	"sigs.k8s.io/cli-utils/pkg/common"
    33  	"sigs.k8s.io/cli-utils/pkg/inventory"
    34  	"sigs.k8s.io/cli-utils/pkg/manifestreader"
    35  	"sigs.k8s.io/kustomize/kyaml/filesys"
    36  	"sigs.k8s.io/kustomize/kyaml/kio"
    37  	"sigs.k8s.io/kustomize/kyaml/yaml"
    38  )
    39  
    40  // inventoryIDfmt is the string format used for generating an inventoryID that is stored on the live cluster
    41  // if one is not provided when the user runs `kpt live init`. This format should be of `namespace-name`.
    42  const inventoryIDfmt = "%s-%s"
    43  
    44  // InventoryInfoValidationError is the error returned if validation of the
    45  // inventory information fails.
    46  type InventoryInfoValidationError struct {
    47  	errors.ValidationError
    48  }
    49  
    50  func (e *InventoryInfoValidationError) Error() string {
    51  	return fmt.Sprintf("inventory failed validation for fields: %s",
    52  		strings.JoinStringsWithQuotes(e.Violations.Fields()))
    53  }
    54  
    55  // Load reads resources either from disk or from an input stream. It filters
    56  // out resources that should be ignored and defaults the namespace for
    57  // namespace-scoped resources that doesn't have the namespace set. It also looks
    58  // for inventory information inside Kptfile or resourcegroup resources.
    59  // It returns the resources in unstructured format and the inventory information.
    60  // If no inventory information is found, that is not considered an error here.
    61  func Load(f util.Factory, path string, stdIn io.Reader) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) {
    62  	if path == "-" {
    63  		return loadFromStream(f, stdIn)
    64  	}
    65  	return loadFromDisk(f, path)
    66  }
    67  
    68  // loadFromStream reads resources from the provided reader and returns the
    69  // filtered resources and any inventory information found in Kptfile resources.
    70  // If there is more than one Kptfile in the stream with inventory information, that
    71  // is considered an error.
    72  func loadFromStream(f util.Factory, r io.Reader) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) {
    73  	var stdInBuf bytes.Buffer
    74  	tee := io.TeeReader(r, &stdInBuf)
    75  
    76  	invInfo, err := readInvInfoFromStream(tee)
    77  	if err != nil {
    78  		return nil, kptfilev1.Inventory{}, err
    79  	}
    80  	if !invInfo.IsValid() {
    81  		return nil, kptfilev1.Inventory{}, &pkg.InvInfoInvalid{}
    82  	}
    83  
    84  	ro, err := toReaderOptions(f)
    85  	if err != nil {
    86  		return nil, kptfilev1.Inventory{}, err
    87  	}
    88  
    89  	objs, err := (&ResourceGroupStreamManifestReader{
    90  		ReaderName:    "stdin",
    91  		Reader:        &stdInBuf,
    92  		ReaderOptions: ro,
    93  	}).Read()
    94  	if err != nil {
    95  		return nil, kptfilev1.Inventory{}, err
    96  	}
    97  	return objs, invInfo, nil
    98  }
    99  
   100  func readInvInfoFromStream(in io.Reader) (kptfilev1.Inventory, error) {
   101  	invFilter := &InventoryFilter{}
   102  	rgFilter := &RGFilter{}
   103  	if err := (&kio.Pipeline{
   104  		Inputs: []kio.Reader{
   105  			&kio.ByteReader{
   106  				Reader:          in,
   107  				WrapBareSeqNode: true,
   108  			},
   109  		},
   110  		Filters: []kio.Filter{
   111  			kio.FilterAll(invFilter),
   112  			kio.FilterAll(rgFilter),
   113  		},
   114  	}).Execute(); err != nil {
   115  		return kptfilev1.Inventory{}, err
   116  	}
   117  
   118  	// Ensure only exactly 1 inventory exists and surface the correct type of error.
   119  	// Multiple Kptfile inventories found.
   120  	if len(invFilter.Inventories) > 1 {
   121  		return kptfilev1.Inventory{}, &pkg.MultipleKfInv{}
   122  	}
   123  	// Multiple ResourceGroup inventories found.
   124  	if len(rgFilter.Inventories) > 1 {
   125  		return kptfilev1.Inventory{}, &pkg.MultipleResourceGroupsError{}
   126  	}
   127  	// Multiple inventories found in Kptfile and ResourceGroup objects.
   128  	if len(invFilter.Inventories) > 0 && len(rgFilter.Inventories) > 0 {
   129  		return kptfilev1.Inventory{}, &pkg.MultipleInventoryInfoError{}
   130  	}
   131  
   132  	// Inventory found within Kptfile.
   133  	if len(invFilter.Inventories) == 1 {
   134  		return *invFilter.Inventories[0], nil
   135  	}
   136  	// Inventory found with ResourceGroup object.
   137  	if len(rgFilter.Inventories) == 1 {
   138  		invID := rgFilter.Inventories[0].Labels[rgfilev1alpha1.RGInventoryIDLabel]
   139  		return kptfilev1.Inventory{
   140  			Name:        rgFilter.Inventories[0].Name,
   141  			Namespace:   rgFilter.Inventories[0].Namespace,
   142  			InventoryID: invID,
   143  		}, nil
   144  	}
   145  
   146  	// No inventories found in stream.
   147  	return kptfilev1.Inventory{}, &pkg.NoInvInfoError{}
   148  }
   149  
   150  // loadFromdisk reads resources from the provided directory and any subfolder.
   151  // It returns the filtered resources and any inventory information found in
   152  // Kptfile resources.
   153  // Only the Kptfile in the root directory will be checked for inventory information.
   154  func loadFromDisk(f util.Factory, path string) ([]*unstructured.Unstructured, kptfilev1.Inventory, error) {
   155  	invInfo, err := readInvInfoFromDisk(path)
   156  	if err != nil {
   157  		return nil, kptfilev1.Inventory{}, err
   158  	}
   159  
   160  	if !invInfo.IsValid() {
   161  		return nil, kptfilev1.Inventory{}, &pkg.InvInfoInvalid{}
   162  	}
   163  
   164  	ro, err := toReaderOptions(f)
   165  	if err != nil {
   166  		return nil, kptfilev1.Inventory{}, err
   167  	}
   168  
   169  	objs, err := (&ResourceGroupPathManifestReader{
   170  		PkgPath:       path,
   171  		ReaderOptions: ro,
   172  	}).Read()
   173  	if err != nil {
   174  		return nil, kptfilev1.Inventory{}, err
   175  	}
   176  
   177  	return objs, invInfo, nil
   178  }
   179  
   180  func readInvInfoFromDisk(path string) (kptfilev1.Inventory, error) {
   181  	absPath, _, err := pathutil.ResolveAbsAndRelPaths(path)
   182  	if err != nil {
   183  		return kptfilev1.Inventory{}, err
   184  	}
   185  	p, err := pkg.New(filesys.FileSystemOrOnDisk{}, absPath)
   186  	if err != nil {
   187  		return kptfilev1.Inventory{}, err
   188  	}
   189  
   190  	return p.LocalInventory()
   191  }
   192  
   193  // InventoryFilter is an implementation of the yaml.Filter interface
   194  // that extracts inventory information from Kptfile resources.
   195  type InventoryFilter struct {
   196  	Inventories []*kptfilev1.Inventory
   197  }
   198  
   199  func (i *InventoryFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) {
   200  	if GroupVersionKindForObject(object) != kptfilev1.KptFileGVK() {
   201  		return object, nil
   202  	}
   203  
   204  	s, err := object.String()
   205  	if err != nil {
   206  		return object, err
   207  	}
   208  	kf, err := pkg.DecodeKptfile(bytes.NewBufferString(s))
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if kf.Inventory != nil {
   213  		i.Inventories = append(i.Inventories, kf.Inventory)
   214  	}
   215  	return object, nil
   216  }
   217  
   218  // RGFilter is an implementation of the yaml.Filter interface
   219  // that extracts inventory information from resourcegroup objects.
   220  type RGFilter struct {
   221  	Inventories []*rgfilev1alpha1.ResourceGroup
   222  }
   223  
   224  // GroupVersionKindForObject extracts the group/version/kind from an RNode holding a kubernetes object.
   225  func GroupVersionKindForObject(object *yaml.RNode) schema.GroupVersionKind {
   226  	apiVersion := object.GetApiVersion()
   227  	gv, err := schema.ParseGroupVersion(apiVersion)
   228  	if err != nil {
   229  		klog.Warningf("error parsing apiVersion=%q", apiVersion)
   230  	}
   231  	return gv.WithKind(object.GetKind())
   232  }
   233  
   234  func (r *RGFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) {
   235  	if GroupVersionKindForObject(object) != rgfilev1alpha1.ResourceGroupGVK() {
   236  		return object, nil
   237  	}
   238  
   239  	s, err := object.String()
   240  	if err != nil {
   241  		return object, err
   242  	}
   243  	rg, err := pkg.DecodeRGFile(bytes.NewBufferString(s))
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	r.Inventories = append(r.Inventories, rg)
   248  	return object, nil
   249  }
   250  
   251  // toReaderOptions returns the readerOptions for a factory.
   252  func toReaderOptions(f util.Factory) (manifestreader.ReaderOptions, error) {
   253  	namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
   254  	if err != nil {
   255  		return manifestreader.ReaderOptions{}, err
   256  	}
   257  	mapper, err := f.ToRESTMapper()
   258  	if err != nil {
   259  		return manifestreader.ReaderOptions{}, err
   260  	}
   261  
   262  	return manifestreader.ReaderOptions{
   263  		Mapper:           mapper,
   264  		Namespace:        namespace,
   265  		EnforceNamespace: enforceNamespace,
   266  	}, nil
   267  }
   268  
   269  // ToInventoryInfo takes the information in the provided inventory object and
   270  // return an InventoryResourceGroup implementation of the InventoryInfo interface.
   271  func ToInventoryInfo(inventory kptfilev1.Inventory) (inventory.Info, error) {
   272  	if err := validateInventory(inventory); err != nil {
   273  		return nil, err
   274  	}
   275  	invObj := generateInventoryObj(inventory)
   276  	return WrapInventoryInfoObj(invObj), nil
   277  }
   278  
   279  func validateInventory(inventory kptfilev1.Inventory) error {
   280  	var violations errors.Violations
   281  	if inventory.Name == "" {
   282  		violations = append(violations, errors.Violation{
   283  			Field:  "name",
   284  			Value:  inventory.Name,
   285  			Type:   errors.Missing,
   286  			Reason: "\"inventory.name\" must not be empty",
   287  		})
   288  	}
   289  	if inventory.Namespace == "" {
   290  		violations = append(violations, errors.Violation{
   291  			Field:  "namespace",
   292  			Value:  inventory.Namespace,
   293  			Type:   errors.Missing,
   294  			Reason: "\"inventory.namespace\" must not be empty",
   295  		})
   296  	}
   297  	if inventory.InventoryID == "" {
   298  		violations = append(violations, errors.Violation{
   299  			Field:  "inventoryID",
   300  			Value:  inventory.InventoryID,
   301  			Type:   errors.Missing,
   302  			Reason: "\"inventory.inventoryID\" must not be empty",
   303  		})
   304  	}
   305  	if len(violations) > 0 {
   306  		return &InventoryInfoValidationError{
   307  			ValidationError: errors.ValidationError{
   308  				Violations: violations,
   309  			},
   310  		}
   311  	}
   312  	return nil
   313  }
   314  
   315  func generateInventoryObj(inv kptfilev1.Inventory) *unstructured.Unstructured {
   316  	// Create and return ResourceGroup custom resource as inventory object.
   317  
   318  	// When inventoryID is not specified by the local resourcegroup, we generate one using
   319  	// depends-on annotation format that we store on the live cluster. This implementation detail
   320  	// is hidden from the local resourcegroup unless the one was explicitly generated by the client.
   321  	if inv.InventoryID == "" {
   322  		inv.InventoryID = fmt.Sprintf(inventoryIDfmt, inv.Namespace, inv.Name)
   323  	}
   324  
   325  	groupVersion := fmt.Sprintf("%s/%s", ResourceGroupGVK.Group, ResourceGroupGVK.Version)
   326  	var inventoryObj = &unstructured.Unstructured{
   327  		Object: map[string]interface{}{
   328  			"apiVersion": groupVersion,
   329  			"kind":       ResourceGroupGVK.Kind,
   330  			"metadata": map[string]interface{}{
   331  				"name":      inv.Name,
   332  				"namespace": inv.Namespace,
   333  				"labels": map[string]interface{}{
   334  					common.InventoryLabel: inv.InventoryID,
   335  				},
   336  			},
   337  			"spec": map[string]interface{}{
   338  				"resources": []interface{}{},
   339  			},
   340  		},
   341  	}
   342  	labels := inv.Labels
   343  	if labels == nil {
   344  		labels = make(map[string]string)
   345  	}
   346  	labels[common.InventoryLabel] = inv.InventoryID
   347  	inventoryObj.SetLabels(labels)
   348  	inventoryObj.SetAnnotations(inv.Annotations)
   349  	return inventoryObj
   350  }