package api import ( "fmt" "strconv" "strings" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/logging" imageutils "github.com/kyverno/kyverno/pkg/utils/image" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) type ImageInfo struct { imageutils.ImageInfo // Pointer is the path to the image object in the resource Pointer string `json:"jsonPointer"` } var ( podExtractors = BuildStandardExtractors("spec") podControllerExtractors = BuildStandardExtractors("spec", "template", "spec") cronjobControllerExtractors = BuildStandardExtractors("spec", "jobTemplate", "spec", "template", "spec") registeredExtractors = map[string][]imageExtractor{ "Pod": podExtractors, "DaemonSet": podControllerExtractors, "Deployment": podControllerExtractors, "ReplicaSet": podControllerExtractors, "ReplicationController": podControllerExtractors, "StatefulSet": podControllerExtractors, "CronJob": cronjobControllerExtractors, "Job": podControllerExtractors, } ) type imageExtractor struct { Fields []string Key string Value string Name string JMESPath string } func (i *imageExtractor) ExtractFromResource(resource interface{}, cfg config.Configuration) (map[string]ImageInfo, error) { imageInfo := map[string]ImageInfo{} if err := extract(resource, []string{}, i.Key, i.Value, i.Fields, i.JMESPath, &imageInfo, cfg); err != nil { return nil, err } return imageInfo, nil } func extract( obj interface{}, path []string, keyPath string, valuePath string, fields []string, jmesPath string, imageInfos *map[string]ImageInfo, cfg config.Configuration, ) error { if obj == nil { return nil } if len(fields) > 0 && fields[0] == "*" { switch typedObj := obj.(type) { case []interface{}: for i, v := range typedObj { if err := extract(v, append(path, strconv.Itoa(i)), keyPath, valuePath, fields[1:], jmesPath, imageInfos, cfg); err != nil { return err } } case map[string]interface{}: for i, v := range typedObj { if err := extract(v, append(path, i), keyPath, valuePath, fields[1:], jmesPath, imageInfos, cfg); err != nil { return err } } case interface{}: return fmt.Errorf("invalid type") } return nil } output, ok := obj.(map[string]interface{}) if !ok { return fmt.Errorf("invalid image config") } if len(fields) == 0 { pointer := fmt.Sprintf("/%s/%s", strings.Join(path, "/"), valuePath) key := pointer if keyPath != "" { key, ok = output[keyPath].(string) if !ok { return fmt.Errorf("invalid key") } } value, ok := output[valuePath].(string) if !ok || strings.TrimSpace(value) == "" { // the image may not be present logging.V(4).Info("image information is not present", "pointer", pointer) return nil } if jmesPath != "" { // TODO: should be injected jp := jmespath.New(cfg) q, err := jp.Query(jmesPath) if err != nil { return fmt.Errorf("invalid jmespath %s: %v", jmesPath, err) } result, err := q.Search(value) if err != nil { return fmt.Errorf("failed to apply jmespath %s: %v", jmesPath, err) } resultStr, ok := result.(string) if !ok { return fmt.Errorf("jmespath %s must produce a string, but produced %v", jmesPath, result) } value = resultStr } if imageInfo, err := imageutils.GetImageInfo(value, cfg); err != nil { return fmt.Errorf("invalid image '%s' (%s)", value, err.Error()) } else { (*imageInfos)[key] = ImageInfo{*imageInfo, pointer} } return nil } currentPath := fields[0] return extract(output[currentPath], append(path, currentPath), keyPath, valuePath, fields[1:], jmesPath, imageInfos, cfg) } func BuildStandardExtractors(tags ...string) []imageExtractor { var extractors []imageExtractor for _, tag := range []string{"initContainers", "containers", "ephemeralContainers"} { var t []string t = append(t, tags...) t = append(t, tag) t = append(t, "*") extractors = append(extractors, imageExtractor{Fields: t, Key: "name", Value: "image", Name: tag}) } return extractors } func lookupImageExtractor(kind string, configs kyvernov1.ImageExtractorConfigs) []imageExtractor { if configs != nil { if extractorConfigs, ok := configs[kind]; ok { extractors := []imageExtractor{} for _, c := range extractorConfigs { fields := func(input []string) []string { output := []string{} for _, i := range input { o := strings.Trim(i, " ") if o != "" { output = append(output, o) } } return output }(strings.Split(c.Path, "/")) name := c.Name if name == "" { name = "custom" } value := c.Value if value == "" { value = fields[len(fields)-1] fields = fields[:len(fields)-1] } extractors = append(extractors, imageExtractor{ Fields: fields, Key: c.Key, Name: name, Value: value, JMESPath: c.JMESPath, }) } return extractors } } return registeredExtractors[kind] } func ExtractImagesFromResource(resource unstructured.Unstructured, configs kyvernov1.ImageExtractorConfigs, cfg config.Configuration) (map[string]map[string]ImageInfo, error) { infos := map[string]map[string]ImageInfo{} extractors := lookupImageExtractor(resource.GetKind(), configs) if extractors != nil && len(extractors) == 0 { return nil, fmt.Errorf("no extractors found for %s", resource.GetKind()) } for _, extractor := range extractors { if infoMap, err := extractor.ExtractFromResource(resource.Object, cfg); err != nil { return nil, err } else if len(infoMap) > 0 { infos[extractor.Name] = infoMap } } return infos, nil }