From c7a56db1935dabe7aea0399ebfbed3cf5661c927 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Wed, 11 May 2016 14:55:13 -0700 Subject: [PATCH] Add Template Api types and wiring Feature Issue: kubernetes/features#3 - Add Template types to extensions/types.go and extensions/v1beta1/types.go - Add etcd storage for Template object - Add stragegy for Template object - Register Template Api types in the extensions api - Register Template Api CRUD and Process endpoints in the master - Add generated client expansions - Add kubectl 'get' and 'describe' support for Template Api objects --- pkg/api/validation/schema.go | 31 +++-- pkg/apis/extensions/register.go | 7 + pkg/apis/extensions/types.go | 78 +++++++++++ pkg/apis/extensions/v1beta1/defaults.go | 31 +++++ pkg/apis/extensions/v1beta1/register.go | 7 + pkg/apis/extensions/v1beta1/types.go | 78 +++++++++++ pkg/apis/extensions/validation/validation.go | 24 ++++ .../fake/fake_template_expansions.go | 34 +++++ .../unversioned/template_expansion.go | 33 +++++ pkg/kubectl/cmd/cmd.go | 1 + pkg/kubectl/describe.go | 34 +++++ pkg/kubectl/resource_printer.go | 31 +++++ pkg/master/master.go | 9 ++ pkg/registry/cachesize/cachesize.go | 2 + pkg/registry/template/doc.go | 19 +++ pkg/registry/template/etcd/etcd.go | 112 +++++++++++++++ pkg/registry/template/etcd/etcd_test.go | 128 ++++++++++++++++++ pkg/registry/template/strategy.go | 108 +++++++++++++++ 18 files changed, 758 insertions(+), 9 deletions(-) create mode 100644 pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/fake/fake_template_expansions.go create mode 100644 pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/template_expansion.go create mode 100644 pkg/registry/template/doc.go create mode 100644 pkg/registry/template/etcd/etcd.go create mode 100644 pkg/registry/template/etcd/etcd_test.go create mode 100644 pkg/registry/template/strategy.go diff --git a/pkg/api/validation/schema.go b/pkg/api/validation/schema.go index c52a0b6d71540..0fddf426f3bb2 100644 --- a/pkg/api/validation/schema.go +++ b/pkg/api/validation/schema.go @@ -94,37 +94,44 @@ func (s *SwaggerSchema) validateItems(items interface{}) []error { if !ok { return append(allErrs, fmt.Errorf("items isn't a slice")) } - for i, item := range itemList { + return s.validateObjectSlice("items", itemList) +} + +// validateObjectSlice validates a field on an Object that is a collection of Generic objects. +// Each object in slice has its metadata fields checked and is then passed to ValidateObject +func (s *SwaggerSchema) validateObjectSlice(fieldName string, slice []interface{}) []error { + allErrs := []error{} + for i, item := range slice { fields, ok := item.(map[string]interface{}) if !ok { - allErrs = append(allErrs, fmt.Errorf("items[%d] isn't a map[string]interface{}", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d] isn't a map[string]interface{}", fieldName, i)) continue } groupVersion := fields["apiVersion"] if groupVersion == nil { - allErrs = append(allErrs, fmt.Errorf("items[%d].apiVersion not set", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].apiVersion not set", fieldName, i)) continue } itemVersion, ok := groupVersion.(string) if !ok { - allErrs = append(allErrs, fmt.Errorf("items[%d].apiVersion isn't string type", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].apiVersion isn't string type", fieldName, i)) continue } if len(itemVersion) == 0 { - allErrs = append(allErrs, fmt.Errorf("items[%d].apiVersion is empty", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].apiVersion is empty", fieldName, i)) } kind := fields["kind"] if kind == nil { - allErrs = append(allErrs, fmt.Errorf("items[%d].kind not set", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].kind not set", fieldName, i)) continue } itemKind, ok := kind.(string) if !ok { - allErrs = append(allErrs, fmt.Errorf("items[%d].kind isn't string type", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].kind isn't string type", fieldName, i)) continue } if len(itemKind) == 0 { - allErrs = append(allErrs, fmt.Errorf("items[%d].kind is empty", i)) + allErrs = append(allErrs, fmt.Errorf("%s[%d].kind is empty", fieldName, i)) } version := apiutil.GetVersion(itemVersion) errs := s.ValidateObject(item, "", version+"."+itemKind) @@ -226,7 +233,13 @@ func (s *SwaggerSchema) ValidateObject(obj interface{}, fieldName, typeName stri // This is because the actual values will be of some sub-type (e.g. Deployment) not the expected // super-type (RawExtention) if s.isGenericArray(details) { - errs := s.validateItems(value) + allErrs := []error{} + genericSlice, ok := value.([]interface{}) + if !ok { + allErrs = append(allErrs, fmt.Errorf("genericSlice isn't a slice")) + continue + } + errs := s.validateObjectSlice(key, genericSlice) if len(errs) > 0 { allErrs = append(allErrs, errs...) } diff --git a/pkg/apis/extensions/register.go b/pkg/apis/extensions/register.go index 0e6482a97e8a6..bcc685eae89ec 100644 --- a/pkg/apis/extensions/register.go +++ b/pkg/apis/extensions/register.go @@ -67,12 +67,16 @@ func addKnownTypes(scheme *runtime.Scheme) { &ThirdPartyResourceDataList{}, &Ingress{}, &IngressList{}, + &api.List{}, &api.ListOptions{}, &ReplicaSet{}, &ReplicaSetList{}, &api.ExportOptions{}, &PodSecurityPolicy{}, &PodSecurityPolicyList{}, + &Template{}, + &TemplateList{}, + &TemplateParameters{}, ) } @@ -93,3 +97,6 @@ func (obj *ReplicaSet) GetObjectKind() unversioned.ObjectKind { func (obj *ReplicaSetList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *PodSecurityPolicy) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *PodSecurityPolicyList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Template) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *TemplateList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *TemplateParameters) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 7a307d2c7e878..b1bcc9fef68e3 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -32,6 +32,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/intstr" ) @@ -809,3 +810,80 @@ type PodSecurityPolicyList struct { Items []PodSecurityPolicy `json:"items"` } + +// +genclient=true + +// Template defines a list of partially complete Objects and a list of Parameters that +// can be processed into a Config by substituting parameterized values at processing time. +// Parameter values maybe set at processing time. +type Template struct { + unversioned.TypeMeta `json:",inline"` + api.ObjectMeta `json:"metadata,omitempty"` + + Spec TemplateSpec `json:"spec"` +} + +type TemplateSpec struct { + // Parameters is a list of Parameters used to substitute values into Template Objects + Parameters []Parameter `json:"parameters,omitempty"` + + // Objects is a list of partially complete Objects with substitution symbols. + Objects []runtime.RawExtension `json:"objects"` +} + +// TemplateList is a list of Template objects. +type TemplateList struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta `json:"metadata,omitempty"` + Items []Template `json:"items"` +} + +const ( + StringParam = "string" + IntParam = "integer" + BoolParam = "boolean" + Base64Param = "base64" +) + +// Parameter defines a name/value variable that is substituted into Template Objects at +// Template processing time. +type Parameter struct { + // Name defines the symbol to be replaced. Name should appear as $(Name) in Objects. + Name string `json:"name"` + + // DisplayName is used instead of Name when displaying the Parameter in a UI + DisplayName string `json:"displayName,omitempty"` + + // Description is used to give context about the Parameter, such as its purpose + // and what values are acceptable. + Description string `json:"description,omitempty"` + + // Value holds a default value to replace all occurrences of $(Name) within the Template's + // Objects when the Template is processed. The Value maybe overridden at Template + // processing time. If no Value is provided as a default or override, then + // the empty string will be used for substitution. + Value string `json:"value,omitempty"` + + // Required indicates the parameter must have a non-empty value when the Template is processed. + // Parameters of Type 'integer' and 'boolean' are always Required. + Required bool `json:"required,omitempty"` + + // Type is the type that the parameter value must be parsed to. Type may be one of + // 'string', 'integer', 'boolean', or 'base64'. + // Type is used by clients to provide validation of user input and direction to users. + // Parameters used to define integer or boolean fields (e.g. replicaCount) should have the + // Type set to integer or boolean accordingly. + Type string `json:"type,omitempty"` +} + +// TemplateParameters contains the substitution parameter overrides when processing a Template +type TemplateParameters struct { + unversioned.TypeMeta `json:",inline"` + + // Name is the name of the Template to be processed. + Name string `json:"name"` + + // ParameterValues is a map of substitution parameter name:value pairs to be expanded in the Template. + // Values defined in this map will override any defaults already set in the Template. + ParameterValues map[string]string `json:"parameters,omitempty"` +} diff --git a/pkg/apis/extensions/v1beta1/defaults.go b/pkg/apis/extensions/v1beta1/defaults.go index bf4989da0526e..eb1f55281e087 100644 --- a/pkg/apis/extensions/v1beta1/defaults.go +++ b/pkg/apis/extensions/v1beta1/defaults.go @@ -28,6 +28,8 @@ func addDefaultingFuncs(scheme *runtime.Scheme) { SetDefaults_Job, SetDefaults_HorizontalPodAutoscaler, SetDefaults_ReplicaSet, + SetDefaults_Parameter, + SetDefaults_TemplateParameters, ) } @@ -150,3 +152,32 @@ func SetDefaults_ReplicaSet(obj *ReplicaSet) { *obj.Spec.Replicas = 1 } } + +func SetDefaults_Parameter(obj *Parameter) { + if obj.Required == nil { + obj.Required = new(bool) + *obj.Required = false + } + if obj.Type == nil { + obj.Type = new(string) + *obj.Type = StringParam + } + if obj.Value == nil { + obj.Value = new(string) + *obj.Value = "" + } + if obj.DisplayName == nil { + obj.DisplayName = new(string) + *obj.DisplayName = "" + } + if obj.Description == nil { + obj.Description = new(string) + *obj.Description = "" + } +} + +func SetDefaults_TemplateParameters(obj *TemplateParameters) { + if obj.ParameterValues == nil { + obj.ParameterValues = map[string]string{} + } +} diff --git a/pkg/apis/extensions/v1beta1/register.go b/pkg/apis/extensions/v1beta1/register.go index ee662c4631473..a4f3dec65090e 100644 --- a/pkg/apis/extensions/v1beta1/register.go +++ b/pkg/apis/extensions/v1beta1/register.go @@ -55,12 +55,16 @@ func addKnownTypes(scheme *runtime.Scheme) { &ThirdPartyResourceDataList{}, &Ingress{}, &IngressList{}, + &v1.List{}, &ListOptions{}, &v1.DeleteOptions{}, &ReplicaSet{}, &ReplicaSetList{}, &PodSecurityPolicy{}, &PodSecurityPolicyList{}, + &Template{}, + &TemplateList{}, + &TemplateParameters{}, ) // Add the watch version that applies versionedwatch.AddToGroupVersion(scheme, SchemeGroupVersion) @@ -88,3 +92,6 @@ func (obj *ReplicaSet) GetObjectKind() unversioned.ObjectKind { func (obj *ReplicaSetList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *PodSecurityPolicy) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } func (obj *PodSecurityPolicyList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *Template) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *TemplateList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } +func (obj *TemplateParameters) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index b7a255d6bf0ea..726208c76f42d 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -20,6 +20,7 @@ import ( "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/intstr" ) @@ -1102,3 +1103,80 @@ type PodSecurityPolicyList struct { // Items is a list of schema objects. Items []PodSecurityPolicy `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// +genclient=true + +// Template defines a list of partially complete Objects and a list of Parameters that +// can be processed into a Config by substituting parameterized values at processing time. +// Parameter values maybe set at processing time. +type Template struct { + unversioned.TypeMeta `json:",inline"` + v1.ObjectMeta `json:"metadata,omitempty"` + + Spec TemplateSpec `json:"spec"` +} + +type TemplateSpec struct { + // Parameters is a list of Parameters used to substitute values into Template Objects + Parameters []Parameter `json:"parameters,omitempty"` + + // Objects is a list of partially complete Objects with substitution symbols. + Objects []runtime.RawExtension `json:"objects"` +} + +// TemplateList is a list of Template objects. +type TemplateList struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta `json:"metadata,omitempty"` + Items []Template `json:"items,omitempty"` +} + +const ( + StringParam = "string" + IntParam = "integer" + BoolParam = "boolean" + Base64Param = "base64" +) + +// Parameter defines a name/value variable that is substituted into Template Objects at +// Template processing time. +type Parameter struct { + // Name defines the symbol to be replaced. Name should appear as $(Name) in Objects. + Name string `json:"name"` + + // DisplayName is used instead of Name when displaying the Parameter in a UI + DisplayName *string `json:"displayName,omitempty"` + + // Description is used to give context about the Parameter, such as its purpose + // and what values are acceptable. + Description *string `json:"description,omitempty"` + + // Value holds a default value to replace all occurrences of $(Name) within the Template's + // Objects when the Template is processed. The Value maybe overridden at Template + // processing time. If no Value is provided as a default or override, then + // the empty string will be used for substitution. + Value *string `json:"value,omitempty"` + + // Required indicates the parameter must have a non-empty value when the Template is processed. + // Parameters of Type 'integer' and 'boolean' are always Required. + Required *bool `json:"required,omitempty"` + + // Type is the type that the parameter value must be parsed to. Type may be one of + // 'string', 'integer', 'boolean', or 'base64'. + // Type is used by clients to provide validation of user input and direction to users. + // Parameters used to define integer or boolean fields (e.g. replicaCount) should have the + // Type set to integer or boolean accordingly. + Type *string `json:"type,omitempty"` +} + +// TemplateParameters contains the substitution parameter overrides when processing a Template +type TemplateParameters struct { + unversioned.TypeMeta `json:",inline"` + + // Name is the name of the Template to be processed. + Name string `json:"name"` + + // ParameterValues is a map of substitution parameter name:value pairs to be expanded in the Template. + // Values defined in this map will override any defaults already set in the Template. + ParameterValues map[string]string `json:"parameters,omitempty"` +} diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index e621466feedc5..65c73abb4a293 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -146,6 +146,11 @@ func ValidateDeploymentName(name string, prefix bool) (bool, string) { return apivalidation.NameIsDNSSubdomain(name, prefix) } +// Validates that the given name can be used as a Template name. +func ValidateTemplateName(name string, prefix bool) (bool, string) { + return apivalidation.NameIsDNSSubdomain(name, prefix) +} + func ValidatePositiveIntOrPercent(intOrPercent intstr.IntOrString, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if intOrPercent.Type == intstr.String { @@ -687,3 +692,22 @@ func ValidatePodSecurityPolicyUpdate(old *extensions.PodSecurityPolicy, new *ext allErrs = append(allErrs, ValidatePodSecurityPolicySpec(&new.Spec, field.NewPath("spec"))...) return allErrs } + +// Template Api Validation +func ValidateTemplateSpec(spec *extensions.TemplateSpec, fldPath *field.Path) field.ErrorList { + // TODO: Write this + allErrs := field.ErrorList{} + return allErrs +} + +func ValidateTemplateUpdate(update, old *extensions.Template) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateTemplateSpec(&update.Spec, field.NewPath("spec"))...) + return allErrs +} + +func ValidateTemplate(obj *extensions.Template) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateTemplateName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateTemplateSpec(&obj.Spec, field.NewPath("spec"))...) + return allErrs +} diff --git a/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/fake/fake_template_expansions.go b/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/fake/fake_template_expansions.go new file mode 100644 index 0000000000000..4655a50c7bfd0 --- /dev/null +++ b/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/fake/fake_template_expansions.go @@ -0,0 +1,34 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fake + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/testing/core" +) + +func (c *FakeTemplates) Process(tp *extensions.TemplateParameters) (*api.List, error) { + action := core.CreateActionImpl{} + action.Verb = "processed" + action.Resource = templatesResource + action.Subresource = "process" + action.Object = tp + + obj, err := c.Fake.Invokes(action, tp) + return obj.(*api.List), err +} diff --git a/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/template_expansion.go b/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/template_expansion.go new file mode 100644 index 0000000000000..e4a7672050cff --- /dev/null +++ b/pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned/template_expansion.go @@ -0,0 +1,33 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package unversioned + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +// The TemplateExpansion interface allows manually adding extra methods to the TemplateInterface. +type TemplateExpansion interface { + Process(tp *extensions.TemplateParameters) (*api.List, error) +} + +func (t *templates) Process(tp *extensions.TemplateParameters) (result *api.List, err error) { + result = &api.List{} + err = t.client.Post().Namespace(t.ns).Resource("templates").SubResource("process").Body(tp).Do().Into(result) + return +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index c0687779e2052..40326409e495f 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -171,6 +171,7 @@ __custom_func() { * resourcequotas (aka 'quota') * replicasets (aka 'rs') * replicationcontrollers (aka 'rc') + * templates * secrets * serviceaccounts (aka 'sa') * services (aka 'svc') diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index c0189f56041dd..aa6d9e182b8e0 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -44,6 +44,7 @@ import ( "k8s.io/kubernetes/pkg/fields" qosutil "k8s.io/kubernetes/pkg/kubelet/qos/util" "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/types" deploymentutil "k8s.io/kubernetes/pkg/util/deployment" "k8s.io/kubernetes/pkg/util/intstr" @@ -105,6 +106,7 @@ func describerMap(c *client.Client) map[unversioned.GroupKind]Describer { extensions.Kind("DaemonSet"): &DaemonSetDescriber{c}, extensions.Kind("Deployment"): &DeploymentDescriber{adapter.FromUnversionedClient(c)}, extensions.Kind("Job"): &JobDescriber{c}, + extensions.Kind("Template"): &TemplateDescriber{adapter.FromUnversionedClient(c)}, batch.Kind("Job"): &JobDescriber{c}, apps.Kind("PetSet"): &PetSetDescriber{c}, extensions.Kind("Ingress"): &IngressDescriber{c}, @@ -1231,6 +1233,38 @@ func describeSecret(secret *api.Secret) (string, error) { }) } +type TemplateDescriber struct { + clientset.Interface +} + +func (td *TemplateDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { + temp, err := td.Extensions().Templates(namespace).Get(name) + if err != nil { + return "", err + } + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%v\n", temp.Name) + fmt.Fprintf(out, "Namespace:\t%v\n", temp.Namespace) + fmt.Fprintf(out, "Parameters (Count: %v):\n Name\tDisplayName\tRequired\tType\tValue\tDescription\n", len(temp.Spec.Parameters)) + fmt.Fprint(out, " ----\t--------\t--------\t----\t------\t-----------\n") + for _, p := range temp.Spec.Parameters { + fmt.Fprintf(out, " %s\t%s\t%t\t%s\t%s\t%s\n", + p.Name, p.DisplayName, p.Required, p.Type, p.Value, p.Description) + } + fmt.Fprintf(out, "Items (Count: %v):\n Name\tApiGroupVersion\tKind\n", len(temp.Spec.Objects)) + fmt.Fprint(out, " ----\t---------------\t----\n") + for _, i := range temp.Spec.Objects { + decodedObj, err := runtime.Decode(runtime.UnstructuredJSONScheme, i.Raw) + unstructured := decodedObj.(*runtime.Unstructured) + if err != nil { + return err + } + fmt.Fprintf(out, " %s\t%s\t%s\n", unstructured.GetName(), unstructured.GetAPIVersion(), unstructured.GroupVersionKind().Kind) + } + return nil + }) +} + type IngressDescriber struct { client.Interface } diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index e29a952d27cc4..cf3724a1ac336 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -442,6 +442,7 @@ var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print var deploymentColumns = []string{"NAME", "DESIRED", "CURRENT", "UP-TO-DATE", "AVAILABLE", "AGE"} var configMapColumns = []string{"NAME", "DATA", "AGE"} var podSecurityPolicyColumns = []string{"NAME", "PRIV", "CAPS", "VOLUMEPLUGINS", "SELINUX", "RUNASUSER"} +var templateColumns = []string{"NAME", "AGE"} // addDefaultHandlers adds print handlers for default Kubernetes types. func (h *HumanReadablePrinter) addDefaultHandlers() { @@ -497,6 +498,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(podSecurityPolicyColumns, printPodSecurityPolicyList) h.Handler(thirdPartyResourceDataColumns, printThirdPartyResourceData) h.Handler(thirdPartyResourceDataColumns, printThirdPartyResourceDataList) + h.Handler(templateColumns, printTemplate) + h.Handler(templateColumns, printTemplateList) } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { @@ -1613,6 +1616,34 @@ func printDeploymentList(list *extensions.DeploymentList, w io.Writer, options P return nil } +func printTemplate(template *extensions.Template, w io.Writer, options PrintOptions) error { + if options.WithNamespace { + if _, err := fmt.Fprintf(w, "%s\t", template.Namespace); err != nil { + return err + } + } + + age := translateTimestamp(template.CreationTimestamp) + + if _, err := fmt.Fprintf(w, "%s\t%s", template.Name, age); err != nil { + return err + } + if _, err := fmt.Fprint(w, appendLabels(template.Labels, options.ColumnLabels)); err != nil { + return err + } + _, err := fmt.Fprint(w, appendAllLabels(options.ShowLabels, template.Labels)) + return err +} + +func printTemplateList(list *extensions.TemplateList, w io.Writer, options PrintOptions) error { + for _, item := range list.Items { + if err := printTemplate(&item, w, options); err != nil { + return err + } + } + return nil +} + func printHorizontalPodAutoscaler(hpa *autoscaling.HorizontalPodAutoscaler, w io.Writer, options PrintOptions) error { namespace := hpa.Namespace name := hpa.Name diff --git a/pkg/master/master.go b/pkg/master/master.go index 0e576b5c4cbb4..62d1bd88c5213 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -80,6 +80,7 @@ import ( serviceetcd "k8s.io/kubernetes/pkg/registry/service/etcd" ipallocator "k8s.io/kubernetes/pkg/registry/service/ipallocator" serviceaccountetcd "k8s.io/kubernetes/pkg/registry/serviceaccount/etcd" + templateetcd "k8s.io/kubernetes/pkg/registry/template/etcd" thirdpartyresourceetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresource/etcd" "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" thirdpartyresourcedataetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata/etcd" @@ -846,6 +847,13 @@ func (m *Master) getExtensionResources(c *Config) map[string]rest.Storage { storage["replicasets/scale"] = replicaSetStorage.Scale } + if c.APIResourceConfigSource.ResourceEnabled(version.WithResource("templates")) { + templatesStorage, templateProcess := templateetcd.NewREST(restOptions("templates")) + // TODO: Add an endpoint for processing a template without storing it (kubernetes/kuberenetes#25373) + storage["templates"] = templatesStorage + storage["templates/process"] = templateProcess + } + return storage } @@ -965,6 +973,7 @@ func DefaultAPIResourceConfigSource() *genericapiserver.ResourceConfig { extensionsapiv1beta1.SchemeGroupVersion.WithResource("jobs"), extensionsapiv1beta1.SchemeGroupVersion.WithResource("replicasets"), extensionsapiv1beta1.SchemeGroupVersion.WithResource("thirdpartyresources"), + extensionsapiv1beta1.SchemeGroupVersion.WithResource("templates"), ) return ret diff --git a/pkg/registry/cachesize/cachesize.go b/pkg/registry/cachesize/cachesize.go index 99161e1093e16..247e34fb95a44 100644 --- a/pkg/registry/cachesize/cachesize.go +++ b/pkg/registry/cachesize/cachesize.go @@ -49,6 +49,7 @@ const ( Secrets Resource = "secrets" ServiceAccounts Resource = "serviceaccounts" Services Resource = "services" + Templates Resource = "templates" ) var watchCacheSizes map[Resource]int @@ -76,6 +77,7 @@ func init() { watchCacheSizes[Secrets] = 100 watchCacheSizes[ServiceAccounts] = 100 watchCacheSizes[Services] = 100 + watchCacheSizes[Templates] = 100 } func SetWatchCacheSizes(cacheSizes []string) { diff --git a/pkg/registry/template/doc.go b/pkg/registry/template/doc.go new file mode 100644 index 0000000000000..06f18aebaec1b --- /dev/null +++ b/pkg/registry/template/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package template provides TemplateProcessor, capable of +// transforming Template objects into Config objects. +package template diff --git a/pkg/registry/template/etcd/etcd.go b/pkg/registry/template/etcd/etcd.go new file mode 100644 index 0000000000000..1e3e1c7c09302 --- /dev/null +++ b/pkg/registry/template/etcd/etcd.go @@ -0,0 +1,112 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "fmt" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/cachesize" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/registry/generic/registry" + "k8s.io/kubernetes/pkg/registry/template" + "k8s.io/kubernetes/pkg/runtime" +) + +// REST implements a RESTStorage for templates against etcd +type REST struct { + *registry.Store +} + +// NewREST returns a RESTStorage object that will work against replication controllers. +func NewREST(opts generic.RESTOptions) (*REST, *ProcessREST) { + prefix := "/templates" + + newListFunc := func() runtime.Object { return &extensions.TemplateList{} } + storageInterface := opts.Decorator( + opts.Storage, cachesize.GetWatchCacheSizeByResource(cachesize.Templates), &extensions.Template{}, prefix, template.Strategy, newListFunc) + + store := ®istry.Store{ + NewFunc: func() runtime.Object { return &extensions.Template{} }, + + // NewListFunc returns an object capable of storing results of an etcd list. + NewListFunc: newListFunc, + // Produces a templates that etcd understands, to the root of the resource + // by combining the namespace in the context with the given prefix + KeyRootFunc: func(ctx api.Context) string { + return registry.NamespaceKeyRootFunc(ctx, prefix) + }, + // Produces a templates that etcd understands, to the resource by combining + // the namespace in the context with the given prefix + KeyFunc: func(ctx api.Context, name string) (string, error) { + return registry.NamespaceKeyFunc(ctx, prefix, name) + }, + // Retrieve the name field of a template + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*extensions.Template).Name, nil + }, + // Used to match objects based on labels/fields for list and watch + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return template.Matcher(label, field) + }, + QualifiedResource: extensions.Resource("templates"), + DeleteCollectionWorkers: opts.DeleteCollectionWorkers, + + // Used to validate template creation + CreateStrategy: template.Strategy, + + // Used to validate template updates + UpdateStrategy: template.Strategy, + + // Used to validate template deletion + DeleteStrategy: template.Strategy, + + Storage: storageInterface, + } + return &REST{store}, &ProcessREST{store} +} + +type ProcessREST struct { + store *registry.Store +} + +func (r *ProcessREST) New() runtime.Object { + return &extensions.TemplateParameters{} +} + +func (r *ProcessREST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { + tparams, ok := obj.(*extensions.TemplateParameters) + if !ok { + return nil, fmt.Errorf("expected input object type to be TemplateParameters, but %T", obj) + } + return r.processTemplate(ctx, tparams) +} + +func (r *ProcessREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + tparams, ok := obj.(*extensions.TemplateParameters) + if !ok { + return nil, false, fmt.Errorf("expected input object type to be TemplateParameters, but %T", obj) + } + obj, err := r.processTemplate(ctx, tparams) + return obj, true, err +} + +func (r *ProcessREST) processTemplate(ctx api.Context, obj *extensions.TemplateParameters) (runtime.Object, error) { + return &api.List{}, nil +} diff --git a/pkg/registry/template/etcd/etcd_test.go b/pkg/registry/template/etcd/etcd_test.go new file mode 100644 index 0000000000000..1ae82cee2c2c7 --- /dev/null +++ b/pkg/registry/template/etcd/etcd_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/registry/registrytest" + "k8s.io/kubernetes/pkg/runtime" + etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing" +) + +var namespace = "foo-namespace" +var name = "foo-template" + +func newRest(t *testing.T) (*REST, *ProcessREST, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, extensions.GroupName) + restOptions := generic.RESTOptions{Storage: etcdStorage, Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1} + rest, processRest := NewREST(restOptions) + return rest, processRest, server +} + +func validNewTemplate() *extensions.Template { + return &extensions.Template{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: extensions.TemplateSpec{ + Objects: []runtime.RawExtension{}, + Parameters: []extensions.Parameter{}, + }, + } +} + +func TestCreate(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + template := validNewTemplate() + template.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + template, + // TODO: Add invalid cases once template Validation is implemented + ) +} + +func TestUpdate(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + test.TestUpdate( + // valid object + validNewTemplate(), + // valid update + func(obj runtime.Object) runtime.Object { + object := obj.(*extensions.Template) + return object + }, + // TODO: Add invalid cases once template Validation is implemented + ) +} + +func TestDelete(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + test.TestDelete(validNewTemplate()) +} + +func TestGet(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + test.TestGet(validNewTemplate()) +} + +func TestList(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + test.TestList(validNewTemplate()) +} + +func TestWatch(t *testing.T) { + rest, _, server := newRest(t) + defer server.Terminate(t) + test := registrytest.New(t, rest.Store) + test.TestWatch( + validNewTemplate(), + // matching labels + []labels.Set{}, + // not matching labels + []labels.Set{ + {"a": "c"}, + {"foo": "bar"}, + }, + // matching fields + []fields.Set{ + {"metadata.name": name}, + }, + // not matchin fields + []fields.Set{ + {"metadata.name": "bar"}, + {"name": name}, + }, + ) +} diff --git a/pkg/registry/template/strategy.go b/pkg/registry/template/strategy.go new file mode 100644 index 0000000000000..0bf42eafb9da8 --- /dev/null +++ b/pkg/registry/template/strategy.go @@ -0,0 +1,108 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package template + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apis/extensions/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// templateStrategy implements behavior for Templates +type templateStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating Template +// objects via the REST API. +var Strategy = templateStrategy{api.Scheme, api.SimpleNameGenerator} + +var _ rest.NamespaceScopedStrategy = Strategy +var _ rest.RESTCreateStrategy = Strategy +var _ rest.RESTDeleteStrategy = Strategy +var _ rest.RESTUpdateStrategy = Strategy + +//var _ rest.RESTExportStrategy = Strategy + +// NamespaceScoped is true for templates. +func (templateStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears fields that are not allowed to be set by end users on creation. +func (templateStrategy) PrepareForCreate(obj runtime.Object) { +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (templateStrategy) PrepareForUpdate(obj, old runtime.Object) { +} + +// Canonicalize normalizes the object after validation. +func (templateStrategy) Canonicalize(obj runtime.Object) { + // TODO: write this +} + +// Validate validates a new template. +func (templateStrategy) Validate(ctx api.Context, obj runtime.Object) field.ErrorList { + return validation.ValidateTemplate(obj.(*extensions.Template)) +} + +// AllowCreateOnUpdate is false for templates. +func (templateStrategy) AllowCreateOnUpdate() bool { + return false +} + +func (templateStrategy) AllowUnconditionalUpdate() bool { + return true +} + +// ValidateUpdate is the default update validation for an end user. +func (templateStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateTemplateUpdate(obj.(*extensions.Template), old.(*extensions.Template)) +} + +// templateToSelectableFields returns a label set that represents the object +// changes to the returned keys require registering conversions for existing versions using Scheme.AddFieldLabelConversionFunc +func TemplateToSelectableFields(template *extensions.Template) fields.Set { + return fields.Set{ + "metadata.name": template.Name, + } +} + +// Matcher returns a generic matcher for a given label and field selector. +func Matcher(label labels.Selector, field fields.Selector) generic.Matcher { + return &generic.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { + template, ok := obj.(*extensions.Template) + if !ok { + return nil, nil, fmt.Errorf("given object is not a template.") + } + return labels.Set(template.ObjectMeta.Labels), TemplateToSelectableFields(template), nil + }, + } +}