diff --git a/cmd/api/app/api.go b/cmd/api/app/api.go index 19321c7c..3616d6b1 100644 --- a/cmd/api/app/api.go +++ b/cmd/api/app/api.go @@ -16,6 +16,7 @@ import ( // Importing route packages forces route registration _ "github.com/karmada-io/dashboard/cmd/api/app/routes/auth" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/cluster" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/configmap" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/deployment" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/namespace" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/overview" diff --git a/cmd/api/app/routes/configmap/handler.go b/cmd/api/app/routes/configmap/handler.go new file mode 100644 index 00000000..3e0cc948 --- /dev/null +++ b/cmd/api/app/routes/configmap/handler.go @@ -0,0 +1,40 @@ +package service + +import ( + "github.com/gin-gonic/gin" + "github.com/karmada-io/dashboard/cmd/api/app/router" + "github.com/karmada-io/dashboard/cmd/api/app/types/common" + "github.com/karmada-io/dashboard/pkg/client" + "github.com/karmada-io/dashboard/pkg/resource/configmap" +) + +func handleGetConfigMap(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + dataSelect := common.ParseDataSelectPathParameter(c) + nsQuery := common.ParseNamespacePathParameter(c) + result, err := configmap.GetConfigMapList(k8sClient, nsQuery, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetConfigMapDetail(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + namespace := c.Param("namespace") + name := c.Param("name") + result, err := configmap.GetConfigMapDetail(k8sClient, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.V1() + r.GET("/configmap", handleGetConfigMap) + r.GET("/configmap/:namespace", handleGetConfigMap) + r.GET("/configmap/:namespace/:name", handleGetConfigMapDetail) +} diff --git a/pkg/dataselect/dataselectquery.go b/pkg/dataselect/dataselectquery.go index 7fdf8da6..f7552bf0 100644 --- a/pkg/dataselect/dataselectquery.go +++ b/pkg/dataselect/dataselectquery.go @@ -53,6 +53,9 @@ var NoFilter = &FilterQuery{ FilterByList: []FilterBy{}, } +// NoDataSelect is an option for no data select (same data will be returned). +var NoDataSelect = NewDataSelectQuery(NoPagination, NoSort, NoFilter) + // NewDataSelectQuery creates DataSelectQuery object from simpler data select queries. func NewDataSelectQuery(paginationQuery *PaginationQuery, sortQuery *SortQuery, filterQuery *FilterQuery) *DataSelectQuery { return &DataSelectQuery{ diff --git a/pkg/resource/common/resourcechannels.go b/pkg/resource/common/resourcechannels.go index 5712ef85..117a6201 100644 --- a/pkg/resource/common/resourcechannels.go +++ b/pkg/resource/common/resourcechannels.go @@ -335,6 +335,34 @@ type ConfigMapListChannel struct { Error chan error } +// GetConfigMapListChannel returns a pair of channels to a ConfigMap list and errors that both must be read +// numReads times. +func GetConfigMapListChannel(client client.Interface, nsQuery *NamespaceQuery, + numReads int) ConfigMapListChannel { + + channel := ConfigMapListChannel{ + List: make(chan *v1.ConfigMapList, numReads), + Error: make(chan error, numReads), + } + + go func() { + list, err := client.CoreV1().ConfigMaps(nsQuery.ToRequestParam()).List(context.TODO(), helpers.ListEverything) + var filteredItems []v1.ConfigMap + for _, item := range list.Items { + if nsQuery.Matches(item.ObjectMeta.Namespace) { + filteredItems = append(filteredItems, item) + } + } + list.Items = filteredItems + for i := 0; i < numReads; i++ { + channel.List <- list + channel.Error <- err + } + }() + + return channel +} + // SecretListChannel is a list and error channels to Secrets. type SecretListChannel struct { List chan *v1.SecretList diff --git a/pkg/resource/configmap/common.go b/pkg/resource/configmap/common.go new file mode 100644 index 00000000..19621670 --- /dev/null +++ b/pkg/resource/configmap/common.go @@ -0,0 +1,40 @@ +package configmap + +import ( + "github.com/karmada-io/dashboard/pkg/dataselect" + api "k8s.io/api/core/v1" +) + +// The code below allows to perform complex data section on []api.ConfigMap + +type ConfigMapCell api.ConfigMap + +func (self ConfigMapCell) GetProperty(name dataselect.PropertyName) dataselect.ComparableValue { + switch name { + case dataselect.NameProperty: + return dataselect.StdComparableString(self.ObjectMeta.Name) + case dataselect.CreationTimestampProperty: + return dataselect.StdComparableTime(self.ObjectMeta.CreationTimestamp.Time) + case dataselect.NamespaceProperty: + return dataselect.StdComparableString(self.ObjectMeta.Namespace) + default: + // if name is not supported then just return a constant dummy value, sort will have no effect. + return nil + } +} + +func toCells(std []api.ConfigMap) []dataselect.DataCell { + cells := make([]dataselect.DataCell, len(std)) + for i := range std { + cells[i] = ConfigMapCell(std[i]) + } + return cells +} + +func fromCells(cells []dataselect.DataCell) []api.ConfigMap { + std := make([]api.ConfigMap, len(cells)) + for i := range std { + std[i] = api.ConfigMap(cells[i].(ConfigMapCell)) + } + return std +} diff --git a/pkg/resource/configmap/detail.go b/pkg/resource/configmap/detail.go new file mode 100644 index 00000000..c4159956 --- /dev/null +++ b/pkg/resource/configmap/detail.go @@ -0,0 +1,41 @@ +package configmap + +import ( + "context" + "log" + + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// ConfigMapDetail API resource provides mechanisms to inject containers with configuration data while keeping +// containers agnostic of Kubernetes +type ConfigMapDetail struct { + // Extends list item structure. + ConfigMap `json:",inline"` + + // Data contains the configuration data. + // Each key must be a valid DNS_SUBDOMAIN with an optional leading dot. + Data map[string]string `json:"data,omitempty"` +} + +// GetConfigMapDetail returns detailed information about a config map +func GetConfigMapDetail(client kubernetes.Interface, namespace, name string) (*ConfigMapDetail, error) { + log.Printf("Getting details of %s config map in %s namespace", name, namespace) + + rawConfigMap, err := client.CoreV1().ConfigMaps(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + + if err != nil { + return nil, err + } + + return getConfigMapDetail(rawConfigMap), nil +} + +func getConfigMapDetail(rawConfigMap *v1.ConfigMap) *ConfigMapDetail { + return &ConfigMapDetail{ + ConfigMap: toConfigMap(rawConfigMap.ObjectMeta), + Data: rawConfigMap.Data, + } +} diff --git a/pkg/resource/configmap/detail_test.go b/pkg/resource/configmap/detail_test.go new file mode 100644 index 00000000..a36b2502 --- /dev/null +++ b/pkg/resource/configmap/detail_test.go @@ -0,0 +1,37 @@ +package configmap + +import ( + "github.com/karmada-io/dashboard/pkg/common/types" + "reflect" + "testing" + + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetConfigMapDetail(t *testing.T) { + cases := []struct { + configMaps *v1.ConfigMap + expected *ConfigMapDetail + }{ + { + &v1.ConfigMap{ + Data: map[string]string{"app": "my-name"}, ObjectMeta: metaV1.ObjectMeta{Name: "foo"}, + }, + &ConfigMapDetail{ + ConfigMap: ConfigMap{ + TypeMeta: types.TypeMeta{Kind: "configmap"}, + ObjectMeta: types.ObjectMeta{Name: "foo"}, + }, + Data: map[string]string{"app": "my-name"}, + }, + }, + } + for _, c := range cases { + actual := getConfigMapDetail(c.configMaps) + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("getConfigMapDetail(%#v) == \n%#v\nexpected \n%#v\n", + c.configMaps, actual, c.expected) + } + } +} diff --git a/pkg/resource/configmap/list.go b/pkg/resource/configmap/list.go new file mode 100644 index 00000000..89a8eafb --- /dev/null +++ b/pkg/resource/configmap/list.go @@ -0,0 +1,80 @@ +package configmap + +import ( + "github.com/karmada-io/dashboard/pkg/common/errors" + "github.com/karmada-io/dashboard/pkg/common/types" + "github.com/karmada-io/dashboard/pkg/dataselect" + "github.com/karmada-io/dashboard/pkg/resource/common" + "log" + + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// ConfigMapList contains a list of Config Maps in the cluster. +type ConfigMapList struct { + ListMeta types.ListMeta `json:"listMeta"` + + // Unordered list of Config Maps + Items []ConfigMap `json:"items"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// ConfigMap API resource provides mechanisms to inject containers with configuration data while keeping +// containers agnostic of Kubernetes +type ConfigMap struct { + ObjectMeta types.ObjectMeta `json:"objectMeta"` + TypeMeta types.TypeMeta `json:"typeMeta"` +} + +// GetConfigMapList returns a list of all ConfigMaps in the cluster. +func GetConfigMapList(client kubernetes.Interface, nsQuery *common.NamespaceQuery, dsQuery *dataselect.DataSelectQuery) (*ConfigMapList, error) { + log.Printf("Getting list config maps in the namespace %s", nsQuery.ToRequestParam()) + channels := &common.ResourceChannels{ + ConfigMapList: common.GetConfigMapListChannel(client, nsQuery, 1), + } + + return GetConfigMapListFromChannels(channels, dsQuery) +} + +// GetConfigMapListFromChannels returns a list of all Config Maps in the cluster reading required resource list once from the channels. +func GetConfigMapListFromChannels(channels *common.ResourceChannels, dsQuery *dataselect.DataSelectQuery) (*ConfigMapList, error) { + configMaps := <-channels.ConfigMapList.List + err := <-channels.ConfigMapList.Error + nonCriticalErrors, criticalError := errors.ExtractErrors(err) + if criticalError != nil { + return nil, criticalError + } + + result := toConfigMapList(configMaps.Items, nonCriticalErrors, dsQuery) + + return result, nil +} + +func toConfigMap(meta metaV1.ObjectMeta) ConfigMap { + return ConfigMap{ + ObjectMeta: types.NewObjectMeta(meta), + TypeMeta: types.NewTypeMeta(types.ResourceKindConfigMap), + } +} + +func toConfigMapList(configMaps []v1.ConfigMap, nonCriticalErrors []error, dsQuery *dataselect.DataSelectQuery) *ConfigMapList { + result := &ConfigMapList{ + Items: make([]ConfigMap, 0), + ListMeta: types.ListMeta{TotalItems: len(configMaps)}, + Errors: nonCriticalErrors, + } + + configMapCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(configMaps), dsQuery) + configMaps = fromCells(configMapCells) + result.ListMeta = types.ListMeta{TotalItems: filteredTotal} + + for _, item := range configMaps { + result.Items = append(result.Items, toConfigMap(item.ObjectMeta)) + } + + return result +}