From 5c463508017bbcabde1f2459f2ced5f499d4b091 Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Wed, 20 Dec 2017 13:15:10 -0800 Subject: [PATCH 1/7] controller-revision docs --- docs/resources/controller-revision.md | 42 +++++++++++++++++++++++++++ docs/resources/index.md | 1 + mkdocs.yml | 1 + 3 files changed, 44 insertions(+) create mode 100644 docs/resources/controller-revision.md diff --git a/docs/resources/controller-revision.md b/docs/resources/controller-revision.md new file mode 100644 index 00000000..4a878cc8 --- /dev/null +++ b/docs/resources/controller-revision.md @@ -0,0 +1,42 @@ +# Introduction + +ControllerRevision is an immutable snapshot of state. +It's primarily intended for internal use by controllers. +For example, it's used by the DaemonSet and StatefulSet controllers for update and rollback. + +Here's an example Kubernetes ControllerRevision: +```yaml +apiVersion: apps/v1 +kind: ControllerRevision +metadata: + name: example +data: + key: value +revision: 1 +``` + +The following sections contain detailed information about each field in Short syntax, including how the field translates to and from Kubernetes syntax. + +# API Overview + +| Field | Type | K8s counterpart(s) | Description | +|:------|:-----|:--------|:-----------------------| +|version| `string` | `apiVersion` | The version of the resource object | +|cluster| `string` | `metadata.clusterName` | The name of the cluster on which this Job is running | +|name | `string` | `metadata.name`| The name of the Job | +|namespace | `string` | `metadata.namespace` | The K8s namespace this Job will be a member of | +|labels | `string` | `metadata.labels`| Metadata about the Job, including identifying information | +|annotations| `string` | `metadata.annotations`| Non-identifying information about the Job | +|data| YAML | `data` | This field can hold any valid YAML. | +|revision| `int64` | The revision number | + +# Examples / Skeleton + +Here's a starter skeleton of a Short ControllerRevision. +```yaml +controller_revision: + name: example + data: + key: value + revision: 1 +``` diff --git a/docs/resources/index.md b/docs/resources/index.md index 11156637..cc8d3331 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -101,3 +101,4 @@ The following types are currently supported | extensions/v1beta1 | Ingress | [Ingress](./ingress.md) | [Ingress Skeleton](./ingress.md#skeleton) | [Ingress Examples](./ingress.md#examples) | | core/v1 | ConfigMap | [ConfigMap](./config-map.md) | [ConfigMap Skeleton](./config-map.md#skeleton) | [ConfigMap Examples](./config-map.md#examples) | | core/v1 | Secret | [Secret](./secret.md) | [Secret Skeleton](./secret.md#skeleton) | [Secret Examples](./secret.md#examples) | +| apps/v1 | ControllerRevision | [ControllerRevision](./controller-revision.md) | [ControllerRevision Skeleton](./controller-revision.md#examples-skeleton) | [ControllerRevision Examples](./controller-revision.md#examples-skeleton) | diff --git a/mkdocs.yml b/mkdocs.yml index 2e83d176..a409c0f7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ pages: - Resources: - Introduction: resources/index.md - ConfigMap: resources/config-map.md + - ControllerRevision: resources/controller-revision.md - CronJob: resources/cron-job.md - DaemonSet: resources/daemon-set.md - Deployment: resources/deployment.md From 04eed39e50e01a068c7a44e71b4fba927a4765ab Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Thu, 21 Dec 2017 13:33:52 -0800 Subject: [PATCH 2/7] lowercase the pod condition type --- docs/resources/pod.md | 2 +- types/pod.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/pod.md b/docs/resources/pod.md index 075fce0c..d3e57aa5 100644 --- a/docs/resources/pod.md +++ b/docs/resources/pod.md @@ -520,7 +520,7 @@ The selector string selects Taints using the following formats | reason| `string` | One word camel case reason for pod's last transition | | msg | `string` | Human readable message about the pod's last transition | | status | `ConditionStatus` | String value that represents the status of the condition. Can be "True", "False" or "Unknown" | -| type | `PodConditionType` | String value that represents the type of condition. Can be "PodScheduled", "Ready" or "Initialized" | +| type | `PodConditionType` | String value that represents the type of condition. Can be "scheduled", "ready" or "initialized" | | last_probe_time | `time` | Last time the condition was probed | | last_transition_time | `time` | Last time the condition transitioned | diff --git a/types/pod.go b/types/pod.go index 3a50b2df..52ac27c0 100644 --- a/types/pod.go +++ b/types/pod.go @@ -71,7 +71,7 @@ type PodCondition struct { type PodConditionType string const ( - PodScheduled PodConditionType = "pod-scheduled" + PodScheduled PodConditionType = "scheduled" PodReady PodConditionType = "ready" PodInitialized PodConditionType = "initialized" PodReasonUnschedulable = "unschedulable" From 05bac44d9bcf06010491ea4946345130685ae75a Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Thu, 21 Dec 2017 13:34:24 -0800 Subject: [PATCH 3/7] crd docs --- docs/resources/custom-resource-definition.md | 65 ++++++++++++++++++++ docs/resources/event.md | 0 mkdocs.yml | 1 + types/crd.go | 4 +- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 docs/resources/custom-resource-definition.md create mode 100644 docs/resources/event.md diff --git a/docs/resources/custom-resource-definition.md b/docs/resources/custom-resource-definition.md new file mode 100644 index 00000000..7db2c1cb --- /dev/null +++ b/docs/resources/custom-resource-definition.md @@ -0,0 +1,65 @@ +# Introduction + +Custom Resource Definition (CRD) represents a resource type to expose in the API server. + +```yaml +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + name: crontabs.stable.example.com +spec: + group: stable.example.com + version: v1 + scope: Namespaced + names: + plural: crontabs + singular: crontab + kind: CronTab + shortNames: + - ct +``` + +The following sections contain detailed information about each field in Short syntax, including how the field translates to and from Kubernetes syntax. + +# API Overview + + +| Field | Type | K8s counterpart(s) | Description | +|:------|:-----|:--------|:-----------------------| +|version| `string` | `apiVersion` | The version of the resource object | +|cluster| `string` | `metadata.clusterName` | The name of the cluster on which this CRD is running | +|name | `string` | `metadata.name`| The name of the CRD. It must be of the form `.` | +|namespace | `string` | `metadata.namespace` | The K8s namespace this CRD will be a member of | +|labels | `string` | `metadata.labels`| Metadata about the CRD, including identifying information | +|annotations| `string` | `metadata.annotations`| Non-identifying information about the CRD | +|meta| `CRD Meta` | `spec.group` `spec.version` `spec.names` | Metadata about the resource defined by the CRD. See [CRD Meta](#crd-meta) | +|scope| `"namespaced"` or `"cluster"` | `spec.scope` | Whether the resource is namespaced or cluster-scoped. Defaults to `"namespaced"` if omitted. | +|validation| `JSONSchemaProps` | Optional OpenAPI schema for the defined resource type. | +|conditions| `[]CRD Condition`| `status.conditions` | The list of current and previous conditions of the CRD. See [CRD Condition](#crd-condition) | +|accepted| `CRD Names` | `spec.names` | The names actually being used for the discovery service. See [CRD Names](#crd-meta) | + +#### CRD Meta + +`CRD Meta` is `CRD Names` + `group` and `version`. + +| Field | Type | Description | +|:------|:-----|:--------| +|group| `string` | The API group for the API resource. e.g. `core` | +|version| `string` | The API version for the API resource definition. e.g. `v1` | +|plural| `string` | Lowercase plural name for the API resource. | +|singular| `string` | Lowercase singular name for the API resource. Defaults to lowercase of `kind`. | +|short| `[]string` | Lowercase abbreviated names for use in the command line. | +|kind| `string` | Capitalized camel-case name for the resource. Usually singular. e.g. `Pod` | +|list| `string`| Defaults to `List`. e.g. `PodList` | + +#### CRD Condition + +| Field | Type | Description | +|:------|:-----|:--------| +| reason| `string` | One word camel case reason for CRD's last transition | +| msg | `string` | Human readable message about the CRD's last transition | +| status | `ConditionStatus` | String value that represents the status of the condition. Can be "True", "False" or "Unknown" | +| type | `CRDConditionType` | String value that represents the type of condition. Can be "established", "names-accepted" or "terminating" | +| last_change | `time` | Last time the condition status changed | + diff --git a/docs/resources/event.md b/docs/resources/event.md new file mode 100644 index 00000000..e69de29b diff --git a/mkdocs.yml b/mkdocs.yml index a409c0f7..b5bd0c86 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ pages: - ConfigMap: resources/config-map.md - ControllerRevision: resources/controller-revision.md - CronJob: resources/cron-job.md + - CustomResourceDefinition: resources/custom-resource-definition.md - DaemonSet: resources/daemon-set.md - Deployment: resources/deployment.md - Endpoint: resources/endpoint.md diff --git a/types/crd.go b/types/crd.go index 3590f75c..ce10f2e3 100644 --- a/types/crd.go +++ b/types/crd.go @@ -17,6 +17,8 @@ type CustomResourceDefinition struct { Labels map[string]string `json:"labels,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` + // Spec::CRDSpec + // Group::string, Version::string, Names::CRDNames CRDMeta CRDMeta `json:"meta,omitempty"` Scope CRDResourceScope `json:"scope,omitempty"` Validation *apiext.JSONSchemaProps `json:"validation"` @@ -72,5 +74,5 @@ type CRDCondition struct { LastTransitionTime metav1.Time `json:"last_change,omitempty"` // Unique, one-word, CamelCase reason for the condition's last transition. Reason string `json:"reason"` - Message string `json:"message"` + Message string `json:"msg"` } From 3567ed2fad133a64c43504ce2862b25c7e19d9d0 Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Thu, 21 Dec 2017 15:03:36 -0800 Subject: [PATCH 4/7] limitrange implementation (w/o docs) --- .../converters/koki_limitrange_to_kube.go | 77 +++++++++++++++++++ .../converters/kube_limitrange_to_koki.go | 73 ++++++++++++++++++ converter/koki_converter.go | 4 + parser/native.go | 7 ++ testdata/limit_range/limit_range.short.yaml | 20 +++++ testdata/limit_range/limit_range.yaml | 23 ++++++ tests/functional_test.go | 7 ++ types/limitrange.go | 46 +++++++++++ 8 files changed, 257 insertions(+) create mode 100644 converter/converters/koki_limitrange_to_kube.go create mode 100644 converter/converters/kube_limitrange_to_koki.go create mode 100644 testdata/limit_range/limit_range.short.yaml create mode 100644 testdata/limit_range/limit_range.yaml create mode 100644 types/limitrange.go diff --git a/converter/converters/koki_limitrange_to_kube.go b/converter/converters/koki_limitrange_to_kube.go new file mode 100644 index 00000000..f73de0c1 --- /dev/null +++ b/converter/converters/koki_limitrange_to_kube.go @@ -0,0 +1,77 @@ +package converters + +import ( + "k8s.io/api/core/v1" + + "github.com/koki/short/types" + serrors "github.com/koki/structurederrors" +) + +func Convert_Koki_LimitRange_to_Kube(wrapper *types.LimitRangeWrapper) (*v1.LimitRange, error) { + var err error + kube := &v1.LimitRange{} + koki := wrapper.LimitRange + + kube.Name = koki.Name + kube.Namespace = koki.Namespace + if len(koki.Version) == 0 { + kube.APIVersion = "v1" + } else { + kube.APIVersion = koki.Version + } + kube.Kind = "LimitRange" + kube.ClusterName = koki.Cluster + kube.Labels = koki.Labels + kube.Annotations = koki.Annotations + + kube.Spec.Limits, err = revertLimitRangeItems(koki.Limits) + if err != nil { + return nil, serrors.ContextualizeErrorf(err, "LimitRange.Spec.Limits") + } + + return kube, nil +} + +func revertLimitRangeItems(kokis []types.LimitRangeItem) ([]v1.LimitRangeItem, error) { + if len(kokis) == 0 { + return nil, nil + } + + var err error + kubes := make([]v1.LimitRangeItem, len(kokis)) + for i, koki := range kokis { + kubes[i], err = revertLimitRangeItem(koki) + if err != nil { + return nil, serrors.ContextualizeErrorf(err, "[%d]", i) + } + } + + return kubes, nil +} + +func revertLimitRangeItem(koki types.LimitRangeItem) (v1.LimitRangeItem, error) { + kube := v1.LimitRangeItem{ + Max: koki.Max, + Min: koki.Min, + Default: koki.Default, + DefaultRequest: koki.DefaultRequest, + MaxLimitRequestRatio: koki.MaxLimitRequestRatio, + } + + var err error + kube.Type, err = revertLimitType(koki.Type) + return kube, err +} + +func revertLimitType(koki types.LimitType) (v1.LimitType, error) { + switch koki { + case types.LimitTypePod: + return v1.LimitTypePod, nil + case types.LimitTypeContainer: + return v1.LimitTypeContainer, nil + case types.LimitTypePersistentVolumeClaim: + return v1.LimitTypePersistentVolumeClaim, nil + default: + return "", serrors.InvalidInstanceError(koki) + } +} diff --git a/converter/converters/kube_limitrange_to_koki.go b/converter/converters/kube_limitrange_to_koki.go new file mode 100644 index 00000000..3302bb84 --- /dev/null +++ b/converter/converters/kube_limitrange_to_koki.go @@ -0,0 +1,73 @@ +package converters + +import ( + "k8s.io/api/core/v1" + + "github.com/koki/short/types" + serrors "github.com/koki/structurederrors" +) + +func Convert_Kube_LimitRange_to_Koki(kube *v1.LimitRange) (*types.LimitRangeWrapper, error) { + var err error + koki := &types.LimitRange{} + + koki.Name = kube.Name + koki.Namespace = kube.Namespace + koki.Version = kube.APIVersion + koki.Cluster = kube.ClusterName + koki.Labels = kube.Labels + koki.Annotations = kube.Annotations + + koki.Limits, err = convertLimitRangeItems(kube.Spec.Limits) + if err != nil { + return nil, serrors.ContextualizeErrorf(err, "limit_range limits") + } + + return &types.LimitRangeWrapper{ + LimitRange: *koki, + }, nil +} + +func convertLimitRangeItems(kubes []v1.LimitRangeItem) ([]types.LimitRangeItem, error) { + if len(kubes) == 0 { + return nil, nil + } + + var err error + kokis := make([]types.LimitRangeItem, len(kubes)) + for i, kube := range kubes { + kokis[i], err = convertLimitRangeItem(kube) + if err != nil { + return nil, serrors.ContextualizeErrorf(err, "[%d]", i) + } + } + + return kokis, nil +} + +func convertLimitRangeItem(kube v1.LimitRangeItem) (types.LimitRangeItem, error) { + koki := types.LimitRangeItem{ + Max: kube.Max, + Min: kube.Min, + Default: kube.Default, + DefaultRequest: kube.DefaultRequest, + MaxLimitRequestRatio: kube.MaxLimitRequestRatio, + } + + var err error + koki.Type, err = convertLimitType(kube.Type) + return koki, err +} + +func convertLimitType(kube v1.LimitType) (types.LimitType, error) { + switch kube { + case v1.LimitTypePod: + return types.LimitTypePod, nil + case v1.LimitTypeContainer: + return types.LimitTypeContainer, nil + case v1.LimitTypePersistentVolumeClaim: + return types.LimitTypePersistentVolumeClaim, nil + default: + return "", serrors.InvalidInstanceError(kube) + } +} diff --git a/converter/koki_converter.go b/converter/koki_converter.go index e9bf2899..c2efa76f 100644 --- a/converter/koki_converter.go +++ b/converter/koki_converter.go @@ -53,6 +53,8 @@ func DetectAndConvertFromKokiObj(kokiObj interface{}) (interface{}, error) { return converters.Convert_Koki_InitializerConfig_to_Kube_InitializerConfig(kokiObj) case *types.JobWrapper: return converters.Convert_Koki_Job_to_Kube_Job(kokiObj) + case *types.LimitRangeWrapper: + return converters.Convert_Koki_LimitRange_to_Kube(kokiObj) case *types.NamespaceWrapper: return converters.Convert_Koki_Namespace_to_Kube_Namespace(kokiObj) case *types.PersistentVolumeClaimWrapper: @@ -116,6 +118,8 @@ func DetectAndConvertFromKubeObj(kubeObj runtime.Object) (interface{}, error) { return converters.Convert_Kube_InitializerConfig_to_Koki_InitializerConfig(kubeObj) case *batchv1.Job: return converters.Convert_Kube_Job_to_Koki_Job(kubeObj) + case *v1.LimitRange: + return converters.Convert_Kube_LimitRange_to_Koki(kubeObj) case *v1.Namespace: return converters.Convert_Kube_Namespace_to_Koki_Namespace(kubeObj) case *v1.PersistentVolume: diff --git a/parser/native.go b/parser/native.go index a1e293d1..95ae2707 100644 --- a/parser/native.go +++ b/parser/native.go @@ -116,6 +116,13 @@ func ParseKokiNativeObject(obj interface{}) (interface{}, error) { return nil, serrors.InvalidValueForTypeContextError(err, objMap, job) } return job, nil + case "limit_range": + result := &types.LimitRangeWrapper{} + err := json.Unmarshal(bytes, result) + if err != nil { + return nil, serrors.InvalidValueForTypeContextError(err, objMap, result) + } + return result, nil case "namespace": namespace := &types.NamespaceWrapper{} err := json.Unmarshal(bytes, namespace) diff --git a/testdata/limit_range/limit_range.short.yaml b/testdata/limit_range/limit_range.short.yaml new file mode 100644 index 00000000..36001732 --- /dev/null +++ b/testdata/limit_range/limit_range.short.yaml @@ -0,0 +1,20 @@ +limit_range: + name: test-limits + limits: + - kind: pod + max: + cpu: 2 + memory: 16G + min: + cpu: 1m + memory: 128M + default_max: + cpu: 100m + memory: 1G + default_min: + cpu: 50m + memory: 500M + max_burst_ratio: + cpu: "2" + memory: "1.5" + version: v1 diff --git a/testdata/limit_range/limit_range.yaml b/testdata/limit_range/limit_range.yaml new file mode 100644 index 00000000..d2162395 --- /dev/null +++ b/testdata/limit_range/limit_range.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: LimitRange +metadata: + name: test-limits +spec: + limits: + - default: + cpu: 100m + memory: 1G + defaultRequest: + cpu: 50m + memory: 500M + max: + cpu: "2" + memory: 16G + maxLimitRequestRatio: + cpu: "2" + memory: "1.5" + min: + cpu: 1m + memory: 128M + type: Pod + diff --git a/tests/functional_test.go b/tests/functional_test.go index d0ae6312..6d83dfc1 100644 --- a/tests/functional_test.go +++ b/tests/functional_test.go @@ -183,6 +183,13 @@ func TestPodSecurityPolicy(t *testing.T) { } } +func TestLimitRange(t *testing.T) { + err := testResource("limit_range", testFuncGenerator(t)) + if err != nil { + t.Fatal(err) + } +} + type filePair struct { kubeSpec string kokiSpec string diff --git a/types/limitrange.go b/types/limitrange.go new file mode 100644 index 00000000..884c0d37 --- /dev/null +++ b/types/limitrange.go @@ -0,0 +1,46 @@ +package types + +import ( + "k8s.io/api/core/v1" +) + +type LimitRangeWrapper struct { + LimitRange `json:"limit_range"` +} + +type LimitRange struct { + Version string `json:"version,omitempty"` + Cluster string `json:"cluster,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + + // Spec::LimitRangeSpec + Limits []LimitRangeItem `json:"limits"` +} + +type LimitRangeItem struct { + // Type of resource that this limit applies to. + Type LimitType `json:"kind,omitempty"` + // Max usage constraints on this kind by resource name. + Max v1.ResourceList `json:"max,omitempty"` + // Min usage constraints on this kind by resource name. + Min v1.ResourceList `json:"min,omitempty"` + // Default resource requirement limit value by resource name + // (if resource limit is omitted) + Default v1.ResourceList `json:"default_max,omitempty"` + // default resource requirement request value by resource name + // (if resource request is omitted) + DefaultRequest v1.ResourceList `json:"default_min,omitempty"` + // MaxLimitRequestRatio represents the max burst for the named resource. + MaxLimitRequestRatio v1.ResourceList `json:"max_burst_ratio,omitempty"` +} + +type LimitType string + +const ( + LimitTypePod LimitType = "pod" + LimitTypeContainer LimitType = "container" + LimitTypePersistentVolumeClaim LimitType = "pvc" +) From b1977d24607e2b421588a538674647ac2277b513 Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Sat, 23 Dec 2017 18:01:07 -0800 Subject: [PATCH 5/7] horizontalpodautoscaler impl --- converter/converters/koki_hpa_to_kube.go | 56 +++++++++++++++ converter/converters/kube_hpa_to_koki.go | 53 ++++++++++++++ converter/koki_converter.go | 5 ++ parser/native.go | 7 ++ testdata/hpas/hpa.short.yaml | 12 ++++ testdata/hpas/hpa.yaml | 19 +++++ tests/functional_test.go | 7 ++ types/hpa.go | 88 ++++++++++++++++++++++++ types/hpa_test.go | 57 +++++++++++++++ 9 files changed, 304 insertions(+) create mode 100644 converter/converters/koki_hpa_to_kube.go create mode 100644 converter/converters/kube_hpa_to_koki.go create mode 100644 testdata/hpas/hpa.short.yaml create mode 100644 testdata/hpas/hpa.yaml create mode 100644 types/hpa.go create mode 100644 types/hpa_test.go diff --git a/converter/converters/koki_hpa_to_kube.go b/converter/converters/koki_hpa_to_kube.go new file mode 100644 index 00000000..436e2a2c --- /dev/null +++ b/converter/converters/koki_hpa_to_kube.go @@ -0,0 +1,56 @@ +package converters + +import ( + autoscaling "k8s.io/api/autoscaling/v1" + + "github.com/koki/short/types" +) + +func Convert_Koki_HPA_to_Kube(wrapper *types.HorizontalPodAutoscalerWrapper) (*autoscaling.HorizontalPodAutoscaler, error) { + kube := &autoscaling.HorizontalPodAutoscaler{} + koki := wrapper.HPA + + kube.Name = koki.Name + kube.Namespace = koki.Namespace + if len(koki.Version) == 0 { + kube.APIVersion = "autoscaling/v1" + } else { + kube.APIVersion = koki.Version + } + kube.Kind = "HorizontalPodAutoscaler" + kube.ClusterName = koki.Cluster + kube.Labels = koki.Labels + kube.Annotations = koki.Annotations + + kube.Spec = revertHPASpec(koki.HorizontalPodAutoscalerSpec) + kube.Status = revertHPAStatus(koki.HorizontalPodAutoscalerStatus) + + return kube, nil +} + +func revertHPASpec(kokiSpec types.HorizontalPodAutoscalerSpec) autoscaling.HorizontalPodAutoscalerSpec { + return autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: revertCrossVersionObjectReference(kokiSpec.ScaleTargetRef), + MinReplicas: kokiSpec.MinReplicas, + MaxReplicas: kokiSpec.MaxReplicas, + TargetCPUUtilizationPercentage: kokiSpec.TargetCPUUtilizationPercentage, + } +} + +func revertHPAStatus(kokiStatus types.HorizontalPodAutoscalerStatus) autoscaling.HorizontalPodAutoscalerStatus { + return autoscaling.HorizontalPodAutoscalerStatus{ + ObservedGeneration: kokiStatus.ObservedGeneration, + LastScaleTime: kokiStatus.LastScaleTime, + CurrentReplicas: kokiStatus.CurrentReplicas, + DesiredReplicas: kokiStatus.DesiredReplicas, + CurrentCPUUtilizationPercentage: kokiStatus.CurrentCPUUtilizationPercentage, + } +} + +func revertCrossVersionObjectReference(kokiRef types.CrossVersionObjectReference) autoscaling.CrossVersionObjectReference { + return autoscaling.CrossVersionObjectReference{ + Kind: kokiRef.Kind, + Name: kokiRef.Name, + APIVersion: kokiRef.APIVersion, + } +} diff --git a/converter/converters/kube_hpa_to_koki.go b/converter/converters/kube_hpa_to_koki.go new file mode 100644 index 00000000..32958805 --- /dev/null +++ b/converter/converters/kube_hpa_to_koki.go @@ -0,0 +1,53 @@ +package converters + +import ( + autoscaling "k8s.io/api/autoscaling/v1" + + "github.com/koki/short/types" +) + +func Convert_Kube_HPA_to_Koki(kube *autoscaling.HorizontalPodAutoscaler) (*types.HorizontalPodAutoscalerWrapper, error) { + koki := &types.HorizontalPodAutoscaler{} + + koki.Name = kube.Name + koki.Namespace = kube.Namespace + koki.Version = kube.APIVersion + koki.Cluster = kube.ClusterName + koki.Labels = kube.Labels + koki.Annotations = kube.Annotations + + koki.HorizontalPodAutoscalerSpec = convertHPASpec(kube.Spec) + koki.HorizontalPodAutoscalerStatus = convertHPAStatus(kube.Status) + + return &types.HorizontalPodAutoscalerWrapper{ + HPA: *koki, + }, nil +} + +func convertHPASpec(kubeSpec autoscaling.HorizontalPodAutoscalerSpec) types.HorizontalPodAutoscalerSpec { + return types.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: convertCrossVersionObjectReference(kubeSpec.ScaleTargetRef), + + MinReplicas: kubeSpec.MinReplicas, + MaxReplicas: kubeSpec.MaxReplicas, + TargetCPUUtilizationPercentage: kubeSpec.TargetCPUUtilizationPercentage, + } +} + +func convertHPAStatus(kubeStatus autoscaling.HorizontalPodAutoscalerStatus) types.HorizontalPodAutoscalerStatus { + return types.HorizontalPodAutoscalerStatus{ + ObservedGeneration: kubeStatus.ObservedGeneration, + LastScaleTime: kubeStatus.LastScaleTime, + CurrentReplicas: kubeStatus.CurrentReplicas, + DesiredReplicas: kubeStatus.DesiredReplicas, + CurrentCPUUtilizationPercentage: kubeStatus.CurrentCPUUtilizationPercentage, + } +} + +func convertCrossVersionObjectReference(kubeRef autoscaling.CrossVersionObjectReference) types.CrossVersionObjectReference { + return types.CrossVersionObjectReference{ + Kind: kubeRef.Kind, + Name: kubeRef.Name, + APIVersion: kubeRef.APIVersion, + } +} diff --git a/converter/koki_converter.go b/converter/koki_converter.go index c2efa76f..3355e178 100644 --- a/converter/koki_converter.go +++ b/converter/koki_converter.go @@ -9,6 +9,7 @@ import ( apps "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" + autoscaling "k8s.io/api/autoscaling/v1" batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" batchv2alpha1 "k8s.io/api/batch/v2alpha1" @@ -47,6 +48,8 @@ func DetectAndConvertFromKokiObj(kokiObj interface{}) (interface{}, error) { return converters.Convert_Koki_Endpoints_to_Kube_v1_Endpoints(kokiObj) case *types.EventWrapper: return converters.Convert_Koki_Event_to_Kube(kokiObj) + case *types.HorizontalPodAutoscalerWrapper: + return converters.Convert_Koki_HPA_to_Kube(kokiObj) case *types.IngressWrapper: return converters.Convert_Koki_Ingress_to_Kube_Ingress(kokiObj) case *types.InitializerConfigWrapper: @@ -112,6 +115,8 @@ func DetectAndConvertFromKubeObj(kubeObj runtime.Object) (interface{}, error) { return converters.Convert_Kube_v1_Endpoints_to_Koki_Endpoints(kubeObj) case *v1.Event: return converters.Convert_Kube_Event_to_Koki(kubeObj) + case *autoscaling.HorizontalPodAutoscaler: + return converters.Convert_Kube_HPA_to_Koki(kubeObj) case *exts.Ingress: return converters.Convert_Kube_Ingress_to_Koki_Ingress(kubeObj) case *admissionregv1alpha1.InitializerConfiguration: diff --git a/parser/native.go b/parser/native.go index 95ae2707..2fa73450 100644 --- a/parser/native.go +++ b/parser/native.go @@ -95,6 +95,13 @@ func ParseKokiNativeObject(obj interface{}) (interface{}, error) { return nil, serrors.InvalidValueForTypeContextError(err, objMap, result) } return result, nil + case "hpa": + result := &types.HorizontalPodAutoscalerWrapper{} + err := json.Unmarshal(bytes, result) + if err != nil { + return nil, serrors.InvalidValueForTypeContextError(err, objMap, result) + } + return result, nil case "ingress": ingress := &types.IngressWrapper{} err := json.Unmarshal(bytes, ingress) diff --git a/testdata/hpas/hpa.short.yaml b/testdata/hpas/hpa.short.yaml new file mode 100644 index 00000000..9cf3e77b --- /dev/null +++ b/testdata/hpas/hpa.short.yaml @@ -0,0 +1,12 @@ +hpa: + name: test-hpa + ref: core.v1.Pod:test-pod + min: 1 + max: 5 + percent_cpu: 70 + generation_observed: 10 + last_scaling: 2017-01-01T00:00:00Z + current: 2 + desired: 3 + current_percent_cpu: 80 + version: autoscaling/v1 diff --git a/testdata/hpas/hpa.yaml b/testdata/hpas/hpa.yaml new file mode 100644 index 00000000..fae0445d --- /dev/null +++ b/testdata/hpas/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: test-hpa +spec: + maxReplicas: 5 + minReplicas: 1 + scaleTargetRef: + apiVersion: core.v1 + kind: Pod + name: test-pod + targetCPUUtilizationPercentage: 70 +status: + currentCPUUtilizationPercentage: 80 + currentReplicas: 2 + desiredReplicas: 3 + lastScaleTime: 2017-01-01T00:00:00Z + observedGeneration: 10 + diff --git a/tests/functional_test.go b/tests/functional_test.go index 6d83dfc1..bd28b5a4 100644 --- a/tests/functional_test.go +++ b/tests/functional_test.go @@ -190,6 +190,13 @@ func TestLimitRange(t *testing.T) { } } +func TestHorizontalPodAutoscaler(t *testing.T) { + err := testResource("hpas", testFuncGenerator(t)) + if err != nil { + t.Fatal(err) + } +} + type filePair struct { kubeSpec string kokiSpec string diff --git a/types/hpa.go b/types/hpa.go new file mode 100644 index 00000000..da053a6b --- /dev/null +++ b/types/hpa.go @@ -0,0 +1,88 @@ +package types + +import ( + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/koki/json" + serrors "github.com/koki/structurederrors" +) + +type HorizontalPodAutoscalerWrapper struct { + HPA HorizontalPodAutoscaler `json:"hpa"` +} + +type HorizontalPodAutoscaler struct { + Version string `json:"version,omitempty"` + Cluster string `json:"cluster,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + + HorizontalPodAutoscalerSpec `json:",inline"` + + HorizontalPodAutoscalerStatus `json:",inline"` +} + +type HorizontalPodAutoscalerSpec struct { + ScaleTargetRef CrossVersionObjectReference `json:"ref"` + MinReplicas *int32 `json:"min,omitempty"` + MaxReplicas int32 `json:"max"` + TargetCPUUtilizationPercentage *int32 `json:"percent_cpu,omitempty"` +} + +// current status of a horizontal pod autoscaler +type HorizontalPodAutoscalerStatus struct { + ObservedGeneration *int64 `json:"generation_observed,omitempty"` + LastScaleTime *metav1.Time `json:"last_scaling,omitempty"` + CurrentReplicas int32 `json:"current,omitempty"` + DesiredReplicas int32 `json:"desired,omitempty"` + CurrentCPUUtilizationPercentage *int32 `json:"current_percent_cpu,omitempty"` +} + +type CrossVersionObjectReference struct { + Kind string + Name string + APIVersion string +} + +func (r CrossVersionObjectReference) VersionKind() string { + if len(r.APIVersion) > 0 { + return fmt.Sprintf("%s.%s", r.APIVersion, r.Kind) + } + + return r.Kind +} + +func (r CrossVersionObjectReference) MarshalJSON() ([]byte, error) { + str := fmt.Sprintf("%s:%s", r.VersionKind(), r.Name) + return json.Marshal(str) +} + +func (r *CrossVersionObjectReference) UnmarshalJSON(data []byte) error { + var str string + err := json.Unmarshal(data, &str) + if err != nil { + return err + } + + segments := strings.Split(str, ":") + if len(segments) != 2 { + return serrors.InvalidValueForTypeErrorf(str, r, "expected 'version.kind:name' OR 'kind:name'") + } + + r.Name = segments[1] + + splitAt := strings.LastIndex(segments[0], ".") + if splitAt >= 0 { + r.APIVersion = segments[0][:splitAt] + r.Kind = segments[0][splitAt+1:] + } else { + r.Kind = segments[0] + } + + return nil +} diff --git a/types/hpa_test.go b/types/hpa_test.go new file mode 100644 index 00000000..17f22b22 --- /dev/null +++ b/types/hpa_test.go @@ -0,0 +1,57 @@ +package types + +import ( + "reflect" + "testing" + + "github.com/koki/json" +) + +func TestCrossVersionObjectReference(t *testing.T) { + testOneCVORef("group.version.kind:name", CrossVersionObjectReference{ + APIVersion: "group.version", + Kind: "kind", + Name: "name", + }, t, false) + testOneCVORef("kind:name", CrossVersionObjectReference{ + Kind: "kind", + Name: "name", + }, t, false) + testOneCVORef("name", CrossVersionObjectReference{}, t, true) + testOneCVORef("group.group1.kind", CrossVersionObjectReference{}, t, true) + testOneCVORef("group.:name", CrossVersionObjectReference{ + APIVersion: "group", + Kind: "", + Name: "name", + }, t, false) +} + +func testOneCVORef(nakedStr string, obj CrossVersionObjectReference, t *testing.T, decodeError bool) { + str := `"` + nakedStr + `"` + t.Log(str, obj) + newObj := CrossVersionObjectReference{} + err := json.Unmarshal([]byte(str), &newObj) + if err != nil { + if decodeError { + return + } + + t.Fatal(err) + } else if decodeError { + t.Fatal("expected a decode error") + } + + b, err := json.Marshal(obj) + if err != nil { + t.Fatal(err) + } + + t.Log(string(b), str, newObj, obj) + if !reflect.DeepEqual(obj, newObj) { + t.Fatal("objects don't match") + } + + if str != string(b) { + t.Fatal("strings don't match") + } +} From a8ab27e88666fbdf45ab262826b497758003bd48 Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Wed, 27 Dec 2017 14:55:13 -0800 Subject: [PATCH 6/7] follow variable name convention in limitrange impl --- .../converters/koki_limitrange_to_kube.go | 36 +++++++++---------- .../converters/kube_limitrange_to_koki.go | 36 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/converter/converters/koki_limitrange_to_kube.go b/converter/converters/koki_limitrange_to_kube.go index f73de0c1..9c5776bd 100644 --- a/converter/converters/koki_limitrange_to_kube.go +++ b/converter/converters/koki_limitrange_to_kube.go @@ -32,39 +32,39 @@ func Convert_Koki_LimitRange_to_Kube(wrapper *types.LimitRangeWrapper) (*v1.Limi return kube, nil } -func revertLimitRangeItems(kokis []types.LimitRangeItem) ([]v1.LimitRangeItem, error) { - if len(kokis) == 0 { +func revertLimitRangeItems(kokiItems []types.LimitRangeItem) ([]v1.LimitRangeItem, error) { + if len(kokiItems) == 0 { return nil, nil } var err error - kubes := make([]v1.LimitRangeItem, len(kokis)) - for i, koki := range kokis { - kubes[i], err = revertLimitRangeItem(koki) + kubeItems := make([]v1.LimitRangeItem, len(kokiItems)) + for i, kokiItem := range kokiItems { + kubeItems[i], err = revertLimitRangeItem(kokiItem) if err != nil { return nil, serrors.ContextualizeErrorf(err, "[%d]", i) } } - return kubes, nil + return kubeItems, nil } -func revertLimitRangeItem(koki types.LimitRangeItem) (v1.LimitRangeItem, error) { - kube := v1.LimitRangeItem{ - Max: koki.Max, - Min: koki.Min, - Default: koki.Default, - DefaultRequest: koki.DefaultRequest, - MaxLimitRequestRatio: koki.MaxLimitRequestRatio, +func revertLimitRangeItem(kokiItem types.LimitRangeItem) (v1.LimitRangeItem, error) { + kubeItem := v1.LimitRangeItem{ + Max: kokiItem.Max, + Min: kokiItem.Min, + Default: kokiItem.Default, + DefaultRequest: kokiItem.DefaultRequest, + MaxLimitRequestRatio: kokiItem.MaxLimitRequestRatio, } var err error - kube.Type, err = revertLimitType(koki.Type) - return kube, err + kubeItem.Type, err = revertLimitType(kokiItem.Type) + return kubeItem, err } -func revertLimitType(koki types.LimitType) (v1.LimitType, error) { - switch koki { +func revertLimitType(kokiType types.LimitType) (v1.LimitType, error) { + switch kokiType { case types.LimitTypePod: return v1.LimitTypePod, nil case types.LimitTypeContainer: @@ -72,6 +72,6 @@ func revertLimitType(koki types.LimitType) (v1.LimitType, error) { case types.LimitTypePersistentVolumeClaim: return v1.LimitTypePersistentVolumeClaim, nil default: - return "", serrors.InvalidInstanceError(koki) + return "", serrors.InvalidInstanceError(kokiType) } } diff --git a/converter/converters/kube_limitrange_to_koki.go b/converter/converters/kube_limitrange_to_koki.go index 3302bb84..71baad81 100644 --- a/converter/converters/kube_limitrange_to_koki.go +++ b/converter/converters/kube_limitrange_to_koki.go @@ -28,39 +28,39 @@ func Convert_Kube_LimitRange_to_Koki(kube *v1.LimitRange) (*types.LimitRangeWrap }, nil } -func convertLimitRangeItems(kubes []v1.LimitRangeItem) ([]types.LimitRangeItem, error) { - if len(kubes) == 0 { +func convertLimitRangeItems(kubeItems []v1.LimitRangeItem) ([]types.LimitRangeItem, error) { + if len(kubeItems) == 0 { return nil, nil } var err error - kokis := make([]types.LimitRangeItem, len(kubes)) - for i, kube := range kubes { - kokis[i], err = convertLimitRangeItem(kube) + kokiItems := make([]types.LimitRangeItem, len(kubeItems)) + for i, kubeItem := range kubeItems { + kokiItems[i], err = convertLimitRangeItem(kubeItem) if err != nil { return nil, serrors.ContextualizeErrorf(err, "[%d]", i) } } - return kokis, nil + return kokiItems, nil } -func convertLimitRangeItem(kube v1.LimitRangeItem) (types.LimitRangeItem, error) { - koki := types.LimitRangeItem{ - Max: kube.Max, - Min: kube.Min, - Default: kube.Default, - DefaultRequest: kube.DefaultRequest, - MaxLimitRequestRatio: kube.MaxLimitRequestRatio, +func convertLimitRangeItem(kubeItem v1.LimitRangeItem) (types.LimitRangeItem, error) { + kokiItem := types.LimitRangeItem{ + Max: kubeItem.Max, + Min: kubeItem.Min, + Default: kubeItem.Default, + DefaultRequest: kubeItem.DefaultRequest, + MaxLimitRequestRatio: kubeItem.MaxLimitRequestRatio, } var err error - koki.Type, err = convertLimitType(kube.Type) - return koki, err + kokiItem.Type, err = convertLimitType(kubeItem.Type) + return kokiItem, err } -func convertLimitType(kube v1.LimitType) (types.LimitType, error) { - switch kube { +func convertLimitType(kubeItem v1.LimitType) (types.LimitType, error) { + switch kubeItem { case v1.LimitTypePod: return types.LimitTypePod, nil case v1.LimitTypeContainer: @@ -68,6 +68,6 @@ func convertLimitType(kube v1.LimitType) (types.LimitType, error) { case v1.LimitTypePersistentVolumeClaim: return types.LimitTypePersistentVolumeClaim, nil default: - return "", serrors.InvalidInstanceError(kube) + return "", serrors.InvalidInstanceError(kubeItem) } } From f41d5765bbffcc93eb2d80372cf218863c908357 Mon Sep 17 00:00:00 2001 From: Kynan Rilee Date: Wed, 27 Dec 2017 15:26:55 -0800 Subject: [PATCH 7/7] allow empty limitrange item type --- .../converters/koki_limitrange_to_kube.go | 4 ++++ .../converters/kube_limitrange_to_koki.go | 4 ++++ .../limit_range_empty_type.short.yaml | 19 ++++++++++++++++ .../limit_range/limit_range_empty_type.yaml | 22 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 testdata/limit_range/limit_range_empty_type.short.yaml create mode 100644 testdata/limit_range/limit_range_empty_type.yaml diff --git a/converter/converters/koki_limitrange_to_kube.go b/converter/converters/koki_limitrange_to_kube.go index 9c5776bd..5766ba2d 100644 --- a/converter/converters/koki_limitrange_to_kube.go +++ b/converter/converters/koki_limitrange_to_kube.go @@ -64,6 +64,10 @@ func revertLimitRangeItem(kokiItem types.LimitRangeItem) (v1.LimitRangeItem, err } func revertLimitType(kokiType types.LimitType) (v1.LimitType, error) { + if len(kokiType) == 0 { + return "", nil + } + switch kokiType { case types.LimitTypePod: return v1.LimitTypePod, nil diff --git a/converter/converters/kube_limitrange_to_koki.go b/converter/converters/kube_limitrange_to_koki.go index 71baad81..c1004836 100644 --- a/converter/converters/kube_limitrange_to_koki.go +++ b/converter/converters/kube_limitrange_to_koki.go @@ -60,6 +60,10 @@ func convertLimitRangeItem(kubeItem v1.LimitRangeItem) (types.LimitRangeItem, er } func convertLimitType(kubeItem v1.LimitType) (types.LimitType, error) { + if len(kubeItem) == 0 { + return "", nil + } + switch kubeItem { case v1.LimitTypePod: return types.LimitTypePod, nil diff --git a/testdata/limit_range/limit_range_empty_type.short.yaml b/testdata/limit_range/limit_range_empty_type.short.yaml new file mode 100644 index 00000000..ab96f094 --- /dev/null +++ b/testdata/limit_range/limit_range_empty_type.short.yaml @@ -0,0 +1,19 @@ +limit_range: + name: test-limits + limits: + - max: + cpu: 2 + memory: 16G + min: + cpu: 1m + memory: 128M + default_max: + cpu: 100m + memory: 1G + default_min: + cpu: 50m + memory: 500M + max_burst_ratio: + cpu: "2" + memory: "1.5" + version: v1 diff --git a/testdata/limit_range/limit_range_empty_type.yaml b/testdata/limit_range/limit_range_empty_type.yaml new file mode 100644 index 00000000..733c6389 --- /dev/null +++ b/testdata/limit_range/limit_range_empty_type.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: LimitRange +metadata: + name: test-limits +spec: + limits: + - default: + cpu: 100m + memory: 1G + defaultRequest: + cpu: 50m + memory: 500M + max: + cpu: "2" + memory: 16G + maxLimitRequestRatio: + cpu: "2" + memory: "1.5" + min: + cpu: 1m + memory: 128M +