diff --git a/apis/project.cattle.io/v3/schema/schema.go b/apis/project.cattle.io/v3/schema/schema.go index f8b8cebe..4626ca89 100644 --- a/apis/project.cattle.io/v3/schema/schema.go +++ b/apis/project.cattle.io/v3/schema/schema.go @@ -10,6 +10,7 @@ import ( "github.com/rancher/types/factory" "github.com/rancher/types/mapper" "k8s.io/api/apps/v1beta2" + autoscaling "k8s.io/api/autoscaling/v2beta2" batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" "k8s.io/api/core/v1" @@ -44,7 +45,8 @@ var ( Init(workloadTypes). Init(appTypes). Init(pipelineTypes). - Init(monitoringTypes) + Init(monitoringTypes). + Init(autoscalingTypes) ) func configMapTypes(schemas *types.Schemas) *types.Schemas { @@ -1020,3 +1022,69 @@ func monitoringTypes(schemas *types.Schemas) *types.Schemas { ). MustImport(&Version, monitoringv1.Alertmanager{}, projectOverride{}) } + +func autoscalingTypes(schemas *types.Schemas) *types.Schemas { + return schemas. + AddMapperForType(&Version, autoscaling.HorizontalPodAutoscaler{}, + &m.ChangeType{Field: "scaleTargetRef", Type: "reference[workload]"}, + &m.Move{From: "scaleTargetRef", To: "workloadId"}, + mapper.CrossVersionObjectToWorkload{Field: "workloadId"}, + &m.Required{Fields: []string{"workloadId", "maxReplicas"}}, + &m.AnnotationField{Field: "displayName"}, + &m.DisplayName{}, + &m.AnnotationField{Field: "description"}, + &m.Embed{Field: "status"}, + mapper.NewMergeListByIndexMapper("currentMetrics", "metrics", "type"), + ). + AddMapperForType(&Version, autoscaling.MetricTarget{}, + &m.Enum{Field: "type", Options: []string{"Utilization", "Value", "AverageValue"}}, + &m.Move{To: "utilization", From: "averageUtilization"}, + ). + AddMapperForType(&Version, autoscaling.MetricValueStatus{}, + &m.Move{To: "utilization", From: "averageUtilization"}, + ). + AddMapperForType(&Version, autoscaling.MetricSpec{}, + &m.Condition{Field: "type", Value: "Object", Mapper: types.Mappers{ + &m.Move{To: "target", From: "object/target", DestDefined: true, NoDeleteFromField: true}, + &m.Move{To: "metric", From: "object/metric", DestDefined: true, NoDeleteFromField: true}, + }}, + &m.Condition{Field: "type", Value: "Pods", Mapper: types.Mappers{ + &m.Move{To: "target", From: "pods/target", DestDefined: true, NoDeleteFromField: true}, + &m.Move{To: "metric", From: "pods/metric", DestDefined: true, NoDeleteFromField: true}, + }}, + &m.Condition{Field: "type", Value: "Resource", Mapper: types.Mappers{ + &m.Move{To: "metric/name", From: "resource/name", DestDefined: true, NoDeleteFromField: true}, + &m.Move{To: "target", From: "resource/target", DestDefined: true, NoDeleteFromField: true}, + }}, + &m.Condition{Field: "type", Value: "External", Mapper: types.Mappers{ + &m.Move{To: "target", From: "external/target", DestDefined: true, NoDeleteFromField: true}, + &m.Move{To: "metric", From: "external/metric", DestDefined: true, NoDeleteFromField: true}, + }}, + &m.Embed{Field: "object", Ignore: []string{"target", "metric"}}, + &m.Embed{Field: "pods", Ignore: []string{"target", "metric"}}, + &m.Embed{Field: "external", Ignore: []string{"target", "metric"}}, + &m.Embed{Field: "resource", Ignore: []string{"target", "name"}}, + &m.Embed{Field: "metric"}, + &m.Enum{Field: "type", Options: []string{"Object", "Pods", "Resource", "External"}}, + ). + MustImportAndCustomize(&Version, autoscaling.MetricSpec{}, func(s *types.Schema) { + s.CodeName = "Metric" + s.PluralName = "metrics" + s.ID = "metric" + s.CodeNamePlural = "Metrics" + }, struct { + Target autoscaling.MetricTarget `json:"target"` + Metric autoscaling.MetricIdentifier `json:"metric"` + Current autoscaling.MetricValueStatus `json:"current" norman:"nocreate,noupdate"` + }{}). + AddMapperForType(&Version, autoscaling.MetricStatus{}, + &m.Condition{Field: "type", Value: "Object", Mapper: &m.Move{To: "current", From: "object/current", DestDefined: true, NoDeleteFromField: true}}, + &m.Condition{Field: "type", Value: "Pods", Mapper: &m.Move{To: "current", From: "pods/current", DestDefined: true, NoDeleteFromField: true}}, + &m.Condition{Field: "type", Value: "Resource", Mapper: &m.Move{To: "current", From: "resource/current"}}, + &m.Condition{Field: "type", Value: "External", Mapper: &m.Move{To: "current", From: "external/current", DestDefined: true, NoDeleteFromField: true}}, + ). + MustImport(&Version, autoscaling.HorizontalPodAutoscaler{}, projectOverride{}, struct { + DisplayName string `json:"displayName,omitempty"` + Description string `json:"description,omitempty"` + }{}) +} diff --git a/config/context.go b/config/context.go index de0df4c8..5250521e 100644 --- a/config/context.go +++ b/config/context.go @@ -10,6 +10,7 @@ import ( "github.com/rancher/norman/store/proxy" "github.com/rancher/norman/types" appsv1beta2 "github.com/rancher/types/apis/apps/v1beta2" + autoscaling "github.com/rancher/types/apis/autoscaling/v2beta2" batchv1 "github.com/rancher/types/apis/batch/v1" batchv1beta1 "github.com/rancher/types/apis/batch/v1beta1" clusterv3 "github.com/rancher/types/apis/cluster.cattle.io/v3" @@ -178,6 +179,7 @@ type UserContext struct { K8sClient kubernetes.Interface Apps appsv1beta2.Interface + Autoscaling autoscaling.Interface Project projectv3.Interface Core corev1.Interface RBAC rbacv1.Interface @@ -212,6 +214,7 @@ func (w *UserContext) UserOnlyContext() *UserOnlyContext { UnversionedClient: w.UnversionedClient, K8sClient: w.K8sClient, + Autoscaling: w.Autoscaling, Apps: w.Apps, Project: w.Project, Core: w.Core, @@ -232,6 +235,7 @@ type UserOnlyContext struct { K8sClient kubernetes.Interface Apps appsv1beta2.Interface + Autoscaling autoscaling.Interface Project projectv3.Interface Core corev1.Interface RBAC rbacv1.Interface @@ -393,6 +397,11 @@ func NewUserContext(scaledContext *ScaledContext, config rest.Config, clusterNam return nil, err } + context.Autoscaling, err = autoscaling.NewForConfig(config) + if err != nil { + return nil, err + } + context.Monitoring, err = monitoringv1.NewForConfig(config) context.Cluster, err = clusterv3.NewForConfig(config) if err != nil { @@ -477,6 +486,11 @@ func NewUserOnlyContext(config rest.Config) (*UserOnlyContext, error) { return nil, err } + context.Autoscaling, err = autoscaling.NewForConfig(config) + if err != nil { + return nil, err + } + context.Monitoring, err = monitoringv1.NewForConfig(config) context.Cluster, err = clusterv3.NewForConfig(config) if err != nil { diff --git a/main.go b/main.go index ec4c56d5..6b978f3c 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" + scalingv2beta2 "k8s.io/api/autoscaling/v2beta2" "k8s.io/api/core/v1" extv1beta1 "k8s.io/api/extensions/v1beta1" knetworkingv1 "k8s.io/api/networking/v1" @@ -89,4 +90,10 @@ func main() { }, []interface{}{}, ) + generator.GenerateNativeTypes(scalingv2beta2.SchemeGroupVersion, + []interface{}{ + scalingv2beta2.HorizontalPodAutoscaler{}, + }, + []interface{}{}, + ) } diff --git a/mapper/cross_version_object.go b/mapper/cross_version_object.go new file mode 100644 index 00000000..b4fe4bf1 --- /dev/null +++ b/mapper/cross_version_object.go @@ -0,0 +1,76 @@ +package mapper + +import ( + "fmt" + "strings" + + "github.com/rancher/norman/types/values" + + "github.com/rancher/norman/types" + "github.com/rancher/norman/types/convert" +) + +var ( + kindMap = map[string]string{ + "deployment": "Deployment", + "replicationcontroller": "ReplicationController", + "statefulset": "StatefulSet", + "daemonset": "DaemonSet", + "job": "Job", + "cronjob": "CronJob", + "replicaset": "ReplicaSet", + } + groupVersionMap = map[string]string{ + "deployment": "apps/v1beta2", + "replicationcontroller": "core/v1", + "statefulset": "apps/v1beta2", + "daemonset": "apps/v1beta2", + "job": "batch/v1", + "cronjob": "batch/v1beta1", + "replicaset": "apps/v1beta2", + } +) + +type CrossVersionObjectToWorkload struct { + Field string +} + +func (c CrossVersionObjectToWorkload) ToInternal(data map[string]interface{}) error { + obj, ok := values.GetValue(data, strings.Split(c.Field, "/")...) + if !ok { + return nil + } + workloadID := convert.ToString(obj) + parts := strings.SplitN(workloadID, ":", 3) + newObj := map[string]interface{}{ + "kind": getKind(parts[0]), + "apiVersion": groupVersionMap[parts[0]], + "name": parts[2], + } + values.PutValue(data, newObj, strings.Split(c.Field, "/")...) + return nil +} + +func (c CrossVersionObjectToWorkload) FromInternal(data map[string]interface{}) { + obj, ok := values.GetValue(data, strings.Split(c.Field, "/")...) + if !ok { + return + } + cvo := convert.ToMapInterface(obj) + ns := convert.ToString(data["namespaceId"]) + values.PutValue(data, + fmt.Sprintf("%s:%s:%s", + strings.ToLower(convert.ToString(cvo["kind"])), + ns, + convert.ToString(cvo["name"])), + strings.Split(c.Field, "/")..., + ) +} + +func (c CrossVersionObjectToWorkload) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { + return nil +} + +func getKind(i string) string { + return kindMap[i] +} diff --git a/mapper/merge_list_by_index.go b/mapper/merge_list_by_index.go new file mode 100644 index 00000000..0bfa22aa --- /dev/null +++ b/mapper/merge_list_by_index.go @@ -0,0 +1,111 @@ +package mapper + +import ( + "fmt" + + "github.com/rancher/norman/types" + "github.com/rancher/norman/types/convert" + "github.com/rancher/norman/types/definition" + "github.com/rancher/norman/types/mapper" +) + +func NewMergeListByIndexMapper(From, To string, Ignores ...string) *MergeListByIndexMapper { + rtn := MergeListByIndexMapper{ + From: From, + To: To, + Ignore: make(map[string]struct{}), + fromFields: []string{}, + } + for _, Ignore := range Ignores { + rtn.Ignore[Ignore] = struct{}{} + } + return &rtn +} + +type MergeListByIndexMapper struct { + From string + To string + Ignore map[string]struct{} + fromFields []string +} + +func (m *MergeListByIndexMapper) FromInternal(data map[string]interface{}) { + fromObj, ok := data[m.From] + if !ok { + return + } + toObj, ok := data[m.To] + if !ok { + return + } + fromList := convert.ToMapSlice(fromObj) + toList := convert.ToMapSlice(toObj) + for i := 0; i < len(fromList) && i < len(toList); i++ { + fromItem := fromList[i] + toItem := toList[i] + for key, value := range fromItem { + if _, ignore := m.Ignore[key]; ignore { + continue + } + toItem[key] = value + } + } + delete(data, m.From) +} + +func (m *MergeListByIndexMapper) ToInternal(data map[string]interface{}) error { + toObj, ok := data[m.To] + if !ok { + return nil + } + if _, ok = data[m.From]; ok { + return fmt.Errorf("field %s should not exist", m.From) + } + + toList := convert.ToMapSlice(toObj) + var fromList []map[string]interface{} + for _, toItem := range toList { + obj := make(map[string]interface{}) + for _, field := range m.fromFields { + value, ok := toItem[field] + if !ok { + continue + } + obj[field] = value + if _, ok := m.Ignore[field]; !ok { + delete(toItem, field) + } + } + fromList = append(fromList, obj) + } + data[m.From] = fromList + return nil +} + +func (m *MergeListByIndexMapper) ModifySchema(schema *types.Schema, schemas *types.Schemas) error { + if err := mapper.ValidateField(m.From, schema); err != nil { + return err + } + + fromType := schema.ResourceFields[m.From].Type + if !definition.IsArrayType(fromType) { + return fmt.Errorf("type of field %s in schema %s is not array", m.From, schema.CodeName) + } + + fromSchema := schemas.Schema(&schema.Version, definition.SubType(fromType)) + for field := range fromSchema.ResourceFields { + m.fromFields = append(m.fromFields, field) + } + + if err := mapper.ValidateField(m.To, schema); err != nil { + return err + } + + toType := schema.ResourceFields[m.To].Type + if !definition.IsArrayType(toType) { + return fmt.Errorf("type of field %s in schema %s is not array", m.To, schema.CodeName) + } + + delete(schema.ResourceFields, m.From) + return nil +} diff --git a/mapper/merge_list_by_index_test.go b/mapper/merge_list_by_index_test.go new file mode 100644 index 00000000..4d1b228f --- /dev/null +++ b/mapper/merge_list_by_index_test.go @@ -0,0 +1,38 @@ +package mapper + +import ( + "reflect" + "testing" +) + +var ( + metrics = []map[string]interface{}{ + {"type": "Resource", "resource": "abc"}, + {"type": "Object", "object": "def"}, + } + currentMetrics = []map[string]interface{}{ + {"type": "Resource", "currentResource": "tuvw"}, + {"type": "Object", "currentObject": "xyz"}, + } + origin = map[string]interface{}{ + "metrics": metrics, + "currentMetrics": currentMetrics, + } +) + +func Test_MergeList(t *testing.T) { + mapper := NewMergeListByIndexMapper("currentMetrics", "metrics", "type") + mapper.fromFields = []string{"type", "currentResource", "currentObject"} + internal := map[string]interface{}{ + "metrics": metrics, + "currentMetrics": currentMetrics, + } + mapper.FromInternal(internal) + + if err := mapper.ToInternal(internal); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(internal, origin) { + t.Fatal("merge list not match after parse") + } +} diff --git a/status/status.go b/status/status.go index c60610f0..ceca8fc2 100644 --- a/status/status.go +++ b/status/status.go @@ -59,6 +59,8 @@ var transitioningMap = map[string]string{ "Updating": "updating", "Waiting": "waiting", "InitialRolesPopulated": "activating", + "ScalingActive": "pending", + "AbleToScale": "pending", } // True == error