diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 3df6cc4c10..366f7218e3 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -42,6 +42,11 @@ | Add --revision-name flag | https://github.com/knative/client/pull/282[#282] +| 🎁 +| Support traffic splitting and tagging targets +| https://github.com/knative/client/pull/345[#345] + + |=== ## v0.2.0 (2019-07-10) diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index b730ead7fe..48c590843b 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -14,14 +14,25 @@ kn service update NAME [flags] ``` - # Updates a service 'mysvc' with new environment variables - kn service update mysvc --env KEY1=VALUE1 --env KEY2=VALUE2 + # Updates a service 'svc' with new environment variables + kn service update svc --env KEY1=VALUE1 --env KEY2=VALUE2 - # Update a service 'mysvc' with new port - kn service update mysvc --port 80 + # Update a service 'svc' with new port + kn service update svc --port 80 - # Updates a service 'mysvc' with new requests and limits parameters - kn service update mysvc --requests-cpu 500m --limits-memory 1024Mi + # Updates a service 'svc' with new requests and limits parameters + kn service update svc --requests-cpu 500m --limits-memory 1024Mi + + # Assign tag 'latest' and 'stable' to revisions 'echo-v2' and 'echo-v1' respectively + kn service update svc --tag echo-v2=latest --tag echo-v1=stable + OR + kn service update svc --tag echo-v2=latest,echo-v1=stable + + # Update tag from 'testing' to 'staging' for latest ready revision of service + kn service update svc --untag testing --tag @latest=staging + + # Add tag 'test' to echo-v3 revision with 10% traffic and rest to latest ready revision of service + kn service update svc --tag echo-v3=test --traffic test=10,@latest=90 ``` ### Options @@ -43,6 +54,9 @@ kn service update NAME [flags] --requests-cpu string The requested CPU (e.g., 250m). --requests-memory string The requested memory (e.g., 64Mi). --revision-name string The revision name to set. Must start with the service name and a dash as a prefix. Empty revision name will result in the server generating a name for the revision. Accepts golang templates, allowing {{.Service}} for the service name, {{.Generation}} for the generation, and {{.Random [n]}} for n random consonants. (default "{{.Service}}-{{.Random 5}}-{{.Generation}}") + --tag strings Set tag (format: --tag revisionRef=tagName) where revisionRef can be a revision or '@latest' string representing latest ready revision. This flag can be specified multiple times. + --traffic strings Set traffic distribution (format: --traffic revisionRef=percent) where revisionRef can be a revision or a tag or '@latest' string representing latest ready revision. This flag can be given multiple times with percent summing up to 100%. + --untag strings Untag revision (format: --untag tagName). This flag can be spcified multiple times. --wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 60) ``` diff --git a/pkg/kn/commands/flags/traffic.go b/pkg/kn/commands/flags/traffic.go new file mode 100644 index 0000000000..89b0a7ad9c --- /dev/null +++ b/pkg/kn/commands/flags/traffic.go @@ -0,0 +1,56 @@ +// 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 flags + +import ( + "github.com/spf13/cobra" +) + +type Traffic struct { + RevisionsPercentages []string + RevisionsTags []string + UntagRevisions []string +} + +func (t *Traffic) Add(cmd *cobra.Command) { + cmd.Flags().StringSliceVar(&t.RevisionsPercentages, + "traffic", + nil, + "Set traffic distribution (format: --traffic revisionRef=percent) where revisionRef can be a revision or a tag or '@latest' string "+ + "representing latest ready revision. This flag can be given multiple times with percent summing up to 100%.") + + cmd.Flags().StringSliceVar(&t.RevisionsTags, + "tag", + nil, + "Set tag (format: --tag revisionRef=tagName) where revisionRef can be a revision or '@latest' string representing latest ready revision. "+ + "This flag can be specified multiple times.") + + cmd.Flags().StringSliceVar(&t.UntagRevisions, + "untag", + nil, + "Untag revision (format: --untag tagName). This flag can be spcified multiple times.") +} + +func (t *Traffic) PercentagesChanged(cmd *cobra.Command) bool { + return cmd.Flags().Changed("traffic") +} + +func (t *Traffic) TagsChanged(cmd *cobra.Command) bool { + return cmd.Flags().Changed("tag") || cmd.Flags().Changed("untag") +} + +func (t *Traffic) Changed(cmd *cobra.Command) bool { + return t.PercentagesChanged(cmd) || t.TagsChanged(cmd) +} diff --git a/pkg/kn/commands/service/service_update.go b/pkg/kn/commands/service/service_update.go index c0aac2bda8..f87cb073a6 100644 --- a/pkg/kn/commands/service/service_update.go +++ b/pkg/kn/commands/service/service_update.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" + "github.com/knative/client/pkg/kn/commands/flags" + "github.com/knative/client/pkg/kn/traffic" "github.com/spf13/cobra" api_errors "k8s.io/apimachinery/pkg/api/errors" @@ -27,19 +29,30 @@ import ( func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command { var editFlags ConfigurationEditFlags var waitFlags commands.WaitFlags - + var trafficFlags flags.Traffic serviceUpdateCommand := &cobra.Command{ Use: "update NAME [flags]", Short: "Update a service.", Example: ` - # Updates a service 'mysvc' with new environment variables - kn service update mysvc --env KEY1=VALUE1 --env KEY2=VALUE2 + # Updates a service 'svc' with new environment variables + kn service update svc --env KEY1=VALUE1 --env KEY2=VALUE2 + + # Update a service 'svc' with new port + kn service update svc --port 80 + + # Updates a service 'svc' with new requests and limits parameters + kn service update svc --requests-cpu 500m --limits-memory 1024Mi - # Update a service 'mysvc' with new port - kn service update mysvc --port 80 + # Assign tag 'latest' and 'stable' to revisions 'echo-v2' and 'echo-v1' respectively + kn service update svc --tag echo-v2=latest --tag echo-v1=stable + OR + kn service update svc --tag echo-v2=latest,echo-v1=stable - # Updates a service 'mysvc' with new requests and limits parameters - kn service update mysvc --requests-cpu 500m --limits-memory 1024Mi`, + # Update tag from 'testing' to 'staging' for latest ready revision of service + kn service update svc --untag testing --tag @latest=staging + + # Add tag 'test' to echo-v3 revision with 10% traffic and rest to latest ready revision of service + kn service update svc --tag echo-v3=test --traffic test=10,@latest=90`, RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { return errors.New("requires the service name.") @@ -69,6 +82,15 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command { return err } + if trafficFlags.Changed(cmd) { + traffic, err := traffic.Compute(cmd, service.Spec.Traffic, &trafficFlags) + if err != nil { + return err + } + + service.Spec.Traffic = traffic + } + err = client.UpdateService(service) if err != nil { // Retry to update when a resource version conflict exists @@ -99,6 +121,7 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command { commands.AddNamespaceFlags(serviceUpdateCommand.Flags(), false) editFlags.AddUpdateFlags(serviceUpdateCommand) waitFlags.AddConditionWaitFlags(serviceUpdateCommand, 60, "Update", "service") + trafficFlags.Add(serviceUpdateCommand) return serviceUpdateCommand } diff --git a/pkg/kn/traffic/compute.go b/pkg/kn/traffic/compute.go new file mode 100644 index 0000000000..6c415b56e0 --- /dev/null +++ b/pkg/kn/traffic/compute.go @@ -0,0 +1,369 @@ +// 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 traffic + +import ( + "fmt" + "strconv" + "strings" + + "github.com/knative/client/pkg/kn/commands/flags" + "github.com/knative/pkg/ptr" + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/spf13/cobra" +) + +var latestRevisionRef = "@latest" + +// ServiceTraffic type for operating on service traffic targets +type ServiceTraffic []v1alpha1.TrafficTarget + +func newServiceTraffic(traffic []v1alpha1.TrafficTarget) ServiceTraffic { + return ServiceTraffic(traffic) +} + +func splitByEqualSign(pair string) (string, string, error) { + parts := strings.Split(pair, "=") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("expecting the value format in value1=value2, given %s", pair) + } + return parts[0], strings.TrimSuffix(parts[1], "%"), nil +} + +func newTarget(tag, revision string, percent int, latestRevision bool) (target v1alpha1.TrafficTarget) { + target.Percent = percent + target.Tag = tag + if latestRevision { + target.LatestRevision = ptr.Bool(true) + } else { + // as LatestRevision and RevisionName can't be specfied together for a target + target.LatestRevision = ptr.Bool(false) + target.RevisionName = revision + } + return +} + +func (e ServiceTraffic) isTagPresentOnRevision(tag, revision string) bool { + for _, target := range e { + if target.Tag == tag && target.RevisionName == revision { + return true + } + } + return false +} + +func (e ServiceTraffic) tagOfLatestReadyRevision() string { + for _, target := range e { + if *target.LatestRevision { + return target.Tag + } + } + return "" +} + +func (e ServiceTraffic) isTagPresent(tag string) bool { + for _, target := range e { + if target.Tag == tag { + return true + } + } + return false +} + +func (e ServiceTraffic) untagRevision(tag string) { + for i, target := range e { + if target.Tag == tag { + e[i].Tag = "" + break + } + } +} + +func (e ServiceTraffic) isRevisionPresent(revision string) bool { + for _, target := range e { + if target.RevisionName == revision { + return true + } + } + return false +} + +func (e ServiceTraffic) isLatestRevisionTrue() bool { + for _, target := range e { + if *target.LatestRevision == true { + return true + } + } + return false +} + +// TagRevision assigns given tag to a revision +func (e ServiceTraffic) TagRevision(tag, revision string) ServiceTraffic { + for i, target := range e { + if target.RevisionName == revision { + if target.Tag != "" { // referenced revision is requested to have multiple tags + break + } else { + e[i].Tag = tag // referenced revision doesn't have tag, tag it + return e + } + } + } + // append a new target if revision doesn't exist in traffic block + // or if referenced revision is requested to have multiple tags + e = append(e, newTarget(tag, revision, 0, false)) + return e +} + +// TagLatestRevision assigns given tag to latest ready revision +func (e ServiceTraffic) TagLatestRevision(tag string) ServiceTraffic { + for i, target := range e { + if *target.LatestRevision { + e[i].Tag = tag + return e + } + } + e = append(e, newTarget(tag, "", 0, true)) + return e +} + +// SetTrafficByRevision checks given revision in existing traffic block and sets given percent if found +func (e ServiceTraffic) SetTrafficByRevision(revision string, percent int) { + for i, target := range e { + if target.RevisionName == revision { + e[i].Percent = percent + break + } + } +} + +// SetTrafficByTag checks given tag in existing traffic block and sets given percent if found +func (e ServiceTraffic) SetTrafficByTag(tag string, percent int) { + for i, target := range e { + if target.Tag == tag { + e[i].Percent = percent + break + } + } +} + +// SetTrafficByLatestRevision sets given percent to latest ready revision of service +func (e ServiceTraffic) SetTrafficByLatestRevision(percent int) { + for i, target := range e { + if *target.LatestRevision { + e[i].Percent = percent + break + } + } +} + +// ResetAllTargetPercent resets (0) 'Percent' field for all the traffic targets +func (e ServiceTraffic) ResetAllTargetPercent() { + for i := range e { + e[i].Percent = 0 + } +} + +// RemoveNullTargets removes targets from traffic block if they don't have and 0 percent traffic +func (e ServiceTraffic) RemoveNullTargets() (newTraffic ServiceTraffic) { + for _, target := range e { + if target.Tag == "" && target.Percent == 0 { + } else { + newTraffic = append(newTraffic, target) + } + } + return newTraffic +} + +func errorOverWritingtagOfLatestReadyRevision(existingTag, requestedTag string) error { + return fmt.Errorf("tag '%s' exists on latest ready revision of service, "+ + "refusing to overwrite existing tag with '%s', "+ + "add flag '--untag %s' in command to untag it", existingTag, requestedTag, existingTag) +} +func errorOverWritingTag(tag string) error { + return fmt.Errorf("refusing to overwrite existing tag in service, "+ + "add flag '--untag %s' in command to untag it", tag) +} + +func errorRepeatingRevision(forFlag string, name string) error { + if name == latestRevisionRef { + name = "identifier " + latestRevisionRef + } else { + name = "revision reference " + name + } + return fmt.Errorf("repetition of %s "+ + "is not allowed, use only once with %s flag", name, forFlag) +} + +// verifies if user has repeated @latest field in --tag or --traffic flags +// verifyInputSanity checks: +// - if user has repeated @latest field in --tag or --traffic flags +// - if provided traffic portion are integers +func verifyInputSanity(trafficFlags *flags.Traffic) error { + var latestRevisionTag = false + var sum = 0 + + for _, each := range trafficFlags.RevisionsTags { + revision, _, err := splitByEqualSign(each) + if err != nil { + return err + } + + if latestRevisionTag && revision == latestRevisionRef { + return errorRepeatingRevision("--tag", latestRevisionRef) + } + + if revision == latestRevisionRef { + latestRevisionTag = true + } + } + + revisionRefMap := make(map[string]int) + for i, each := range trafficFlags.RevisionsPercentages { + revisionRef, percent, err := splitByEqualSign(each) + if err != nil { + return err + } + + // To check if there are duplicate revision names in traffic flags + if _, exist := revisionRefMap[revisionRef]; exist { + return errorRepeatingRevision("--traffic", revisionRef) + } else { + revisionRefMap[revisionRef] = i + } + + percentInt, err := strconv.Atoi(percent) + if err != nil { + return fmt.Errorf("error converting given %s to integer value for traffic distribution", percent) + } + + if percentInt < 0 || percentInt > 100 { + return fmt.Errorf("invalid value for traffic percent %d, expected 0 <= percent <= 100", percentInt) + } + + sum += percentInt + } + + // equivalent check for `cmd.Flags().Changed("traffic")` as we don't have `cmd` in this function + if len(trafficFlags.RevisionsPercentages) > 0 && sum != 100 { + return fmt.Errorf("given traffic percents sum to %d, want 100", sum) + } + + return nil +} + +// Compute takes service traffic targets and updates per given traffic flags +func Compute(cmd *cobra.Command, targets []v1alpha1.TrafficTarget, trafficFlags *flags.Traffic) ([]v1alpha1.TrafficTarget, error) { + err := verifyInputSanity(trafficFlags) + if err != nil { + return nil, err + } + + traffic := newServiceTraffic(targets) + + // First precedence: Untag revisions + for _, tag := range trafficFlags.UntagRevisions { + traffic.untagRevision(tag) + } + + for _, each := range trafficFlags.RevisionsTags { + revision, tag, _ := splitByEqualSign(each) // err is checked in verifyInputSanity + + // Second precedence: Tag latestRevision + if revision == latestRevisionRef { + existingTagOnLatestRevision := traffic.tagOfLatestReadyRevision() + + // just pass if existing == requested + if existingTagOnLatestRevision == tag { + continue + } + + // apply requested tag only if it doesnt exist in traffic block + if traffic.isTagPresent(tag) { + return nil, errorOverWritingTag(tag) + } + + if existingTagOnLatestRevision == "" { + traffic = traffic.TagLatestRevision(tag) + continue + } else { + return nil, errorOverWritingtagOfLatestReadyRevision(existingTagOnLatestRevision, tag) + } + + } + + // Third precedence: Tag other revisions + // dont throw error if the tag present == requested tag + if traffic.isTagPresentOnRevision(tag, revision) { + continue + } + + // error if the tag is assigned to some other revision + if traffic.isTagPresent(tag) { + return nil, errorOverWritingTag(tag) + } + + traffic = traffic.TagRevision(tag, revision) + } + + if cmd.Flags().Changed("traffic") { + // reset existing traffic portions as what's on CLI is desired state of traffic split portions + traffic.ResetAllTargetPercent() + + for _, each := range trafficFlags.RevisionsPercentages { + // revisionRef works here as either revision or tag as either can be specified on CLI + revisionRef, percent, _ := splitByEqualSign(each) // err is verified in verifyInputSanity + percentInt, _ := strconv.Atoi(percent) // percentInt (for int) is verified in verifyInputSanity + + // fourth precedence: set traffic for latest revision + if revisionRef == latestRevisionRef { + if traffic.isLatestRevisionTrue() { + traffic.SetTrafficByLatestRevision(percentInt) + } else { + // if no latestRevision ref is present in traffic block + traffic = append(traffic, newTarget("", "", percentInt, true)) + } + continue + } + + // fifth precedence: set traffic for rest of revisions + // If in a traffic block, revisionName of one target == tag of another, + // one having tag is assigned given percent, as tags are supposed to be unique + // and should be used (in this case) to avoid ambiguity + + // first check if given revisionRef is a tag + if traffic.isTagPresent(revisionRef) { + traffic.SetTrafficByTag(revisionRef, percentInt) + continue + } + + // check if given revisionRef is a revision + if traffic.isRevisionPresent(revisionRef) { + traffic.SetTrafficByRevision(revisionRef, percentInt) + continue + } + + // TODO Check at serving level, improve error + //if !RevisionExists(revisionRef) { + // return error.New("Revision/Tag %s does not exists in traffic block.") + //} + + // provided revisionRef isn't present in traffic block, add it + traffic = append(traffic, newTarget("", revisionRef, percentInt, false)) + } + } + // remove any targets having no tags and 0% traffic portion + return traffic.RemoveNullTargets(), nil +} diff --git a/pkg/kn/traffic/compute_test.go b/pkg/kn/traffic/compute_test.go new file mode 100644 index 0000000000..9507c8f9d6 --- /dev/null +++ b/pkg/kn/traffic/compute_test.go @@ -0,0 +1,289 @@ +// 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 traffic + +import ( + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "gotest.tools/assert" + + "testing" + + "github.com/knative/client/pkg/kn/commands/flags" + "github.com/spf13/cobra" +) + +type trafficTestCase struct { + name string + existingTraffic []v1alpha1.TrafficTarget + inputFlags []string + desiredRevisions []string + desiredTags []string + desiredPercents []int +} + +type trafficErrorTestCase struct { + name string + existingTraffic []v1alpha1.TrafficTarget + inputFlags []string + errMsg string +} + +func newTestTrafficCommand() (*cobra.Command, *flags.Traffic) { + var trafficFlags flags.Traffic + trafficCmd := &cobra.Command{ + Use: "kn", + Short: "Traffic test kn command", + Run: func(cmd *cobra.Command, args []string) {}, + } + trafficFlags.Add(trafficCmd) + return trafficCmd, &trafficFlags +} + +func TestCompute(t *testing.T) { + for _, testCase := range []trafficTestCase{ + { + "assign 'latest' tag to @latest revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--tag", "@latest=latest"}, + []string{"@latest"}, + []string{"latest"}, + []int{100}, + }, + { + "assign tag to revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "echo-v1", 100, false)), + []string{"--tag", "echo-v1=stable"}, + []string{"echo-v1"}, + []string{"stable"}, + []int{100}, + }, + { + "re-assign same tag to same revision (unchanged)", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("current", "", 100, true)), + []string{"--tag", "@latest=current"}, + []string{""}, + []string{"current"}, + []int{100}, + }, + { + "split traffic to tags", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true), newTarget("", "rev-v1", 0, false)), + []string{"--traffic", "@latest=10,rev-v1=90"}, + []string{"@latest", "rev-v1"}, + []string{"", ""}, + []int{10, 90}, + }, + { + "split traffic to tags with '%' suffix", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true), newTarget("", "rev-v1", 0, false)), + []string{"--traffic", "@latest=10%,rev-v1=90%"}, + []string{"@latest", "rev-v1"}, + []string{"", ""}, + []int{10, 90}, + }, + { + "add 2 more tagged revisions without giving them traffic portions", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "", 100, true)), + []string{"--tag", "echo-v0=stale,echo-v1=old"}, + []string{"@latest", "echo-v0", "echo-v1"}, + []string{"latest", "stale", "old"}, + []int{100, 0, 0}, + }, + { + "re-assign same tag to 'echo-v1' revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 100, false)), + []string{"--tag", "echo-v1=latest"}, + []string{"echo-v1"}, + []string{"latest"}, + []int{100}, + }, + { + "set 2% traffic to latest revision by appending it in traffic block", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 100, false)), + []string{"--traffic", "@latest=2,echo-v1=98"}, + []string{"echo-v1", "@latest"}, + []string{"latest", ""}, + []int{98, 2}, + }, + { + "set 2% to @latest with tag (append it in traffic block)", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 100, false)), + []string{"--traffic", "@latest=2,echo-v1=98", "--tag", "@latest=testing"}, + []string{"echo-v1", "@latest"}, + []string{"latest", "testing"}, + []int{98, 2}, + }, + { + "change traffic percent of an existing revision in traffic block, add new revision with traffic share", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("v1", "echo-v1", 100, false)), + []string{"--tag", "echo-v2=v2", "--traffic", "v1=10,v2=90"}, + []string{"echo-v1", "echo-v2"}, + []string{"v1", "v2"}, + []int{10, 90}, //default value, + }, + { + "untag 'latest' tag from 'echo-v1' revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 100, false)), + []string{"--untag", "latest"}, + []string{"echo-v1"}, + []string{""}, + []int{100}, + }, + { + "replace revision pointing to 'latest' tag from 'echo-v1' to 'echo-v2' revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 50, false), newTarget("", "echo-v2", 50, false)), + []string{"--untag", "latest", "--tag", "echo-v1=old,echo-v2=latest"}, + []string{"echo-v1", "echo-v2"}, + []string{"old", "latest"}, + []int{50, 50}, + }, + { + "have multiple tags for a revision, revision present in traffic block", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "echo-v1", 50, false), newTarget("", "echo-v2", 50, false)), + []string{"--tag", "echo-v1=latest,echo-v1=current"}, + []string{"echo-v1", "echo-v2", "echo-v1"}, // appends a new target + []string{"latest", "", "current"}, // with new tag requested + []int{50, 50, 0}, // and assign 0% to it + }, + + { + "have multiple tags for a revision, revision absent in traffic block", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "echo-v2", 100, false)), + []string{"--tag", "echo-v1=latest,echo-v1=current"}, + []string{"echo-v2", "echo-v1", "echo-v1"}, // appends two new targets + []string{"", "latest", "current"}, // with new tags requested + []int{100, 0, 0}, // and assign 0% to each + }, + { + "re-assign same tag 'current' to @latest", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("current", "", 100, true)), + []string{"--tag", "@latest=current"}, + []string{""}, + []string{"current"}, // since no change, no error + []int{100}, + }, + { + "assign echo-v1 10% traffic adjusting rest to @latest, echo-v1 isn't present in existing traffic block", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "echo-v1=10,@latest=90"}, + []string{"", "echo-v1"}, + []string{"", ""}, // since no change, no error + []int{90, 10}, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + if lper, lrev, ltag := len(testCase.desiredPercents), len(testCase.desiredRevisions), len(testCase.desiredTags); lper != lrev || lper != ltag { + t.Fatalf("length of desird revisions, tags and percents is mismatched: got=(desiredPercents, desiredRevisions, desiredTags)=(%d, %d, %d)", + lper, lrev, ltag) + } + + testCmd, tFlags := newTestTrafficCommand() + testCmd.SetArgs(testCase.inputFlags) + testCmd.Execute() + targets, err := Compute(testCmd, testCase.existingTraffic, tFlags) + if err != nil { + t.Fatal(err) + } + for i, target := range targets { + if testCase.desiredRevisions[i] == "@latest" { + assert.Equal(t, *target.LatestRevision, true) + } else { + assert.Equal(t, target.RevisionName, testCase.desiredRevisions[i]) + } + assert.Equal(t, target.Tag, testCase.desiredTags[i]) + assert.Equal(t, target.Percent, testCase.desiredPercents[i]) + } + }) + } +} + +func TestComputeErrMsg(t *testing.T) { + for _, testCase := range []trafficErrorTestCase{ + { + "invalid format for --traffic option", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "@latest=100=latest"}, + "expecting the value format in value1=value2, given @latest=100=latest", + }, + { + "invalid format for --tag option", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--tag", "@latest="}, + "expecting the value format in value1=value2, given @latest=", + }, + { + "repeatedly spliting traffic to @latest revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "@latest=90,@latest=10"}, + "repetition of identifier @latest is not allowed, use only once with --traffic flag", + }, + { + "repeatedly tagging to @latest revision not allowed", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--tag", "@latest=latest,@latest=2"}, + "repetition of identifier @latest is not allowed, use only once with --tag flag", + }, + { + "overwriting tag not allowed, to @latest from other revision", + append(append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "", 2, true)), newTarget("stable", "echo-v2", 98, false)), + []string{"--tag", "@latest=stable"}, + "refusing to overwrite existing tag in service, add flag '--untag stable' in command to untag it", + }, + { + "overwriting tag not allowed, to a revision from other revision", + append(append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("latest", "", 2, true)), newTarget("stable", "echo-v2", 98, false)), + []string{"--tag", "echo-v2=latest"}, + "refusing to overwrite existing tag in service, add flag '--untag latest' in command to untag it", + }, + { + "overwriting tag of @latest not allowed, existing != requested", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("candidate", "", 100, true)), + []string{"--tag", "@latest=current"}, + "tag 'candidate' exists on latest ready revision of service, refusing to overwrite existing tag with 'current', add flag '--untag candidate' in command to untag it", + }, + { + "verify error for non integer values given to percent", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "@latest=100p"}, + "error converting given 100p to integer value for traffic distribution", + }, + { + "verify error for traffic sum not equal to 100", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "@latest=19,echo-v1=71"}, + "given traffic percents sum to 90, want 100", + }, + { + "verify error for values out of range given to percent", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "@latest=-100"}, + "invalid value for traffic percent -100, expected 0 <= percent <= 100", + }, + { + "repeatedly spliting traffic to the same revision", + append(newServiceTraffic([]v1alpha1.TrafficTarget{}), newTarget("", "", 100, true)), + []string{"--traffic", "echo-v1=40", "--traffic", "echo-v1=60"}, + "repetition of revision reference echo-v1 is not allowed, use only once with --traffic flag", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + testCmd, tFlags := newTestTrafficCommand() + testCmd.SetArgs(testCase.inputFlags) + testCmd.Execute() + _, err := Compute(testCmd, testCase.existingTraffic, tFlags) + assert.Error(t, err, testCase.errMsg) + }) + } +} diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index 6ba5c8f83d..ae511f85d7 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -59,6 +59,5 @@ initialize $@ header "Running tests for Knative serving $KNATIVE_VERSION" -go_test_e2e ./test/e2e || fail_test - +go_test_e2e -timeout=30m ./test/e2e || fail_test success diff --git a/test/e2e/common.go b/test/e2e/common.go index 4136b148b2..7371814d86 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "regexp" + "strconv" "strings" "sync" "testing" @@ -72,6 +73,14 @@ func getNamespaceCountAndIncrement() int { return current } +func getServiceNameAndIncrement(base string) string { + m.Lock() + defer m.Unlock() + current := serviceCount + serviceCount++ + return base + strconv.Itoa(current) +} + // Teardown clean up func (test *e2eTest) Teardown(t *testing.T) { test.DeleteTestNamespace(t, test.env.Namespace) diff --git a/test/e2e/env.go b/test/e2e/env.go index 438669b2ac..5bf95a087f 100644 --- a/test/e2e/env.go +++ b/test/e2e/env.go @@ -26,7 +26,10 @@ type env struct { const defaultKnE2ENamespace = "kne2etests" -var namespaceCount = 0 +var ( + namespaceCount = 0 + serviceCount = 0 +) func buildEnv(t *testing.T) env { env := env{ diff --git a/test/e2e/traffic_split_test.go b/test/e2e/traffic_split_test.go new file mode 100644 index 0000000000..c0fa5af386 --- /dev/null +++ b/test/e2e/traffic_split_test.go @@ -0,0 +1,389 @@ +// 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 im +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build e2e + +package e2e + +import ( + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/knative/client/pkg/util" + "gotest.tools/assert" +) + +var targetsSeparator = "|" +var targetFieldsSeparator = "," +var targetFieldsLength = 4 + +// returns deployed service targets separated by '|' and each target fields seprated by comma +var targetsJsonPath = "jsonpath={range .status.traffic[*]}{.tag}{','}{.revisionName}{','}{.percent}{','}{.latestRevision}{'|'}{end}" + +// returns deployed service latest revision name +var latestRevisionJsonPath = "jsonpath={.status.traffic[?(@.latestRevision==true)].revisionName}" + +// TargetFileds are used in e2e to store expected fields per traffic target +// and actual traffic targets fields of deployed service are converted into struct before comparing +type TargetFields struct { + Tag string + Revision string + Percent int + Latest bool +} + +func newTargetFields(tag, revision string, percent int, latest bool) TargetFields { + return TargetFields{tag, revision, percent, latest} +} + +func splitTargets(s, separator string, partsCount int) ([]string, error) { + parts := strings.SplitN(s, separator, partsCount) + if len(parts) != partsCount { + return nil, errors.New(fmt.Sprintf("expecting to receive parts of length %d, got %d "+ + "string: %s seprator: %s", partsCount, len(parts), s, separator)) + } + return parts, nil +} + +// formatActualTargets takes the traffic targets string received after jsonpath operation and converts +// them into []TargetFields for comparison +func formatActualTargets(t *testing.T, actualTargets []string) (formattedTargets []TargetFields) { + for _, each := range actualTargets { + each := strings.TrimSuffix(each, targetFieldsSeparator) + fields, err := splitTargets(each, targetFieldsSeparator, targetFieldsLength) + assert.NilError(t, err) + percentInt, err := strconv.Atoi(fields[2]) + assert.NilError(t, err) + latestBool, err := strconv.ParseBool(fields[3]) + assert.NilError(t, err) + formattedTargets = append(formattedTargets, newTargetFields(fields[0], fields[1], percentInt, latestBool)) + } + return +} + +// TestTrafficSplit runs different e2e tests for service traffic splitting and verifies the traffic targets from service status +func TestTrafficSplit(t *testing.T) { + t.Parallel() + test := NewE2eTest(t) + test.Setup(t) + defer test.Teardown(t) + + serviceBase := "echo" + t.Run("tag two revisions as v1 and v2 and give 50-50% share", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v1"}) + rev1 := test.latestRevisionOfService(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + tflags := []string{"--tag", fmt.Sprintf("%s=v1,%s=v2", rev1, rev2), + "--traffic", "v1=50,v2=50"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // make ordered fields per tflags (tag, revision, percent, latest) + expectedTargets := []TargetFields{newTargetFields("v1", rev1, 50, false), newTargetFields("v2", rev2, 50, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("ramp/up down a revision to 20% adjusting other traffic to accommodate", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v1"}) + rev1 := test.latestRevisionOfService(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + tflags := []string{"--traffic", fmt.Sprintf("%s=20,%s=80", rev1, rev2)} // traffic by revision name + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev1, 20, false), newTargetFields("", rev2, 80, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("tag a revision as candidate, without otherwise changing any traffic split", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v1"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + tflags := []string{"--tag", fmt.Sprintf("%s=%s", rev1, "candidate")} // no traffic, append new target with tag in traffic block + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 100, true), newTargetFields("candidate", rev1, 0, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("tag a revision as candidate, set 2% traffic adjusting other traffic to accommodate", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v1"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + tflags := []string{"--tag", fmt.Sprintf("%s=%s", rev1, "candidate"), + "--traffic", "candidate=2%,@latest=98%"} // traffic by tag name and use % at the end + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 98, true), newTargetFields("candidate", rev1, 2, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("update tag for a revision from candidate to current, tag current is present on another revision", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + // make available 3 revisions for service first + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v3"}) //note that this gives 100% traffic to latest revision (rev3) + rev3 := test.latestRevisionOfService(t, serviceName) + + // make existing state: tag current and candidate exist in traffic block + tflags := []string{"--tag", fmt.Sprintf("%s=current,%s=candidate", rev1, rev2)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state of tags: update tag of revision (rev2) from candidate to current (which is present on rev1) + tflags = []string{"--untag", "current,candidate", "--tag", fmt.Sprintf("%s=current", rev2)} //untag first to update + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // there will be 2 targets in existing block 1. @latest, 2.for revision $rev2 + // target for rev1 is removed as it had no traffic and we untagged it's tag current + expectedTargets := []TargetFields{newTargetFields("", rev3, 100, true), newTargetFields("current", rev2, 0, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("update tag from testing to staging for @latest revision", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + // make existing state: tag @latest as testing + tflags := []string{"--tag", "@latest=testing"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state: change tag from testing to staging + tflags = []string{"--untag", "testing", "--tag", "@latest=staging"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("staging", rev1, 100, true)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("update tag from testing to staging for a revision (non @latest)", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + // make existing state: tag a revision as testing + tflags := []string{"--tag", fmt.Sprintf("%s=testing", rev1)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state: change tag from testing to staging + tflags = []string{"--untag", "testing", "--tag", fmt.Sprintf("%s=staging", rev1)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 100, true), + newTargetFields("staging", rev1, 0, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + // test reducing number of targets from traffic blockdd + t.Run("remove a revision with tag old from traffic block entierly", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + // existing state: traffic block having a revision with tag old and some traffic + tflags := []string{"--tag", fmt.Sprintf("%s=old", rev1), + "--traffic", "old=2,@latest=98"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state: remove revision with tag old + tflags = []string{"--untag", "old", "--traffic", "@latest=100"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 100, true)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("tag a revision as stable and current with 50-50% traffic", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + // existing state: traffic block having two targets + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + + // desired state: tag non-@latest revision with two tags and 50-50% traffic each + tflags := []string{"--tag", fmt.Sprintf("%s=stable,%s=current", rev1, rev1), + "--traffic", "stable=50%,current=50%"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("stable", rev1, 50, false), newTargetFields("current", rev1, 50, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("revert all traffic to latest ready revision of service", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + // existing state: latest revision not getting any traffic + tflags := []string{"--traffic", fmt.Sprintf("%s=100", rev1)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state: revert traffic to latest revision + tflags = []string{"--traffic", "@latest=100"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 100, true)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("tag latest ready revision of service as current", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + // existing state: latest revision has no tag + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + // desired state: tag current to latest ready revision + tflags := []string{"--tag", "@latest=current"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("current", rev1, 100, true)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("update tag for a revision as testing and assign all the traffic to it:", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + // existing state: two revision exists with traffic share and + // each revision has tag and traffic portions + tflags := []string{"--tag", fmt.Sprintf("@latest=current,%s=candidate", rev1), + "--traffic", "current=90,candidate=10"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state: update tag for rev1 as testing (from candidate) with 100% traffic + tflags = []string{"--untag", "candidate", "--tag", fmt.Sprintf("%s=testing", rev1), + "--traffic", "testing=100"} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("current", rev2, 0, true), + newTargetFields("testing", rev1, 100, false)} + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) + t.Run("replace latest tag of a revision with old and give latest to another revision", + func(t *testing.T) { + serviceName := getServiceNameAndIncrement(serviceBase) + test.serviceCreate(t, serviceName) + rev1 := test.latestRevisionOfService(t, serviceName) + + test.serviceUpdateWithOptions(t, serviceName, []string{"--env", "TARGET=v2"}) + rev2 := test.latestRevisionOfService(t, serviceName) + + // existing state: a revision exist with latest tag + tflags := []string{"--tag", fmt.Sprintf("%s=latest", rev1)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + // desired state of revision tags: rev1=old rev2=latest + tflags = []string{"--untag", "latest", "--tag", fmt.Sprintf("%s=old,%s=latest", rev1, rev2)} + test.serviceUpdateWithOptions(t, serviceName, tflags) + + expectedTargets := []TargetFields{newTargetFields("", rev2, 100, true), + newTargetFields("old", rev1, 0, false), + // Tagging by revision name adds a new target even though latestReadyRevision==rev2, + // because we didn't refer @latest reference, but explcit name of revision. + // In spec of traffic block (not status) either latestReadyRevision:true or revisionName can be given per target + newTargetFields("latest", rev2, 0, false)} + + test.verifyTargets(t, serviceName, expectedTargets) + test.serviceDelete(t, serviceName) + }, + ) +} + +func (test *e2eTest) verifyTargets(t *testing.T, serviceName string, expectedTargets []TargetFields) { + out := test.serviceDescribeWithJsonPath(t, serviceName, targetsJsonPath) + assert.Check(t, out != "") + out = strings.TrimSuffix(out, targetsSeparator) + actualTargets, err := splitTargets(out, targetsSeparator, len(expectedTargets)) + assert.NilError(t, err) + formattedActualTargets := formatActualTargets(t, actualTargets) + assert.DeepEqual(t, expectedTargets, formattedActualTargets) +} + +func (test *e2eTest) latestRevisionOfService(t *testing.T, serviceName string) string { + return test.serviceDescribeWithJsonPath(t, serviceName, latestRevisionJsonPath) +} + +func (test *e2eTest) serviceDescribeWithJsonPath(t *testing.T, serviceName, jsonpath string) string { + command := []string{"service", "describe", serviceName, "-o", jsonpath} + out, err := test.kn.RunWithOpts(command, runOpts{}) + assert.NilError(t, err) + return out +} + +func (test *e2eTest) serviceUpdateWithOptions(t *testing.T, serviceName string, options []string) { + command := []string{"service", "update", serviceName} + command = append(command, options...) + out, err := test.kn.RunWithOpts(command, runOpts{NoNamespace: false}) + assert.NilError(t, err) + assert.Check(t, util.ContainsAll(out, "Service", serviceName, "update", "namespace", test.kn.namespace)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c00710e25f..ef1bee2b87 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,11 +66,11 @@ github.com/knative/build/pkg/apis/build # github.com/knative/pkg v0.0.0-20190617142447-13b093adc272 github.com/knative/pkg/apis github.com/knative/pkg/apis/duck/v1beta1 +github.com/knative/pkg/ptr github.com/knative/pkg/kmp github.com/knative/pkg/apis/duck github.com/knative/pkg/apis/duck/v1alpha1 github.com/knative/pkg/kmeta -github.com/knative/pkg/ptr github.com/knative/pkg/configmap # github.com/knative/serving v0.6.0 github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1