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 }