diff --git a/.gitignore b/.gitignore index c2aff810..9e5f80da 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ docker/ # build artifacts /cluster-registry* -kubeconfig +kubeconfig* .dynamodb .hack* diff --git a/cmd/client/client.go b/cmd/client/client.go index 57bf43c7..ae5821f1 100644 --- a/cmd/client/client.go +++ b/cmd/client/client.go @@ -15,6 +15,7 @@ package main import ( "encoding/base64" "flag" + "fmt" registryv1alpha1 "github.com/adobe/cluster-registry/pkg/api/registry/v1alpha1" "github.com/adobe/cluster-registry/pkg/client/controllers" "github.com/adobe/cluster-registry/pkg/config" @@ -22,6 +23,7 @@ import ( "github.com/adobe/cluster-registry/pkg/sqs" "github.com/prometheus/client_golang/prometheus/promhttp" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" "k8s.io/client-go/tools/leaderelection/resourcelock" "os" @@ -148,20 +150,10 @@ func main() { } if err = (&controllers.ServiceMetadataWatcherReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("ServiceMetadataWatcher"), - Scheme: mgr.GetScheme(), - WatchedGVKs: func(cfg configv1.ClientConfig) []schema.GroupVersionKind { - var GVKs []schema.GroupVersionKind - for _, gvk := range cfg.ServiceMetadata.WatchedGVKs { - GVKs = append(GVKs, schema.GroupVersionKind{ - Group: gvk.Group, - Version: gvk.Version, - Kind: gvk.Kind, - }) - } - return GVKs - }(clientConfig), + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("ServiceMetadataWatcher"), + Scheme: mgr.GetScheme(), + WatchedGVKs: loadWatchedGVKs(clientConfig), ServiceIdAnnotation: clientConfig.ServiceMetadata.ServiceIdAnnotation, }).SetupWithManager(ctx, mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServiceMetadataWatcher") @@ -204,6 +196,27 @@ func main() { } } +func loadWatchedGVKs(cfg configv1.ClientConfig) []schema.GroupVersionKind { + availableGVKs, err := getAvailableGVKs() + if err != nil { + return []schema.GroupVersionKind{} + } + var GVKs []schema.GroupVersionKind + for _, gvk := range cfg.ServiceMetadata.WatchedGVKs { + gvk := schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } + if _, found := availableGVKs[gvk]; !found { + setupLog.Info("GVK not installed in the cluster", "gvk", gvk) + continue + } + GVKs = append(GVKs, gvk) + } + return GVKs +} + func apply(configFile string, clientConfigDefaults *configv1.ClientConfig) (ctrl.Options, configv1.ClientConfig, error) { options, cfg, err := configv1.Load(scheme, configFile, clientConfigDefaults) if err != nil { @@ -218,3 +231,30 @@ func apply(configFile string, clientConfigDefaults *configv1.ClientConfig) (ctrl return options, cfg, nil } + +func getAvailableGVKs() (map[schema.GroupVersionKind]bool, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(ctrl.GetConfigOrDie()) + if err != nil { + return nil, fmt.Errorf("unable to create discovery client: %w", err) + } + + _, availableResources, err := discoveryClient.ServerGroupsAndResources() + if err != nil { + return nil, fmt.Errorf("unable to get available API resources: %w", err) + } + + availableGVKs := make(map[schema.GroupVersionKind]bool) + for _, list := range availableResources { + groupVersion, _ := schema.ParseGroupVersion(list.GroupVersion) + for _, resource := range list.APIResources { + gvk := schema.GroupVersionKind{ + Group: groupVersion.Group, + Version: groupVersion.Version, + Kind: resource.Kind, + } + availableGVKs[gvk] = true + } + } + + return availableGVKs, nil +} diff --git a/pkg/client/controllers/servicemetadatawatcher_controller.go b/pkg/client/controllers/servicemetadatawatcher_controller.go index 033f0d96..9b3f4edd 100644 --- a/pkg/client/controllers/servicemetadatawatcher_controller.go +++ b/pkg/client/controllers/servicemetadatawatcher_controller.go @@ -131,7 +131,14 @@ func (r *ServiceMetadataWatcherReconciler) Reconcile(ctx context.Context, req ct for _, field := range wso.WatchedFields { // TODO: validate field Source & Destination somewhere - value, found, err := getNestedString(obj.Object, strings.Split(field.Source, ".")) + path, err := parsePath(field.Source) + if err != nil { + // TODO: update status with error + log.Error(err, "cannot parse path", "field", field.Source) + continue + } + + value, found, err := getNestedString(obj.Object, path) if err != nil { // TODO: update status with error log.Error(err, "cannot get field", "field", field.Source) @@ -165,6 +172,15 @@ func (r *ServiceMetadataWatcherReconciler) Reconcile(ctx context.Context, req ct return noRequeue() } +func parsePath(path string) ([]string, error) { + re := regexp.MustCompile(`(?:[^.\[]+|\[.*?\])`) + p := re.FindAllString(path, -1) + if len(p) > 0 { + return p, nil + } + return nil, fmt.Errorf("invalid path") +} + // SetupWithManager sets up the controller with the Manager. func (r *ServiceMetadataWatcherReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { options := controller.Options{MaxConcurrentReconciles: 10} @@ -318,32 +334,40 @@ func createServiceMetadataPatch(serviceId string, namespace string, field string } // getNestedString returns the value of a nested field in the provided object -// path is a list of keys separated by dots, e.g. "spec.template.spec.containers[0].image" -// if the field is a slice, the last key must be in the form of "key[index]" +// path is a list of strings separated by dots, e.g. "spec.template.spec.containers[0].image" +// If the field is a slice, the last string must be in the form of "field[index]", where index is an integer +// If the field is a map, the last string must be in the form of "field[key]", where key is a string func getNestedString(object interface{}, path []string) (string, bool, error) { - re := regexp.MustCompile(`^(.*)\[(\d+|[a-z]+)]$`) var cpath []string for i, key := range path { - m := re.FindStringSubmatch(key) - if len(m) > 0 { - cpath = append(cpath, m[1]) - slice, found, err := unstructured.NestedSlice(object.(map[string]interface{}), cpath...) - if !found || err != nil { - return "", false, err - } - index, err := strconv.Atoi(m[2]) - if err != nil && m[2] != "last" { - return "", false, fmt.Errorf("invalid array index: %s", m[2]) - } - if m[2] == "last" { - index = len(slice) - 1 + if strings.HasPrefix(key, "[") && strings.HasSuffix(key, "]") { + k := key[1 : len(key)-1] + + sliceObj, found, err := unstructured.NestedSlice(object.(map[string]interface{}), cpath...) + if err == nil && found { + index, err := strconv.Atoi(k) + if err != nil { + return "", false, fmt.Errorf("invalid array index: %s", k) + } + if index < 0 { + index = len(sliceObj) + index + } + if index >= len(sliceObj) || index < 0 { + return "", false, fmt.Errorf("index out of range") + } + return getNestedString(sliceObj[index], path[i+1:]) } - if len(slice) <= index { - return "", false, fmt.Errorf("index out of range") + + mapObj, found, err := unstructured.NestedMap(object.(map[string]interface{}), cpath...) + if err == nil && found { + if _, ok := mapObj[k]; !ok { + return "", false, fmt.Errorf("key not found: %s", k) + } + return getNestedString(mapObj[k], path[i+1:]) } - return getNestedString(slice[index], path[i+1:]) + } else { + cpath = append(cpath, key) } - cpath = append(cpath, key) } if reflect.TypeOf(object).String() == "string" { @@ -364,7 +388,15 @@ func getNestedString(object interface{}, path []string) (string, bool, error) { return strconv.FormatBool(boolVal), found, err } - // TODO: handle additional types? + intVal, found, err := unstructured.NestedInt64(object.(map[string]interface{}), path...) + if found && err == nil { + return strconv.FormatInt(intVal, 10), found, err + } + + floatVal, found, err := unstructured.NestedFloat64(object.(map[string]interface{}), path...) + if found && err == nil { + return strconv.FormatFloat(floatVal, 'g', -1, 64), found, err + } return "", found, fmt.Errorf("invalid field type") }