mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-29 21:29:24 +00:00
Merge pull request #15921 from brendandburns/fix2
Add a --expose flag to kubectl run
This commit is contained in:
@@ -24,12 +24,14 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/meta"
|
||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/resource"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
"k8s.io/kubernetes/pkg/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -77,6 +79,11 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
|
||||
},
|
||||
}
|
||||
cmdutil.AddPrinterFlags(cmd)
|
||||
addRunFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addRunFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'.")
|
||||
cmd.Flags().String("image", "", "The image for the container to run.")
|
||||
cmd.MarkFlagRequired("image")
|
||||
@@ -84,7 +91,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
|
||||
cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.")
|
||||
cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")
|
||||
cmd.Flags().StringSlice("env", []string{}, "Environment variables to set in the container")
|
||||
cmd.Flags().Int("port", -1, "The port that this container exposes.")
|
||||
cmd.Flags().Int("port", -1, "The port that this container exposes. If --expose is true, this is also the port used by the service that is created.")
|
||||
cmd.Flags().Int("hostport", -1, "The host port mapping for the container port. To demonstrate a single-machine container.")
|
||||
cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s).")
|
||||
cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.")
|
||||
@@ -95,7 +102,9 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c
|
||||
cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.")
|
||||
cmd.Flags().String("requests", "", "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'")
|
||||
cmd.Flags().String("limits", "", "The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi'")
|
||||
return cmd
|
||||
cmd.Flags().Bool("expose", false, "If true, a public, external service is created for the container(s) which are run")
|
||||
cmd.Flags().String("service-generator", "service/v2", "The name of the generator to use for creating a service. Only used if --expose is true")
|
||||
cmd.Flags().String("service-overrides", "", "An inline JSON override for the generated service object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if --expose is true.")
|
||||
}
|
||||
|
||||
func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string) error {
|
||||
@@ -129,6 +138,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
|
||||
if restartPolicy != api.RestartPolicyAlways && replicas != 1 {
|
||||
return cmdutil.UsageError(cmd, fmt.Sprintf("--restart=%s requires that --replicas=1, found %d", restartPolicy, replicas))
|
||||
}
|
||||
|
||||
generatorName := cmdutil.GetFlagString(cmd, "generator")
|
||||
if len(generatorName) == 0 {
|
||||
if restartPolicy == api.RestartPolicyAlways {
|
||||
@@ -150,64 +160,20 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
|
||||
|
||||
params["env"] = cmdutil.GetFlagStringSlice(cmd, "env")
|
||||
|
||||
err = kubectl.ValidateParams(names, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err := generator.Generate(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mapper, typer := f.Object()
|
||||
version, kind, err := typer.ObjectVersionAndKind(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inline := cmdutil.GetFlagString(cmd, "overrides")
|
||||
if len(inline) > 0 {
|
||||
obj, err = cmdutil.Merge(obj, inline, kind)
|
||||
if err != nil {
|
||||
if cmdutil.GetFlagBool(cmd, "expose") {
|
||||
serviceGenerator := cmdutil.GetFlagString(cmd, "service-generator")
|
||||
if len(serviceGenerator) == 0 {
|
||||
return cmdutil.UsageError(cmd, fmt.Sprintf("No service generator specified"))
|
||||
}
|
||||
if err := generateService(f, cmd, args, serviceGenerator, params, namespace, cmdOut); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
mapping, err := mapper.RESTMapping(kind, version)
|
||||
obj, kind, mapper, mapping, err := createGeneratedObject(f, cmd, generator, names, params, cmdutil.GetFlagString(cmd, "overrides"), namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := f.RESTClient(mapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: extract this flag to a central location, when such a location exists.
|
||||
if !cmdutil.GetFlagBool(cmd, "dry-run") {
|
||||
resourceMapper := &resource.Mapper{ObjectTyper: typer, RESTMapper: mapper, ClientMapper: f.ClientMapperForCommand()}
|
||||
info, err := resourceMapper.InfoForObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Serialize the configuration into an annotation.
|
||||
if err := kubectl.UpdateApplyAnnotation(info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Serialize the object with the annotation applied.
|
||||
data, err := mapping.Codec.Encode(info.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = resource.NewHelper(client, mapping).Create(namespace, false, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
attachFlag := cmd.Flags().Lookup("attach")
|
||||
attach := cmdutil.GetFlagBool(cmd, "attach")
|
||||
|
||||
@@ -337,3 +303,110 @@ func getRestartPolicy(cmd *cobra.Command, interactive bool) (api.RestartPolicy,
|
||||
return "", cmdutil.UsageError(cmd, fmt.Sprintf("invalid restart policy: %s", restart))
|
||||
}
|
||||
}
|
||||
|
||||
func generateService(f *cmdutil.Factory, cmd *cobra.Command, args []string, serviceGenerator string, paramsIn map[string]interface{}, namespace string, out io.Writer) error {
|
||||
generator, found := f.Generator(serviceGenerator)
|
||||
if !found {
|
||||
return fmt.Errorf("missing service generator: %s", serviceGenerator)
|
||||
}
|
||||
names := generator.ParamNames()
|
||||
|
||||
port := cmdutil.GetFlagInt(cmd, "port")
|
||||
if port < 1 {
|
||||
return fmt.Errorf("--port must be a positive integer when exposing a service")
|
||||
}
|
||||
|
||||
params := map[string]interface{}{}
|
||||
for key, value := range paramsIn {
|
||||
_, isString := value.(string)
|
||||
if isString {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
name, found := params["name"]
|
||||
if !found || len(name.(string)) == 0 {
|
||||
return fmt.Errorf("name is a required parameter")
|
||||
}
|
||||
selector, found := params["labels"]
|
||||
if !found || len(selector.(string)) == 0 {
|
||||
selector = fmt.Sprintf("run=%s", name.(string))
|
||||
}
|
||||
params["selector"] = selector
|
||||
|
||||
if defaultName, found := params["default-name"]; !found || len(defaultName.(string)) == 0 {
|
||||
params["default-name"] = name
|
||||
}
|
||||
|
||||
obj, _, mapper, mapping, err := createGeneratedObject(f, cmd, generator, names, params, cmdutil.GetFlagString(cmd, "service-overrides"), namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmdutil.GetFlagString(cmd, "output") != "" {
|
||||
return f.PrintObject(cmd, obj, out)
|
||||
}
|
||||
cmdutil.PrintSuccess(mapper, false, out, mapping.Resource, args[0], "created")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createGeneratedObject(f *cmdutil.Factory, cmd *cobra.Command, generator kubectl.Generator, names []kubectl.GeneratorParam, params map[string]interface{}, overrides, namespace string) (runtime.Object, string, meta.RESTMapper, *meta.RESTMapping, error) {
|
||||
err := kubectl.ValidateParams(names, params)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
obj, err := generator.Generate(params)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
mapper, typer := f.Object()
|
||||
version, kind, err := typer.ObjectVersionAndKind(obj)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
if len(overrides) > 0 {
|
||||
obj, err = cmdutil.Merge(obj, overrides, kind)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mapping, err := mapper.RESTMapping(kind, version)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
client, err := f.RESTClient(mapping)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: extract this flag to a central location, when such a location exists.
|
||||
if !cmdutil.GetFlagBool(cmd, "dry-run") {
|
||||
resourceMapper := &resource.Mapper{ObjectTyper: typer, RESTMapper: mapper, ClientMapper: f.ClientMapperForCommand()}
|
||||
info, err := resourceMapper.InfoForObject(obj)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
// Serialize the configuration into an annotation.
|
||||
if err := kubectl.UpdateApplyAnnotation(info); err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
// Serialize the object with the annotation applied.
|
||||
data, err := mapping.Codec.Encode(info.Object)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
|
||||
obj, err = resource.NewHelper(client, mapping).Create(namespace, false, data)
|
||||
if err != nil {
|
||||
return nil, "", nil, nil, err
|
||||
}
|
||||
}
|
||||
return obj, kind, mapper, mapping, err
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@ limitations under the License.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/client/unversioned/fake"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func TestGetRestartPolicy(t *testing.T) {
|
||||
@@ -97,3 +105,160 @@ func TestGetEnv(t *testing.T) {
|
||||
t.Errorf("expected: %s, saw: %s", test.expected, envStrings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateService(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
port string
|
||||
args []string
|
||||
serviceGenerator string
|
||||
params map[string]interface{}
|
||||
expectErr bool
|
||||
name string
|
||||
service api.Service
|
||||
expectPOST bool
|
||||
}{
|
||||
{
|
||||
port: "80",
|
||||
args: []string{"foo"},
|
||||
serviceGenerator: "service/v2",
|
||||
params: map[string]interface{}{
|
||||
"name": "foo",
|
||||
},
|
||||
expectErr: false,
|
||||
name: "basic",
|
||||
service: api.Service{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: api.ServiceSpec{
|
||||
Ports: []api.ServicePort{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: "TCP",
|
||||
TargetPort: util.NewIntOrStringFromInt(80),
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"run": "foo",
|
||||
},
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
SessionAffinity: api.ServiceAffinityNone,
|
||||
},
|
||||
},
|
||||
expectPOST: true,
|
||||
},
|
||||
{
|
||||
port: "80",
|
||||
args: []string{"foo"},
|
||||
serviceGenerator: "service/v2",
|
||||
params: map[string]interface{}{
|
||||
"name": "foo",
|
||||
"labels": "app=bar",
|
||||
},
|
||||
expectErr: false,
|
||||
name: "custom labels",
|
||||
service: api.Service{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{"app": "bar"},
|
||||
},
|
||||
Spec: api.ServiceSpec{
|
||||
Ports: []api.ServicePort{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: "TCP",
|
||||
TargetPort: util.NewIntOrStringFromInt(80),
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"app": "bar",
|
||||
},
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
SessionAffinity: api.ServiceAffinityNone,
|
||||
},
|
||||
},
|
||||
expectPOST: true,
|
||||
},
|
||||
{
|
||||
expectErr: true,
|
||||
name: "missing port",
|
||||
expectPOST: false,
|
||||
},
|
||||
{
|
||||
port: "80",
|
||||
args: []string{"foo"},
|
||||
serviceGenerator: "service/v2",
|
||||
params: map[string]interface{}{
|
||||
"name": "foo",
|
||||
},
|
||||
expectErr: false,
|
||||
name: "dry-run",
|
||||
expectPOST: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
sawPOST := false
|
||||
f, tf, codec := NewAPIFactory()
|
||||
tf.ClientConfig = &client.Config{Version: testapi.Default.Version()}
|
||||
tf.Client = &fake.RESTClient{
|
||||
Codec: codec,
|
||||
Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case test.expectPOST && m == "POST" && p == "/namespaces/namespace/services":
|
||||
sawPOST = true
|
||||
body := objBody(codec, &test.service)
|
||||
data, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
defer req.Body.Close()
|
||||
svc := &api.Service{}
|
||||
if err := codec.DecodeInto(data, svc); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
// Copy things that are defaulted by the system
|
||||
test.service.Annotations = svc.Annotations
|
||||
|
||||
if !reflect.DeepEqual(&test.service, svc) {
|
||||
t.Errorf("expected:\n%v\nsaw:\n%v\n", &test.service, svc)
|
||||
}
|
||||
return &http.Response{StatusCode: 200, Body: body}, nil
|
||||
default:
|
||||
// Ensures no GET is performed when deleting by name
|
||||
t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req)
|
||||
return nil, fmt.Errorf("unexpected request")
|
||||
}
|
||||
}),
|
||||
}
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("output", "", "")
|
||||
addRunFlags(cmd)
|
||||
|
||||
if !test.expectPOST {
|
||||
cmd.Flags().Set("dry-run", "true")
|
||||
}
|
||||
|
||||
if len(test.port) > 0 {
|
||||
cmd.Flags().Set("port", test.port)
|
||||
test.params["port"] = test.port
|
||||
}
|
||||
|
||||
buff := &bytes.Buffer{}
|
||||
err := generateService(f, cmd, test.args, test.serviceGenerator, test.params, "namespace", buff)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Error("unexpected non-error")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if test.expectPOST != sawPOST {
|
||||
t.Error("expectPost: %v, sawPost: %v", test.expectPOST, sawPOST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user