Introduce new generator for apps/v1beta1 deployments

This commit is contained in:
Maciej Szulik 2017-03-08 11:11:29 +01:00
parent 1049dad0a4
commit aa4390750c
13 changed files with 578 additions and 69 deletions

View File

@ -1046,11 +1046,23 @@ run_kubectl_run_tests() {
# Pre-Condition: no Deployment exists
kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" ''
# Command
kubectl run nginx "--image=$IMAGE_NGINX" --generator=deployment/v1beta1 "${kube_flags[@]}"
kubectl run nginx-extensions "--image=$IMAGE_NGINX" "${kube_flags[@]}"
# Post-Condition: Deployment "nginx" is created
kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" 'nginx:'
kube::test::get_object_assert deployment.extensions "{{range.items}}{{$id_field}}:{{end}}" 'nginx-extensions:'
# and old generator was used, iow. old defaults are applied
output_message=$(kubectl get deployment.extensions/nginx-extensions -o jsonpath='{.spec.revisionHistoryLimit}')
kube::test::if_has_not_string "${output_message}" '2'
# Clean up
kubectl delete deployment nginx "${kube_flags[@]}"
kubectl delete deployment nginx-extensions "${kube_flags[@]}"
# Command
kubectl run nginx-apps "--image=$IMAGE_NGINX" --generator=deployment/apps.v1beta1 "${kube_flags[@]}"
# Post-Condition: Deployment "nginx" is created
kube::test::get_object_assert deployment.apps "{{range.items}}{{$id_field}}:{{end}}" 'nginx-apps:'
# and new generator was used, iow. new defaults are applied
output_message=$(kubectl get deployment/nginx-apps -o jsonpath='{.spec.revisionHistoryLimit}')
kube::test::if_has_string "${output_message}" '2'
# Clean up
kubectl delete deployment nginx-apps "${kube_flags[@]}"
}
run_kubectl_get_tests() {
@ -2283,17 +2295,35 @@ run_rc_tests() {
}
run_deployment_tests() {
# Test kubectl create deployment
kubectl create deployment test-nginx --image=gcr.io/google-containers/nginx:test-cmd
# Post-Condition: Deployment has 2 replicas defined in its spec.
kube::test::get_object_assert 'deploy test-nginx' "{{$container_name_field}}" 'nginx'
# Test kubectl create deployment (using default - old generator)
kubectl create deployment test-nginx-extensions --image=gcr.io/google-containers/nginx:test-cmd
# Post-Condition: Deployment "nginx" is created.
kube::test::get_object_assert 'deploy test-nginx-extensions' "{{$container_name_field}}" 'nginx'
# and old generator was used, iow. old defaults are applied
output_message=$(kubectl get deployment.extensions/test-nginx-extensions -o jsonpath='{.spec.revisionHistoryLimit}')
kube::test::if_has_not_string "${output_message}" '2'
# Ensure we can interact with deployments through extensions and apps endpoints
output_message=$(kubectl get deployment.extensions -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'extensions/v1beta1'
output_message=$(kubectl get deployment.apps -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'apps/v1beta1'
# Clean up
kubectl delete deployment test-nginx "${kube_flags[@]}"
kubectl delete deployment test-nginx-extensions "${kube_flags[@]}"
# Test kubectl create deployment
kubectl create deployment test-nginx-apps --image=gcr.io/google-containers/nginx:test-cmd --generator=deployment-basic/apps.v1beta1
# Post-Condition: Deployment "nginx" is created.
kube::test::get_object_assert 'deploy test-nginx-apps' "{{$container_name_field}}" 'nginx'
# and new generator was used, iow. new defaults are applied
output_message=$(kubectl get deployment/test-nginx-apps -o jsonpath='{.spec.revisionHistoryLimit}')
kube::test::if_has_string "${output_message}" '2'
# Ensure we can interact with deployments through extensions and apps endpoints
output_message=$(kubectl get deployment.extensions -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'extensions/v1beta1'
output_message=$(kubectl get deployment.apps -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'apps/v1beta1'
# Clean up
kubectl delete deployment test-nginx-apps "${kube_flags[@]}"
### Test cascading deletion
## Test that rs is deleted when deployment is deleted.

View File

@ -53,6 +53,7 @@ go_library(
"//pkg/api/util:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/apps/v1beta1:go_default_library",
"//pkg/apis/autoscaling:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/batch/v1:go_default_library",
@ -137,6 +138,7 @@ go_test(
"//pkg/api/testapi:go_default_library",
"//pkg/api/testing:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/apps/v1beta1:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/batch/v1:go_default_library",
"//pkg/apis/batch/v2alpha1:go_default_library",

View File

@ -70,6 +70,7 @@ go_library(
"//pkg/api/annotations:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/api/validation:go_default_library",
"//pkg/apis/apps/v1beta1:go_default_library",
"//pkg/apis/batch/v1:go_default_library",
"//pkg/apis/certificates:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",

View File

@ -94,7 +94,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
cmd.AddCommand(NewCmdCreateConfigMap(f, out))
cmd.AddCommand(NewCmdCreateServiceAccount(f, out))
cmd.AddCommand(NewCmdCreateService(f, out, errOut))
cmd.AddCommand(NewCmdCreateDeployment(f, out))
cmd.AddCommand(NewCmdCreateDeployment(f, out, errOut))
cmd.AddCommand(NewCmdCreateClusterRole(f, out))
cmd.AddCommand(NewCmdCreateClusterRoleBinding(f, out))
cmd.AddCommand(NewCmdCreateRole(f, out))

View File

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
@ -38,7 +39,7 @@ var (
)
// NewCmdCreateDeployment is a macro command to create a new deployment
func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut, cmdErr io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "deployment NAME --image=image [--dry-run]",
Aliases: []string{"deploy"},
@ -46,7 +47,7 @@ func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command
Long: deploymentLong,
Example: deploymentExample,
Run: func(cmd *cobra.Command, args []string) {
err := CreateDeployment(f, cmdOut, cmd, args)
err := CreateDeployment(f, cmdOut, cmdErr, cmd, args)
cmdutil.CheckErr(err)
},
}
@ -60,13 +61,33 @@ func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command
}
// CreateDeployment implements the behavior to run the create deployment command
func CreateDeployment(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, args []string) error {
func CreateDeployment(f cmdutil.Factory, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string) error {
name, err := NameFromCommandArgs(cmd, args)
if err != nil {
return err
}
clientset, err := f.ClientSet()
if err != nil {
return err
}
resourcesList, err := clientset.Discovery().ServerResources()
// ServerResources ignores errors for old servers do not expose discovery
if err != nil {
return fmt.Errorf("failed to discover supported resources: %v", err)
}
generatorName := cmdutil.GetFlagString(cmd, "generator")
// fallback to the old generator if server does not support apps/v1beta1 deployments
if generatorName == cmdutil.DeploymentBasicAppsV1Beta1GeneratorName &&
!contains(resourcesList, appsv1beta1.SchemeGroupVersion.WithResource("deployments")) {
fmt.Fprintf(cmdErr, "WARNING: New deployments generator specified (%s), but apps/v1beta1.Deployments are not available, falling back to the old one (%s).\n",
cmdutil.DeploymentBasicAppsV1Beta1GeneratorName, cmdutil.DeploymentBasicV1Beta1GeneratorName)
generatorName = cmdutil.DeploymentBasicV1Beta1GeneratorName
}
var generator kubectl.StructuredGenerator
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
switch generatorName {
case cmdutil.DeploymentBasicAppsV1Beta1GeneratorName:
generator = &kubectl.DeploymentBasicAppsGeneratorV1{Name: name, Images: cmdutil.GetFlagStringSlice(cmd, "image")}
case cmdutil.DeploymentBasicV1Beta1GeneratorName:
generator = &kubectl.DeploymentBasicGeneratorV1{Name: name, Images: cmdutil.GetFlagStringSlice(cmd, "image")}
default:

View File

@ -18,20 +18,37 @@ package cmd
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake"
"k8s.io/kubernetes/pkg/api"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
)
func TestCreateDeployment(t *testing.T) {
depName := "jonny-dep"
f, tf, _, _ := cmdtesting.NewAPIFactory()
f, tf, _, ns := cmdtesting.NewAPIFactory()
tf.Client = &fake.RESTClient{
APIRegistry: api.Registry,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(&bytes.Buffer{}),
}, nil
}),
}
tf.ClientConfig = &restclient.Config{}
tf.Printer = &testPrinter{}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdCreateDeployment(f, buf)
cmd := NewCmdCreateDeployment(f, buf, buf)
cmd.Flags().Set("dry-run", "true")
cmd.Flags().Set("output", "name")
cmd.Flags().Set("image", "hollywood/jonny.depp:v2")
@ -44,13 +61,25 @@ func TestCreateDeployment(t *testing.T) {
func TestCreateDeploymentNoImage(t *testing.T) {
depName := "jonny-dep"
f, tf, _, _ := cmdtesting.NewAPIFactory()
f, tf, _, ns := cmdtesting.NewAPIFactory()
tf.Client = &fake.RESTClient{
APIRegistry: api.Registry,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(&bytes.Buffer{}),
}, nil
}),
}
tf.ClientConfig = &restclient.Config{}
tf.Printer = &testPrinter{}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdCreateDeployment(f, buf)
cmd := NewCmdCreateDeployment(f, buf, buf)
cmd.Flags().Set("dry-run", "true")
cmd.Flags().Set("output", "name")
err := CreateDeployment(f, buf, cmd, []string{depName})
err := CreateDeployment(f, buf, buf, cmd, []string{depName})
assert.Error(t, err, "at least one image must be specified")
}

View File

@ -33,8 +33,9 @@ import (
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/api"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1"
"k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
conditions "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/kubectl"
@ -196,38 +197,50 @@ func Run(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobr
return err
}
clientset, err := f.ClientSet()
if err != nil {
return err
}
resourcesList, err := clientset.Discovery().ServerResources()
// ServerResources ignores errors for old servers do not expose discovery
if err != nil {
return fmt.Errorf("failed to discover supported resources: %v", err)
}
generatorName := cmdutil.GetFlagString(cmd, "generator")
schedule := cmdutil.GetFlagString(cmd, "schedule")
if len(schedule) != 0 && len(generatorName) == 0 {
generatorName = "cronjob/v2alpha1"
generatorName = cmdutil.CronJobV2Alpha1GeneratorName
}
if len(generatorName) == 0 {
clientset, err := f.ClientSet()
if err != nil {
return err
}
resourcesList, err := clientset.Discovery().ServerResources()
// ServerResources ignores errors for old servers do not expose discovery
if err != nil {
return fmt.Errorf("failed to discover supported resources: %v", err)
}
switch restartPolicy {
case api.RestartPolicyAlways:
if contains(resourcesList, v1beta1.SchemeGroupVersion.WithResource("deployments")) {
generatorName = "deployment/v1beta1"
// TODO: we need to deprecate this along with extensions/v1beta1.Deployments
// in favor of the new generator for apps/v1beta1.Deployments
if contains(resourcesList, extensionsv1beta1.SchemeGroupVersion.WithResource("deployments")) {
generatorName = cmdutil.DeploymentV1Beta1GeneratorName
} else {
generatorName = "run/v1"
generatorName = cmdutil.RunV1GeneratorName
}
case api.RestartPolicyOnFailure:
if contains(resourcesList, batchv1.SchemeGroupVersion.WithResource("jobs")) {
generatorName = "job/v1"
generatorName = cmdutil.JobV1GeneratorName
} else {
generatorName = "run-pod/v1"
generatorName = cmdutil.RunPodV1GeneratorName
}
case api.RestartPolicyNever:
generatorName = "run-pod/v1"
generatorName = cmdutil.RunPodV1GeneratorName
}
}
// TODO: this should be removed alongside with extensions/v1beta1 depployments generator
if generatorName == cmdutil.DeploymentAppsV1Beta1GeneratorName &&
!contains(resourcesList, appsv1beta1.SchemeGroupVersion.WithResource("deployments")) {
fmt.Fprintf(cmdErr, "WARNING: New deployments generator specified (%s), but apps/v1beta1.Deployments are not available, falling back to the old one (%s).\n",
cmdutil.DeploymentAppsV1Beta1GeneratorName, cmdutil.DeploymentV1Beta1GeneratorName)
generatorName = cmdutil.DeploymentV1Beta1GeneratorName
}
generators := f.Generators("run")
generator, found := generators[generatorName]
if !found {

View File

@ -35,6 +35,7 @@ import (
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
@ -113,7 +114,13 @@ func TestGetEnv(t *testing.T) {
}
func TestRunArgsFollowDashRules(t *testing.T) {
_, _, rc := testData()
one := int32(1)
rc := &v1.ReplicationController{
ObjectMeta: metav1.ObjectMeta{Name: "rc1", Namespace: "test", ResourceVersion: "18"},
Spec: v1.ReplicationControllerSpec{
Replicas: &one,
},
}
tests := []struct {
args []string
@ -158,7 +165,14 @@ func TestRunArgsFollowDashRules(t *testing.T) {
APIRegistry: api.Registry,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil
if req.URL.Path == "/namespaces/test/replicationcontrollers" {
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, rc)}, nil
} else {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(&bytes.Buffer{}),
}, nil
}
}),
}
tf.Namespace = "test"

View File

@ -456,31 +456,33 @@ func (f *ring0Factory) DefaultNamespace() (string, bool, error) {
}
const (
RunV1GeneratorName = "run/v1"
RunPodV1GeneratorName = "run-pod/v1"
ServiceV1GeneratorName = "service/v1"
ServiceV2GeneratorName = "service/v2"
ServiceNodePortGeneratorV1Name = "service-nodeport/v1"
ServiceClusterIPGeneratorV1Name = "service-clusterip/v1"
ServiceLoadBalancerGeneratorV1Name = "service-loadbalancer/v1"
ServiceExternalNameGeneratorV1Name = "service-externalname/v1"
ServiceAccountV1GeneratorName = "serviceaccount/v1"
HorizontalPodAutoscalerV1GeneratorName = "horizontalpodautoscaler/v1"
DeploymentV1Beta1GeneratorName = "deployment/v1beta1"
DeploymentBasicV1Beta1GeneratorName = "deployment-basic/v1beta1"
JobV1GeneratorName = "job/v1"
CronJobV2Alpha1GeneratorName = "cronjob/v2alpha1"
ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1"
NamespaceV1GeneratorName = "namespace/v1"
ResourceQuotaV1GeneratorName = "resourcequotas/v1"
SecretV1GeneratorName = "secret/v1"
SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1"
SecretForTLSV1GeneratorName = "secret-for-tls/v1"
ConfigMapV1GeneratorName = "configmap/v1"
ClusterRoleBindingV1GeneratorName = "clusterrolebinding.rbac.authorization.k8s.io/v1alpha1"
RoleBindingV1GeneratorName = "rolebinding.rbac.authorization.k8s.io/v1alpha1"
ClusterV1Beta1GeneratorName = "cluster/v1beta1"
PodDisruptionBudgetV1GeneratorName = "poddisruptionbudget/v1beta1"
RunV1GeneratorName = "run/v1"
RunPodV1GeneratorName = "run-pod/v1"
ServiceV1GeneratorName = "service/v1"
ServiceV2GeneratorName = "service/v2"
ServiceNodePortGeneratorV1Name = "service-nodeport/v1"
ServiceClusterIPGeneratorV1Name = "service-clusterip/v1"
ServiceLoadBalancerGeneratorV1Name = "service-loadbalancer/v1"
ServiceExternalNameGeneratorV1Name = "service-externalname/v1"
ServiceAccountV1GeneratorName = "serviceaccount/v1"
HorizontalPodAutoscalerV1GeneratorName = "horizontalpodautoscaler/v1"
DeploymentV1Beta1GeneratorName = "deployment/v1beta1"
DeploymentAppsV1Beta1GeneratorName = "deployment/apps.v1beta1"
DeploymentBasicV1Beta1GeneratorName = "deployment-basic/v1beta1"
DeploymentBasicAppsV1Beta1GeneratorName = "deployment-basic/apps.v1beta1"
JobV1GeneratorName = "job/v1"
CronJobV2Alpha1GeneratorName = "cronjob/v2alpha1"
ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1"
NamespaceV1GeneratorName = "namespace/v1"
ResourceQuotaV1GeneratorName = "resourcequotas/v1"
SecretV1GeneratorName = "secret/v1"
SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1"
SecretForTLSV1GeneratorName = "secret-for-tls/v1"
ConfigMapV1GeneratorName = "configmap/v1"
ClusterRoleBindingV1GeneratorName = "clusterrolebinding.rbac.authorization.k8s.io/v1alpha1"
RoleBindingV1GeneratorName = "rolebinding.rbac.authorization.k8s.io/v1alpha1"
ClusterV1Beta1GeneratorName = "cluster/v1beta1"
PodDisruptionBudgetV1GeneratorName = "poddisruptionbudget/v1beta1"
)
// DefaultGenerators returns the set of default generators for use in Factory instances
@ -506,16 +508,18 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator {
}
case "deployment":
generator = map[string]kubectl.Generator{
DeploymentBasicV1Beta1GeneratorName: kubectl.DeploymentBasicGeneratorV1{},
DeploymentBasicV1Beta1GeneratorName: kubectl.DeploymentBasicGeneratorV1{},
DeploymentBasicAppsV1Beta1GeneratorName: kubectl.DeploymentBasicAppsGeneratorV1{},
}
case "run":
generator = map[string]kubectl.Generator{
RunV1GeneratorName: kubectl.BasicReplicationController{},
RunPodV1GeneratorName: kubectl.BasicPod{},
DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{},
JobV1GeneratorName: kubectl.JobV1{},
ScheduledJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{},
CronJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{},
RunV1GeneratorName: kubectl.BasicReplicationController{},
RunPodV1GeneratorName: kubectl.BasicPod{},
DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{},
DeploymentAppsV1Beta1GeneratorName: kubectl.DeploymentAppsV1Beta1{},
JobV1GeneratorName: kubectl.JobV1{},
ScheduledJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{},
CronJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{},
}
case "autoscale":
generator = map[string]kubectl.Generator{

View File

@ -23,6 +23,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/api/v1"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
)
@ -113,3 +114,91 @@ func (s *DeploymentBasicGeneratorV1) validate() error {
}
return nil
}
// DeploymentBasicAppsGeneratorV1 supports stable generation of a deployment under apps/v1beta1 endpoint
type DeploymentBasicAppsGeneratorV1 struct {
Name string
Images []string
}
// Ensure it supports the generator pattern that uses parameters specified during construction
var _ StructuredGenerator = &DeploymentBasicAppsGeneratorV1{}
func (DeploymentBasicAppsGeneratorV1) ParamNames() []GeneratorParam {
return []GeneratorParam{
{"name", true},
{"image", true},
}
}
func (s DeploymentBasicAppsGeneratorV1) Generate(params map[string]interface{}) (runtime.Object, error) {
err := ValidateParams(s.ParamNames(), params)
if err != nil {
return nil, err
}
name, isString := params["name"].(string)
if !isString {
return nil, fmt.Errorf("expected string, saw %v for 'name'", name)
}
imageStrings, isArray := params["image"].([]string)
if !isArray {
return nil, fmt.Errorf("expected []string, found :%v", imageStrings)
}
delegate := &DeploymentBasicAppsGeneratorV1{Name: name, Images: imageStrings}
return delegate.StructuredGenerate()
}
// StructuredGenerate outputs a deployment object using the configured fields
func (s *DeploymentBasicAppsGeneratorV1) StructuredGenerate() (runtime.Object, error) {
if err := s.validate(); err != nil {
return nil, err
}
podSpec := v1.PodSpec{Containers: []v1.Container{}}
for _, imageString := range s.Images {
// Retain just the image name
imageSplit := strings.Split(imageString, "/")
name := imageSplit[len(imageSplit)-1]
// Remove any tag or hash
if strings.Contains(name, ":") {
name = strings.Split(name, ":")[0]
} else if strings.Contains(name, "@") {
name = strings.Split(name, "@")[0]
}
podSpec.Containers = append(podSpec.Containers, v1.Container{Name: name, Image: imageString})
}
// setup default label and selector
labels := map[string]string{}
labels["app"] = s.Name
one := int32(1)
selector := metav1.LabelSelector{MatchLabels: labels}
deployment := appsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: s.Name,
Labels: labels,
},
Spec: appsv1beta1.DeploymentSpec{
Replicas: &one,
Selector: &selector,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: podSpec,
},
},
}
return &deployment, nil
}
// validate validates required fields are set to support structured generation
func (s *DeploymentBasicAppsGeneratorV1) validate() error {
if len(s.Name) == 0 {
return fmt.Errorf("name must be specified")
}
if len(s.Images) == 0 {
return fmt.Errorf("at least one image must be specified")
}
return nil
}

View File

@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/api/v1"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
)
@ -134,3 +135,113 @@ func TestDeploymentGenerate(t *testing.T) {
}
}
}
func TestAppsDeploymentGenerate(t *testing.T) {
one := int32(1)
tests := []struct {
params map[string]interface{}
expected *appsv1beta1.Deployment
expectErr bool
}{
{
params: map[string]interface{}{
"name": "foo",
"image": []string{"abc/app:v4"},
},
expected: &appsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{"app": "foo"},
},
Spec: appsv1beta1.DeploymentSpec{
Replicas: &one,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "foo"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{{Name: "app", Image: "abc/app:v4"}},
},
},
},
},
expectErr: false,
},
{
params: map[string]interface{}{
"name": "foo",
"image": []string{"abc/app:v4", "zyx/ape"},
},
expected: &appsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{"app": "foo"},
},
Spec: appsv1beta1.DeploymentSpec{
Replicas: &one,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "foo"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{{Name: "app", Image: "abc/app:v4"},
{Name: "ape", Image: "zyx/ape"}},
},
},
},
},
expectErr: false,
},
{
params: map[string]interface{}{},
expectErr: true,
},
{
params: map[string]interface{}{
"name": 1,
},
expectErr: true,
},
{
params: map[string]interface{}{
"name": nil,
},
expectErr: true,
},
{
params: map[string]interface{}{
"name": "foo",
"image": []string{},
},
expectErr: true,
},
{
params: map[string]interface{}{
"NAME": "some_value",
},
expectErr: true,
},
}
generator := DeploymentBasicAppsGeneratorV1{}
for index, test := range tests {
t.Logf("running scenario %d", index)
obj, err := generator.Generate(test.params)
switch {
case test.expectErr && err != nil:
continue // loop, since there's no output to check
case test.expectErr && err == nil:
t.Errorf("expected error and didn't get one")
continue // loop, no expected output object
case !test.expectErr && err != nil:
t.Errorf("unexpected error %v", err)
continue // loop, no output object
case !test.expectErr && err == nil:
// do nothing and drop through
}
if !reflect.DeepEqual(obj.(*appsv1beta1.Deployment), test.expected) {
t.Errorf("expected:\n%#v\nsaw:\n%#v", test.expected, obj.(*appsv1beta1.Deployment))
}
}
}

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1"
batchv2alpha1 "k8s.io/kubernetes/pkg/apis/batch/v2alpha1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
@ -121,6 +122,94 @@ func (DeploymentV1Beta1) Generate(genericParams map[string]interface{}) (runtime
return &deployment, nil
}
type DeploymentAppsV1Beta1 struct{}
func (DeploymentAppsV1Beta1) ParamNames() []GeneratorParam {
return []GeneratorParam{
{"labels", false},
{"default-name", false},
{"name", true},
{"replicas", true},
{"image", true},
{"image-pull-policy", false},
{"port", false},
{"hostport", false},
{"stdin", false},
{"tty", false},
{"command", false},
{"args", false},
{"env", false},
{"requests", false},
{"limits", false},
}
}
func (DeploymentAppsV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object, error) {
args, err := getArgs(genericParams)
if err != nil {
return nil, err
}
envs, err := getEnvs(genericParams)
if err != nil {
return nil, err
}
params, err := getParams(genericParams)
if err != nil {
return nil, err
}
name, err := getName(params)
if err != nil {
return nil, err
}
labels, err := getLabels(params, true, name)
if err != nil {
return nil, err
}
count, err := strconv.Atoi(params["replicas"])
if err != nil {
return nil, err
}
podSpec, err := makePodSpec(params, name)
if err != nil {
return nil, err
}
imagePullPolicy := v1.PullPolicy(params["image-pull-policy"])
if err = updatePodContainers(params, args, envs, imagePullPolicy, podSpec); err != nil {
return nil, err
}
if err := updatePodPorts(params, podSpec); err != nil {
return nil, err
}
count32 := int32(count)
deployment := appsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
},
Spec: appsv1beta1.DeploymentSpec{
Replicas: &count32,
Selector: &metav1.LabelSelector{MatchLabels: labels},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: *podSpec,
},
},
}
return &deployment, nil
}
// getLabels returns map of labels.
func getLabels(params map[string]string, defaultRunLabel bool, name string) (map[string]string, error) {
labelString, found := params["labels"]
var labels map[string]string
@ -138,6 +227,7 @@ func getLabels(params map[string]string, defaultRunLabel bool, name string) (map
return labels, nil
}
// getName returns the name of newly created resource.
func getName(params map[string]string) (string, error) {
name, found := params["name"]
if !found || len(name) == 0 {
@ -149,6 +239,7 @@ func getName(params map[string]string) (string, error) {
return name, nil
}
// getParams returns map of generic parameters.
func getParams(genericParams map[string]interface{}) (map[string]string, error) {
params := map[string]string{}
for key, value := range genericParams {
@ -161,6 +252,7 @@ func getParams(genericParams map[string]interface{}) (map[string]string, error)
return params, nil
}
// getArgs returns arguments for the container command.
func getArgs(genericParams map[string]interface{}) ([]string, error) {
args := []string{}
val, found := genericParams["args"]
@ -175,6 +267,7 @@ func getArgs(genericParams map[string]interface{}) ([]string, error) {
return args, nil
}
// getEnvs returns environment variables.
func getEnvs(genericParams map[string]interface{}) ([]v1.EnvVar, error) {
var envs []v1.EnvVar
envStrings, found := genericParams["env"]
@ -409,6 +502,7 @@ func (BasicReplicationController) ParamNames() []GeneratorParam {
}
// populateResourceList takes strings of form <resourceName1>=<value1>,<resourceName1>=<value2>
// and returns ResourceList.
func populateResourceList(spec string) (api.ResourceList, error) {
// empty input gets a nil response to preserve generator test expected behaviors
if spec == "" {
@ -433,6 +527,7 @@ func populateResourceList(spec string) (api.ResourceList, error) {
}
// populateResourceListV1 takes strings of form <resourceName1>=<value1>,<resourceName1>=<value2>
// and returns ResourceList.
func populateResourceListV1(spec string) (v1.ResourceList, error) {
// empty input gets a nil response to preserve generator test expected behaviors
if spec == "" {
@ -457,6 +552,7 @@ func populateResourceListV1(spec string) (v1.ResourceList, error) {
}
// HandleResourceRequirements parses the limits and requests parameters if specified
// and returns ResourceRequirements.
func HandleResourceRequirements(params map[string]string) (api.ResourceRequirements, error) {
result := api.ResourceRequirements{}
limits, err := populateResourceList(params["limits"])
@ -473,6 +569,7 @@ func HandleResourceRequirements(params map[string]string) (api.ResourceRequireme
}
// HandleResourceRequirementsV1 parses the limits and requests parameters if specified
// and returns ResourceRequirements.
func HandleResourceRequirementsV1(params map[string]string) (v1.ResourceRequirements, error) {
result := v1.ResourceRequirements{}
limits, err := populateResourceListV1(params["limits"])
@ -488,6 +585,7 @@ func HandleResourceRequirementsV1(params map[string]string) (v1.ResourceRequirem
return result, nil
}
// makePodSpec returns PodSpec filled with passed parameters.
func makePodSpec(params map[string]string, name string) (*v1.PodSpec, error) {
stdin, err := GetBool(params, "stdin", false)
if err != nil {
@ -583,6 +681,7 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{})
return &controller, nil
}
// updatePodContainers updates PodSpec.Containers with passed parameters.
func updatePodContainers(params map[string]string, args []string, envs []v1.EnvVar, imagePullPolicy v1.PullPolicy, podSpec *v1.PodSpec) error {
if len(args) > 0 {
command, err := GetBool(params, "command", false)
@ -607,6 +706,7 @@ func updatePodContainers(params map[string]string, args []string, envs []v1.EnvV
return nil
}
// updatePodContainers updates PodSpec.Containers.Ports with passed parameters.
func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) {
port := -1
hostPort := -1
@ -747,6 +847,7 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object,
return &pod, nil
}
// parseEnvs converts string into EnvVar objects.
func parseEnvs(envArray []string) ([]v1.EnvVar, error) {
envs := make([]v1.EnvVar, 0, len(envArray))
for _, env := range envArray {

View File

@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/api/v1"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1"
batchv2alpha1 "k8s.io/kubernetes/pkg/apis/batch/v2alpha1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
@ -733,6 +734,99 @@ func TestGenerateDeployment(t *testing.T) {
}
}
func TestGenerateAppsDeployment(t *testing.T) {
three := int32(3)
tests := []struct {
params map[string]interface{}
expected *appsv1beta1.Deployment
expectErr bool
}{
{
params: map[string]interface{}{
"labels": "foo=bar,baz=blah",
"name": "foo",
"replicas": "3",
"image": "someimage",
"image-pull-policy": "Always",
"port": "80",
"hostport": "80",
"stdin": "true",
"command": "true",
"args": []string{"bar", "baz", "blah"},
"env": []string{"a=b", "c=d"},
"requests": "cpu=100m,memory=100Mi",
"limits": "cpu=400m,memory=200Mi",
},
expected: &appsv1beta1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{"foo": "bar", "baz": "blah"},
},
Spec: appsv1beta1.DeploymentSpec{
Replicas: &three,
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar", "baz": "blah"}},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"foo": "bar", "baz": "blah"},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "foo",
Image: "someimage",
ImagePullPolicy: v1.PullAlways,
Stdin: true,
Ports: []v1.ContainerPort{
{
ContainerPort: 80,
HostPort: 80,
},
},
Command: []string{"bar", "baz", "blah"},
Env: []v1.EnvVar{
{
Name: "a",
Value: "b",
},
{
Name: "c",
Value: "d",
},
},
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("100m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
},
Limits: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("400m"),
v1.ResourceMemory: resource.MustParse("200Mi"),
},
},
},
},
},
},
},
},
},
}
generator := DeploymentAppsV1Beta1{}
for _, test := range tests {
obj, err := generator.Generate(test.params)
if !test.expectErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
if test.expectErr && err != nil {
continue
}
if !reflect.DeepEqual(obj.(*appsv1beta1.Deployment), test.expected) {
t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*appsv1beta1.Deployment))
}
}
}
func TestGenerateJob(t *testing.T) {
tests := []struct {
params map[string]interface{}