github.com/verrazzano/verrazzano@v1.7.1/pkg/k8sutil/apply_yaml.go (about) 1 // Copyright (c) 2021, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package k8sutil 5 6 import ( 7 "bufio" 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "os" 14 "path" 15 "reflect" 16 "strings" 17 "text/template" 18 19 "github.com/verrazzano/verrazzano/pkg/kubectlutil" 20 "k8s.io/apimachinery/pkg/api/errors" 21 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 controllerruntime "sigs.k8s.io/controller-runtime" 23 crtpkg "sigs.k8s.io/controller-runtime/pkg/client" 24 "sigs.k8s.io/yaml" 25 ) 26 27 const ( 28 sep = "---" 29 ) 30 31 type ( 32 YAMLApplier struct { 33 client crtpkg.Client 34 objects []unstructured.Unstructured 35 namespaceOverride string 36 objectResultMsgs []string 37 } 38 39 action func(obj *unstructured.Unstructured) error 40 ) 41 42 // funcMap contains the helper functions used during templating 43 var funcMap template.FuncMap = map[string]any{ 44 "contains": strings.Contains, 45 "nindent": nindent, 46 "multiLineIndent": multiLineIndent, 47 } 48 49 func NewYAMLApplier(client crtpkg.Client, namespaceOverride string) *YAMLApplier { 50 return &YAMLApplier{ 51 client: client, 52 objects: []unstructured.Unstructured{}, 53 namespaceOverride: namespaceOverride, 54 objectResultMsgs: []string{}, 55 } 56 } 57 58 // Objects is the list of objects created using the ApplyX methods 59 func (y *YAMLApplier) Objects() []unstructured.Unstructured { 60 return y.objects 61 } 62 63 // ObjectResultMsgs is the list of object result messages using the ApplyX methods 64 func (y *YAMLApplier) ObjectResultMsgs() []string { 65 return y.objectResultMsgs 66 } 67 68 // ApplyD applies all YAML files in a directory to Kubernetes 69 func (y *YAMLApplier) ApplyD(directory string) error { 70 files, err := os.ReadDir(directory) 71 if err != nil { 72 return err 73 } 74 filteredFiles := filterYamlExt(files) 75 if len(filteredFiles) < 1 { 76 return fmt.Errorf("no files passed to apply: %s", directory) 77 } 78 for _, file := range filteredFiles { 79 filePath := path.Join(directory, file.Name()) 80 if err = y.ApplyF(filePath); err != nil { 81 return err 82 } 83 } 84 85 return nil 86 } 87 88 // ApplyDT applies a directory of file templates to Kubernetes 89 func (y *YAMLApplier) ApplyDT(directory string, args any) error { 90 files, err := os.ReadDir(directory) 91 if err != nil { 92 return err 93 } 94 filteredFiles := filterYamlExt(files) 95 if len(filteredFiles) < 1 { 96 return fmt.Errorf("no files passed to apply: %s", directory) 97 } 98 for _, file := range filteredFiles { 99 filePath := path.Join(directory, file.Name()) 100 if err = y.ApplyFT(filePath, args); err != nil { 101 return err 102 } 103 } 104 105 return nil 106 } 107 108 func (y *YAMLApplier) ApplyBT(b []byte, args any) error { 109 return y.doTemplatedBytesAction(b, y.applyAction, args) 110 } 111 112 // ApplyF applies a file spec to Kubernetes 113 func (y *YAMLApplier) ApplyF(filePath string) error { 114 return y.doFileAction(filePath, y.applyAction) 115 } 116 117 // ApplyS applies a spec to Kubernetes via a string 118 func (y *YAMLApplier) ApplyS(spec string) error { 119 return y.doStringAction(spec, y.applyAction) 120 } 121 122 // ApplyFT applies a file template spec (go text.template) to Kubernetes 123 func (y *YAMLApplier) ApplyFT(filePath string, args any) error { 124 return y.doTemplatedFileAction(filePath, y.applyAction, args) 125 } 126 127 // ApplyFTDefaultConfig calls ApplyFT with rest client from the default config 128 func (y *YAMLApplier) ApplyFTDefaultConfig(filePath string, args any) error { 129 config, err := GetKubeConfig() 130 if err != nil { 131 return err 132 } 133 client, err := crtpkg.New(config, crtpkg.Options{}) 134 if err != nil { 135 return err 136 } 137 y.client = client 138 return y.ApplyFT(filePath, args) 139 } 140 141 // DeleteF deletes a file spec from Kubernetes 142 func (y *YAMLApplier) DeleteF(filePath string) error { 143 return y.doFileAction(filePath, y.deleteAction) 144 } 145 146 // DeleteS deletes resources in a spec from Kubernetes via a string 147 func (y *YAMLApplier) DeleteS(spec string) error { 148 return y.doStringAction(spec, y.deleteAction) 149 } 150 151 // DeleteFWithDependents deletes a file spec from Kubernetes along with other dependent objects in the background 152 func (y *YAMLApplier) DeleteFWithDependents(filePath string) error { 153 return y.doFileAction(filePath, y.deleteActionWithDependents) 154 } 155 156 // DeleteFT deletes a file template spec (go text.template) to Kubernetes 157 func (y *YAMLApplier) DeleteFT(filePath string, args any) error { 158 return y.doTemplatedFileAction(filePath, y.deleteAction, args) 159 } 160 161 // DeleteFTDefaultConfig calls deleteFT with rest client from the default config 162 func (y *YAMLApplier) DeleteFTDefaultConfig(filePath string, args any) error { 163 config, err := GetKubeConfig() 164 if err != nil { 165 return err 166 } 167 client, err := crtpkg.New(config, crtpkg.Options{}) 168 if err != nil { 169 return err 170 } 171 y.client = client 172 return y.DeleteFT(filePath, args) 173 } 174 175 // applyAction creates a merge patch of the object with the server object 176 func (y *YAMLApplier) applyAction(obj *unstructured.Unstructured) error { 177 var ns = strings.TrimSpace(y.namespaceOverride) 178 if len(ns) > 0 { 179 obj.SetNamespace(ns) 180 } 181 182 // Struct to store a copy of a client field 183 type clientField struct { 184 name string 185 nestedCopy interface{} 186 typeOf string 187 } 188 189 // Make a nested copy of each client field. 190 var clientFields []clientField 191 var err error 192 for fieldName, fieldObj := range obj.Object { 193 if fieldName == "kind" || fieldName == "apiVersion" { 194 continue 195 } 196 cf := clientField{} 197 cf.name = fieldName 198 cf.nestedCopy, _, err = unstructured.NestedFieldCopy(obj.Object, fieldName) 199 if err != nil { 200 return err 201 } 202 cf.typeOf = reflect.TypeOf(fieldObj).String() 203 clientFields = append(clientFields, cf) 204 } 205 206 err = kubectlutil.SetLastAppliedConfigurationAnnotation(obj) 207 if err != nil { 208 return err 209 } 210 211 result, err := controllerruntime.CreateOrUpdate(context.TODO(), y.client, obj, func() error { 212 // For each nested copy of a client field, determine if it needs to be added or merged 213 // with the server. 214 for _, clientField := range clientFields { 215 216 serverField, _, err := unstructured.NestedFieldCopy(obj.Object, clientField.name) 217 if err != nil { 218 return err 219 } 220 221 // See if merge needed on objects of type map[string]interface {} 222 if clientField.typeOf == "map[string]interface {}" { 223 if serverField != nil { 224 merge(serverField.(map[string]interface{}), clientField.nestedCopy.(map[string]interface{})) 225 } 226 } 227 228 // For objects of type []interface{}, e.g. secrets or imagePullSecrets, a replace will be 229 // done. This appears to be consistent with the behavior of kubectl. 230 if clientField.typeOf == "[]interface {}" { 231 serverField = clientField.nestedCopy 232 } 233 234 // If serverSpec is nil, then the clientSpec field is being added 235 if serverField == nil { 236 serverField = clientField.nestedCopy 237 } 238 239 // Set the resulting value in the server object 240 err = unstructured.SetNestedField(obj.Object, serverField, clientField.name) 241 if err != nil { 242 return err 243 } 244 } 245 // Delete any keys in server obj not included in the client fields. 246 for key := range obj.Object { 247 if key == "kind" || key == "apiVersion" { 248 continue 249 } 250 keyFound := false 251 for _, clientField := range clientFields { 252 if clientField.name == key { 253 keyFound = true 254 break 255 } 256 } 257 if !keyFound { 258 err = unstructured.SetNestedField(obj.Object, nil, key) 259 if err != nil { 260 return err 261 } 262 } 263 } 264 return nil 265 }) 266 if err != nil { 267 return err 268 } 269 y.objects = append(y.objects, *obj) 270 271 // Add an informational message (to mimic what you see on a kubectl apply) 272 group := obj.GetObjectKind().GroupVersionKind().Group 273 if len(group) > 0 { 274 group = fmt.Sprintf(".%s", group) 275 } 276 y.objectResultMsgs = append(y.objectResultMsgs, fmt.Sprintf("%s%s/%s %s", obj.GetKind(), group, obj.GetName(), string(result))) 277 278 return nil 279 } 280 281 // deleteAction deletes the object from the server 282 func (y *YAMLApplier) deleteAction(obj *unstructured.Unstructured) error { 283 return y.execDeleteAction(obj, metav1.DeletePropagationOrphan) 284 } 285 286 // deleteAction deletes the object from the server 287 func (y *YAMLApplier) deleteActionWithDependents(obj *unstructured.Unstructured) error { 288 return y.execDeleteAction(obj, metav1.DeletePropagationBackground) 289 } 290 291 func (y *YAMLApplier) execDeleteAction(obj *unstructured.Unstructured, propagationPolicy metav1.DeletionPropagation) error { 292 var ns = strings.TrimSpace(y.namespaceOverride) 293 if len(ns) > 0 { 294 obj.SetNamespace(ns) 295 } 296 deleteOptions := &crtpkg.DeleteOptions{ 297 PropagationPolicy: &propagationPolicy, 298 } 299 300 if err := y.client.Delete(context.TODO(), obj, deleteOptions); err != nil { 301 if !errors.IsNotFound(err) { 302 return err 303 } 304 } 305 return nil 306 } 307 308 // doFileAction runs the action against a file 309 func (y *YAMLApplier) doFileAction(filePath string, f action) error { 310 file, err := os.Open(filePath) 311 if err != nil { 312 return err 313 } 314 defer file.Close() 315 return y.doAction(bufio.NewReader(file), f) 316 } 317 318 // doStringAction runs the action against a string 319 func (y *YAMLApplier) doStringAction(spec string, f action) error { 320 return y.doAction(bufio.NewReader(strings.NewReader(spec)), f) 321 } 322 323 // doTemplatedFileAction runs the action against a template file 324 func (y *YAMLApplier) doTemplatedFileAction(filePath string, f action, args any) error { 325 templateName := path.Base(filePath) 326 tmpl, err := template.New(templateName). 327 Option("missingkey=error"). // Treat any missing keys as errors 328 Funcs(funcMap). 329 ParseFiles(filePath) 330 if err != nil { 331 return err 332 } 333 buffer := &bytes.Buffer{} 334 if err = tmpl.Execute(buffer, args); err != nil { 335 return err 336 } 337 return y.doAction(bufio.NewReader(buffer), f) 338 } 339 340 func (y *YAMLApplier) doTemplatedBytesAction(b []byte, f action, args any) error { 341 tmpl, err := template.New("bytetemplate"). 342 Option("missingkey=error"). // Treat any missing keys as errors 343 Funcs(funcMap). 344 Parse(string(b)) 345 if err != nil { 346 return err 347 } 348 buffer := &bytes.Buffer{} 349 if err = tmpl.Execute(buffer, args); err != nil { 350 return err 351 } 352 return y.doAction(bufio.NewReader(buffer), f) 353 } 354 355 // doAction executes the action on a YAML reader 356 func (y *YAMLApplier) doAction(reader *bufio.Reader, f action) error { 357 objs, err := Unmarshall(reader) 358 if err != nil { 359 return err 360 } 361 362 for i := range objs { 363 if err := f(&objs[i]); err != nil { 364 return err 365 } 366 } 367 return nil 368 } 369 370 // Unmarshall a reader containing YAML to a list of unstructured objects 371 func Unmarshall(reader *bufio.Reader) ([]unstructured.Unstructured, error) { 372 buffer := bytes.Buffer{} 373 objs := []unstructured.Unstructured{} 374 375 flushBuffer := func() error { 376 if buffer.Len() < 1 { 377 return nil 378 } 379 obj := unstructured.Unstructured{Object: map[string]interface{}{}} 380 yamlBytes := buffer.Bytes() 381 if err := yaml.Unmarshal(yamlBytes, &obj); err != nil { 382 return err 383 } 384 if len(obj.Object) > 0 { 385 objs = append(objs, obj) 386 } 387 buffer.Reset() 388 return nil 389 } 390 391 eofReached := false 392 for { 393 // Read the file line by line 394 line, err := reader.ReadBytes('\n') 395 if err != nil { 396 if err == io.EOF { 397 // EOF has been reached, but there may be some line data to process 398 eofReached = true 399 } else { 400 return objs, err 401 } 402 } 403 lineStr := string(line) 404 // Flush buffer at document break 405 if strings.TrimSpace(lineStr) == sep { 406 if err = flushBuffer(); err != nil { 407 return objs, err 408 } 409 } else { 410 // Save line to buffer 411 if !strings.HasPrefix(lineStr, "#") && len(strings.TrimSpace(lineStr)) > 0 { 412 if _, err := buffer.Write(line); err != nil { 413 return objs, err 414 } 415 } 416 } 417 // if EOF, flush the buffer and return the objs 418 if eofReached { 419 flushErr := flushBuffer() 420 return objs, flushErr 421 } 422 } 423 } 424 425 // merge keys from m2 into m1, overwriting existing keys of m1. 426 func merge(m1, m2 map[string]interface{}) { 427 for k, v := range m2 { 428 m1[k] = v 429 } 430 } 431 432 // DeleteAll deletes all objects created by the applier 433 // If you are using a YAMLApplier in a temporary context, please use defer y.DeleteAll() 434 // to clean up resources when you are done. 435 func (y *YAMLApplier) DeleteAll() error { 436 for i := range y.objects { 437 if err := y.client.Delete(context.TODO(), &y.objects[i]); err != nil { 438 if !errors.IsNotFound(err) { 439 return err 440 } 441 } 442 } 443 444 y.objects = []unstructured.Unstructured{} 445 return nil 446 } 447 448 // isYamlExt checks if a file has a YAML extension. 449 func isYamlExt(fileName string) bool { 450 ext := path.Ext(fileName) 451 return ext == ".yml" || ext == ".yaml" 452 } 453 454 func filterYamlExt(files []os.DirEntry) []os.DirEntry { 455 res := []os.DirEntry{} 456 for _, file := range files { 457 if !file.IsDir() && isYamlExt(file.Name()) { 458 res = append(res, file) 459 } 460 } 461 462 return res 463 } 464 465 func nindent(indent int, s string) string { 466 spacing := strings.Repeat(" ", indent) 467 split := strings.FieldsFunc(s, func(r rune) bool { 468 switch r { 469 case '\n', '\v', '\f', '\r': 470 return true 471 default: 472 return false 473 } 474 }) 475 sb := strings.Builder{} 476 for i := 0; i < len(split); i++ { 477 segment := split[i] 478 sb.WriteString(spacing) 479 sb.WriteString(strings.TrimSpace(segment)) 480 if i < len(split)-1 { 481 sb.WriteRune('\n') 482 } 483 } 484 485 return sb.String() 486 } 487 488 func multiLineIndent(indentNum int, aff string) string { 489 var b = make([]byte, indentNum) 490 for i := 0; i < indentNum; i++ { 491 b[i] = 32 492 } 493 lines := strings.SplitAfter(aff, "\n") 494 for i, line := range lines { 495 lines[i] = string(b) + line 496 } 497 return strings.Join(lines[:], "") 498 }