feature(service): Implements traffic splitting and tagging targets (#345)

- Add e2e tests
 - Use '=' for traffic and tag assignment instead of ':'
 - Use --tag and --untag flags for tagging traffic targets
 - Use --traffic flag for setting traffic portions
 - Allow --traffic portion to either take revisionName or tagName
 - Uses @latest identifier for referencing latest revision of service
 - Dont throw error if requested revision=tag pair is same
 - Support having multiple tags for a revision
	 - creates a new target in traffic block if revision present in traffic block with new tag requested
	 - creates N new targets in traffic block if revision absent in traffic block with Nxnew tags requested
 - Ensure updating tag of @latest requires --untag flag
	 - streamline updating tag for latestReadyRevision
	 - adds respective tests
	 - adds tests for ensuring given traffic sum to 100 on CLI and fail fast
 - Add note about preference of order in case where tagOfOneRevision == revisionOfAnother,
   first tags are checked and assigned traffic if any, as tags are supposed to be
   unique in traffic block and should be referenced in such scenario.
 - Remove the examples from flag description, moves it to service update command example section
 - Pass only traffic block to compute trffic, makes it better to consume.
 - Cover more error cases for invalid value format for assignments, covers a=b=c, a=, =b, or variants of them
 - Separate and improves the error messages
 - Add unit tests for traffic computing
 - Add sanity checks in dedicated function verifyInputSanity
 	  - traffic perents should sum to 100
          - individual percent should be in 0-100
          - repetition of @latest or tagName or revisionRef is disallowed
 - Verify traffic percents sum to 100 on client side and fail fast
 - Add e2e tests for traffic splitting
	 - create and update service, assign tags and set traffic to make an existing state
	 - run the scenario on existing state of service
	 - form the desired state traffic block
	 - extract the traffic block and form the traffic block struct actual state
	 - assert.DeepEqual actual and desired traffic blocks
 - Use logic to generate service name in the same way as namespace, use different service name per test case
 - Run e2e test for traffic splitting in parallel
 - Use timeout duration of 30m for e2e tests, use timeout parameter for go_test_e2e library function
 - Use tagName in flag description of --untag, avoiding conflict with --tag flag
 - Update CHANGELOG
This commit is contained in:
Navid Shaikh 2019-08-15 23:26:08 +05:30 committed by Knative Prow Robot
parent c4e8d5a964
commit 746dacc47c
11 changed files with 1173 additions and 17 deletions

View File

@ -42,6 +42,11 @@
| Add --revision-name flag | Add --revision-name flag
| https://github.com/knative/client/pull/282[#282] | 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) ## v0.2.0 (2019-07-10)

View File

@ -14,14 +14,25 @@ kn service update NAME [flags]
``` ```
# Updates a service 'mysvc' with new environment variables # Updates a service 'svc' with new environment variables
kn service update mysvc --env KEY1=VALUE1 --env KEY2=VALUE2 kn service update svc --env KEY1=VALUE1 --env KEY2=VALUE2
# Update a service 'mysvc' with new port # Update a service 'svc' with new port
kn service update mysvc --port 80 kn service update svc --port 80
# Updates a service 'mysvc' with new requests and limits parameters # Updates a service 'svc' with new requests and limits parameters
kn service update mysvc --requests-cpu 500m --limits-memory 1024Mi 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 ### Options
@ -43,6 +54,9 @@ kn service update NAME [flags]
--requests-cpu string The requested CPU (e.g., 250m). --requests-cpu string The requested CPU (e.g., 250m).
--requests-memory string The requested memory (e.g., 64Mi). --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}}") --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) --wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 60)
``` ```

View File

@ -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)
}

View File

@ -18,6 +18,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/knative/client/pkg/kn/commands/flags"
"github.com/knative/client/pkg/kn/traffic"
"github.com/spf13/cobra" "github.com/spf13/cobra"
api_errors "k8s.io/apimachinery/pkg/api/errors" api_errors "k8s.io/apimachinery/pkg/api/errors"
@ -27,19 +29,30 @@ import (
func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command { func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
var editFlags ConfigurationEditFlags var editFlags ConfigurationEditFlags
var waitFlags commands.WaitFlags var waitFlags commands.WaitFlags
var trafficFlags flags.Traffic
serviceUpdateCommand := &cobra.Command{ serviceUpdateCommand := &cobra.Command{
Use: "update NAME [flags]", Use: "update NAME [flags]",
Short: "Update a service.", Short: "Update a service.",
Example: ` Example: `
# Updates a service 'mysvc' with new environment variables # Updates a service 'svc' with new environment variables
kn service update mysvc --env KEY1=VALUE1 --env KEY2=VALUE2 kn service update svc --env KEY1=VALUE1 --env KEY2=VALUE2
# Update a service 'mysvc' with new port # Update a service 'svc' with new port
kn service update mysvc --port 80 kn service update svc --port 80
# Updates a service 'mysvc' with new requests and limits parameters # Updates a service 'svc' with new requests and limits parameters
kn service update mysvc --requests-cpu 500m --limits-memory 1024Mi`, 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`,
RunE: func(cmd *cobra.Command, args []string) (err error) { RunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) != 1 { if len(args) != 1 {
return errors.New("requires the service name.") return errors.New("requires the service name.")
@ -69,6 +82,15 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
return err 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) err = client.UpdateService(service)
if err != nil { if err != nil {
// Retry to update when a resource version conflict exists // 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) commands.AddNamespaceFlags(serviceUpdateCommand.Flags(), false)
editFlags.AddUpdateFlags(serviceUpdateCommand) editFlags.AddUpdateFlags(serviceUpdateCommand)
waitFlags.AddConditionWaitFlags(serviceUpdateCommand, 60, "Update", "service") waitFlags.AddConditionWaitFlags(serviceUpdateCommand, 60, "Update", "service")
trafficFlags.Add(serviceUpdateCommand)
return serviceUpdateCommand return serviceUpdateCommand
} }

369
pkg/kn/traffic/compute.go Normal file
View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -59,6 +59,5 @@ initialize $@
header "Running tests for Knative serving $KNATIVE_VERSION" 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 success

View File

@ -21,6 +21,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -72,6 +73,14 @@ func getNamespaceCountAndIncrement() int {
return current return current
} }
func getServiceNameAndIncrement(base string) string {
m.Lock()
defer m.Unlock()
current := serviceCount
serviceCount++
return base + strconv.Itoa(current)
}
// Teardown clean up // Teardown clean up
func (test *e2eTest) Teardown(t *testing.T) { func (test *e2eTest) Teardown(t *testing.T) {
test.DeleteTestNamespace(t, test.env.Namespace) test.DeleteTestNamespace(t, test.env.Namespace)

View File

@ -26,7 +26,10 @@ type env struct {
const defaultKnE2ENamespace = "kne2etests" const defaultKnE2ENamespace = "kne2etests"
var namespaceCount = 0 var (
namespaceCount = 0
serviceCount = 0
)
func buildEnv(t *testing.T) env { func buildEnv(t *testing.T) env {
env := env{ env := env{

View File

@ -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))
}

2
vendor/modules.txt vendored
View File

@ -66,11 +66,11 @@ github.com/knative/build/pkg/apis/build
# github.com/knative/pkg v0.0.0-20190617142447-13b093adc272 # github.com/knative/pkg v0.0.0-20190617142447-13b093adc272
github.com/knative/pkg/apis github.com/knative/pkg/apis
github.com/knative/pkg/apis/duck/v1beta1 github.com/knative/pkg/apis/duck/v1beta1
github.com/knative/pkg/ptr
github.com/knative/pkg/kmp github.com/knative/pkg/kmp
github.com/knative/pkg/apis/duck github.com/knative/pkg/apis/duck
github.com/knative/pkg/apis/duck/v1alpha1 github.com/knative/pkg/apis/duck/v1alpha1
github.com/knative/pkg/kmeta github.com/knative/pkg/kmeta
github.com/knative/pkg/ptr
github.com/knative/pkg/configmap github.com/knative/pkg/configmap
# github.com/knative/serving v0.6.0 # github.com/knative/serving v0.6.0
github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1 github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1