mirror of https://github.com/knative/client.git
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:
parent
c4e8d5a964
commit
746dacc47c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 '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`,
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue