diff --git a/pkg/kn/commands/configuration_edit_flags.go b/pkg/kn/commands/configuration_edit_flags.go new file mode 100644 index 000000000..713e50bdf --- /dev/null +++ b/pkg/kn/commands/configuration_edit_flags.go @@ -0,0 +1,58 @@ +// Copyright © 2018 The Knative Authors +// +// 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 commands + +import ( + "fmt" + "strings" + + servinglib "github.com/knative/client/pkg/serving" + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/spf13/cobra" +) + +type ConfigurationEditFlags struct { + Image string + Env []string +} + +func (p *ConfigurationEditFlags) AddFlags(command *cobra.Command) { + command.Flags().StringVar(&p.Image, "image", "", "Image to run.") + command.Flags().StringArrayVarP(&p.Env, "env", "e", []string{}, + "Environment variable to set. NAME=value; you may provide this flag "+ + "any number of times to set multiple environment variables.") +} + +func (p *ConfigurationEditFlags) Apply(config *servingv1alpha1.ConfigurationSpec) error { + envMap := map[string]string{} + for _, pairStr := range p.Env { + pairSlice := strings.SplitN(pairStr, "=", 2) + if len(pairSlice) <= 1 { + return fmt.Errorf( + "--env argument requires a value that contains the '=' character; got %s", + pairStr) + } + envMap[pairSlice[0]] = pairSlice[1] + } + err := servinglib.UpdateEnvVars(config, envMap) + if err != nil { + return err + } + err = servinglib.UpdateImage(config, p.Image) + if err != nil { + return err + } + return nil +} diff --git a/pkg/kn/commands/service.go b/pkg/kn/commands/service.go index 1eb3417d6..d078e2776 100644 --- a/pkg/kn/commands/service.go +++ b/pkg/kn/commands/service.go @@ -26,5 +26,6 @@ func NewServiceCommand(p *KnParams) *cobra.Command { serviceCmd.PersistentFlags().StringP("namespace", "n", "default", "Namespace to use.") serviceCmd.AddCommand(NewServiceListCommand(p)) serviceCmd.AddCommand(NewServiceDescribeCommand(p)) + serviceCmd.AddCommand(NewServiceCreateCommand(p)) return serviceCmd } diff --git a/pkg/kn/commands/service_create.go b/pkg/kn/commands/service_create.go new file mode 100644 index 000000000..e38d97bf7 --- /dev/null +++ b/pkg/kn/commands/service_create.go @@ -0,0 +1,71 @@ +// Copyright © 2019 The Knative Authors +// +// 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 commands + +import ( + "errors" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" + + serving_lib "github.com/knative/client/pkg/serving" + "github.com/spf13/cobra" +) + +func NewServiceCreateCommand(p *KnParams) *cobra.Command { + var editFlags ConfigurationEditFlags + + serviceCreateCommand := &cobra.Command{ + Use: "create NAME", + Short: "Create a service.", + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 { + return errors.New("requires the service name.") + } + + namespace := cmd.Flag("namespace").Value.String() + + service := servingv1alpha1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: args[0], + Namespace: namespace, + }, + } + service.Spec.RunLatest = &servingv1alpha1.RunLatestType{} + + config, err := serving_lib.GetConfiguration(&service) + if err != nil { + return err + } + err = editFlags.Apply(config) + if err != nil { + return err + } + client, err := p.ServingFactory() + if err != nil { + return err + } + _, err = client.Services(namespace).Create(&service) + if err != nil { + return err + } + + return nil + }, + } + editFlags.AddFlags(serviceCreateCommand) + return serviceCreateCommand +} diff --git a/pkg/kn/commands/service_create_test.go b/pkg/kn/commands/service_create_test.go new file mode 100644 index 000000000..67817205f --- /dev/null +++ b/pkg/kn/commands/service_create_test.go @@ -0,0 +1,114 @@ +// Copyright © 2019 The Knative Authors +// +// 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 commands + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "testing" + + servinglib "github.com/knative/client/pkg/serving" + + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + serving "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1" + "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake" + "k8s.io/apimachinery/pkg/runtime" + client_testing "k8s.io/client-go/testing" +) + +func fakeServiceCreate(args []string) ( + action client_testing.Action, + created *v1alpha1.Service, + output string, + err error) { + + buf := new(bytes.Buffer) + fakeServing := &fake.FakeServingV1alpha1{&client_testing.Fake{}} + cmd := NewKnCommand(KnParams{ + Output: buf, + ServingFactory: func() (serving.ServingV1alpha1Interface, error) { return fakeServing, nil }, + }) + fakeServing.AddReactor("*", "*", + func(a client_testing.Action) (bool, runtime.Object, error) { + createAction, ok := a.(client_testing.CreateAction) + action = createAction + if !ok { + return true, nil, fmt.Errorf("wrong kind of action %v", action) + } + created, ok = createAction.GetObject().(*v1alpha1.Service) + if !ok { + return true, nil, errors.New("was passed the wrong object") + } + return true, created, nil + }) + cmd.SetArgs(args) + err = cmd.Execute() + if err != nil { + return + } + output = buf.String() + return +} + +func TestServiceCreateImage(t *testing.T) { + action, created, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz"}) + + if err != nil { + t.Fatal(err) + } else if !action.Matches("create", "services") { + t.Fatalf("Bad action %v", action) + } + conf, err := servinglib.GetConfiguration(created) + if err != nil { + t.Fatal(err) + } else if conf.RevisionTemplate.Spec.Container.Image != "gcr.io/foo/bar:baz" { + t.Fatalf("wrong image set: %v", conf.RevisionTemplate.Spec.Container.Image) + } +} + +func TestServiceCreateEnv(t *testing.T) { + action, created, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "-e", "A=DOGS", "--env", "B=WOLVES"}) + + if err != nil { + t.Fatal(err) + } else if !action.Matches("create", "services") { + t.Fatalf("Bad action %v", action) + } + + expectedEnvVars := map[string]string{ + "A": "DOGS", + "B": "WOLVES"} + + conf, err := servinglib.GetConfiguration(created) + actualEnvVars, err := servinglib.EnvToMap(conf.RevisionTemplate.Spec.Container.Env) + if err != nil { + t.Fatal(err) + } + + if err != nil { + t.Fatal(err) + } else if conf.RevisionTemplate.Spec.Container.Image != "gcr.io/foo/bar:baz" { + t.Fatalf("wrong image set: %v", conf.RevisionTemplate.Spec.Container.Image) + } else if !reflect.DeepEqual( + actualEnvVars, + expectedEnvVars) { + t.Fatalf("wrong env vars %v", conf.RevisionTemplate.Spec.Container.Env) + } + +} diff --git a/pkg/serving/config_changes.go b/pkg/serving/config_changes.go new file mode 100644 index 000000000..ff10d1891 --- /dev/null +++ b/pkg/serving/config_changes.go @@ -0,0 +1,69 @@ +// Copyright © 2019 The Knative Authors +// +// 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 serving + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" +) + +// Give the configuration all the env var values listed in the given map of +// vars. Does not touch any environment variables not mentioned, but it can add +// new env vars and change the values of existing ones. +func UpdateEnvVars(config *servingv1alpha1.ConfigurationSpec, vars map[string]string) error { + set := make(map[string]bool) + for i, _ := range config.RevisionTemplate.Spec.Container.Env { + envVar := &config.RevisionTemplate.Spec.Container.Env[i] + value, present := vars[envVar.Name] + if present { + envVar.Value = value + set[envVar.Name] = true + } + } + for name, value := range vars { + if !set[name] { + config.RevisionTemplate.Spec.Container.Env = append( + config.RevisionTemplate.Spec.Container.Env, + corev1.EnvVar{ + Name: name, + Value: value, + }) + } + } + return nil + +} + +// Utility function to translate between the API list form of env vars, and the +// more convenient map form. +func EnvToMap(vars []corev1.EnvVar) (map[string]string, error) { + result := map[string]string{} + for _, envVar := range vars { + _, present := result[envVar.Name] + if present { + return nil, fmt.Errorf("Env var name present more than once: %v", envVar.Name) + } + result[envVar.Name] = envVar.Value + } + return result, nil +} + +func UpdateImage(config *servingv1alpha1.ConfigurationSpec, image string) error { + config.RevisionTemplate.Spec.Container.Image = image + return nil +} diff --git a/pkg/serving/config_changes_test.go b/pkg/serving/config_changes_test.go new file mode 100644 index 000000000..ba416acaf --- /dev/null +++ b/pkg/serving/config_changes_test.go @@ -0,0 +1,122 @@ +// Copyright ¬© 2019 The Knative Authors +// +// 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 serving + +import ( + "reflect" + "testing" + + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +func TestUpdateEnvVarsNew(t *testing.T) { + config := servingv1alpha1.ConfigurationSpec{} + env := map[string]string{ + "a": "foo", + "b": "bar", + } + err := UpdateEnvVars(&config, env) + if err != nil { + t.Fatal(err) + } + found, err := EnvToMap(config.RevisionTemplate.Spec.Container.Env) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(env, found) { + t.Fatalf("Env did not match expected %v found %v", env, found) + } +} + +func TestUpdateEnvVarsAppend(t *testing.T) { + config := servingv1alpha1.ConfigurationSpec{} + config.RevisionTemplate.Spec.Container.Env = []corev1.EnvVar{ + corev1.EnvVar{Name: "a", Value: "foo"}} + env := map[string]string{ + "b": "bar", + } + err := UpdateEnvVars(&config, env) + if err != nil { + t.Fatal(err) + } + + expected := map[string]string{ + "a": "foo", + "b": "bar", + } + + found, err := EnvToMap(config.RevisionTemplate.Spec.Container.Env) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expected, found) { + t.Fatalf("Env did not match expected %v found %v", env, found) + } +} + +func TestUpdateEnvVarsModify(t *testing.T) { + config := servingv1alpha1.ConfigurationSpec{} + config.RevisionTemplate.Spec.Container.Env = []corev1.EnvVar{ + corev1.EnvVar{Name: "a", Value: "foo"}} + env := map[string]string{ + "a": "fancy", + } + err := UpdateEnvVars(&config, env) + if err != nil { + t.Fatal(err) + } + + expected := map[string]string{ + "a": "fancy", + } + + found, err := EnvToMap(config.RevisionTemplate.Spec.Container.Env) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expected, found) { + t.Fatalf("Env did not match expected %v found %v", env, found) + } +} + +func TestUpdateEnvVarsBoth(t *testing.T) { + config := servingv1alpha1.ConfigurationSpec{} + config.RevisionTemplate.Spec.Container.Env = []corev1.EnvVar{ + corev1.EnvVar{Name: "a", Value: "foo"}, + corev1.EnvVar{Name: "c", Value: "caroline"}} + env := map[string]string{ + "a": "fancy", + "b": "boo", + } + err := UpdateEnvVars(&config, env) + if err != nil { + t.Fatal(err) + } + + expected := map[string]string{ + "a": "fancy", + "b": "boo", + "c": "caroline", + } + + found, err := EnvToMap(config.RevisionTemplate.Spec.Container.Env) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expected, found) { + t.Fatalf("Env did not match expected %v found %v", env, found) + } +} diff --git a/pkg/serving/service.go b/pkg/serving/service.go new file mode 100644 index 000000000..98872fd6b --- /dev/null +++ b/pkg/serving/service.go @@ -0,0 +1,33 @@ +// Copyright © 2019 The Knative Authors +// +// 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 serving + +import ( + "errors" + + servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" +) + +func GetConfiguration(service *servingv1alpha1.Service) (*servingv1alpha1.ConfigurationSpec, error) { + if service.Spec.RunLatest != nil { + return &service.Spec.RunLatest.Configuration, nil + } else if service.Spec.Release != nil { + return &service.Spec.Release.Configuration, nil + } else if service.Spec.Pinned != nil { + return &service.Spec.Pinned.Configuration, nil + } else { + return nil, errors.New("Service does not specify a Configuration") + } +}