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