Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ spec: # One of "runLatest", "release", "pinned" (DEPRECATED), or "manual"
release:
# Ordered list of 1 or 2 revisions. First revision is traffic target
# "current" and second revision is traffic target "candidate".
# It is possible to specify `"@latest"` string as a shortcut to the lastest available revision.
Comment thread
vagababov marked this conversation as resolved.
revisions: ["myservice-00013", "myservice-00015"]
rolloutPercent: 50 # Percent [0-99] of traffic to route to "candidate" revision
configuration: # serving.knative.dev/v1alpha1.ConfigurationSpec
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/serving/v1alpha1/service_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ type ReleaseType struct {
Configuration ConfigurationSpec `json:"configuration,omitempty"`
}

// ReleaseLatestRevisionKeyword is a shortcut for usage in the `release` mode
// to refer to the latest created revision.
// See #2819 for details.
const ReleaseLatestRevisionKeyword = "@latest"

// RunLatestType contains the options for always having a route to the latest configuration. See
// ServiceSpec for more details.
type RunLatestType struct {
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/serving/v1alpha1/service_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func (rt *ReleaseType) Validate() *apis.FieldError {
errs = errs.Also(apis.ErrOutOfBoundsValue(strconv.Itoa(numRevisions), "1", "2", "revisions"))
}
for i, r := range rt.Revisions {
// Skip over the last revision special keyword.
if r == ReleaseLatestRevisionKeyword {
continue
}
if msgs := validation.IsDNS1035Label(r); len(msgs) > 0 {
errs = errs.Also(apis.ErrInvalidArrayValue(
fmt.Sprintf("not a DNS 1035 label: %v", msgs), "revisions", i))
Expand Down
22 changes: 22 additions & 0 deletions pkg/apis/serving/v1alpha1/service_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,28 @@ func TestServiceValidation(t *testing.T) {
},
},
want: apis.ErrInvalidValue(incorrectDNS1035Label, "spec.release.revisions[0]"),
}, {
name: "valid release -- with @latest",
s: &Service{
ObjectMeta: metav1.ObjectMeta{
Name: "valid",
},
Spec: ServiceSpec{
Release: &ReleaseType{
Revisions: []string{"s-1-00001", ReleaseLatestRevisionKeyword},
Configuration: ConfigurationSpec{
RevisionTemplate: RevisionTemplateSpec{
Spec: RevisionSpec{
Container: corev1.Container{
Image: "hellworld",
},
},
},
},
},
},
},
want: nil,
}, {
name: "invalid release -- too few revisions; empty slice",
s: &Service{
Expand Down
29 changes: 21 additions & 8 deletions pkg/reconciler/v1alpha1/service/resources/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,34 @@ func MakeRoute(service *v1alpha1.Service) (*v1alpha1.Route, error) {
numRevisions := len(service.Spec.Release.Revisions)

// Configure the 'current' route.
currentRevisionName := service.Spec.Release.Revisions[0]
ttCurrent := v1alpha1.TrafficTarget{
Name: "current",
Percent: 100 - rolloutPercent,
RevisionName: currentRevisionName,
Name: "current",
Percent: 100 - rolloutPercent,
}
currentRevisionName := service.Spec.Release.Revisions[0]

// If the `current` revision refers to the well known name of the last
// known revision, use `Configuration` instead.
// Same for the `candidate` below.
// Part of #2819.
if currentRevisionName == v1alpha1.ReleaseLatestRevisionKeyword {
ttCurrent.ConfigurationName = names.Configuration(service)
Comment thread
mattmoor marked this conversation as resolved.
} else {
ttCurrent.RevisionName = currentRevisionName
}
c.Spec.Traffic = append(c.Spec.Traffic, ttCurrent)

// Configure the 'candidate' route.
if numRevisions == 2 {
candidateRevisionName := service.Spec.Release.Revisions[1]
ttCandidate := v1alpha1.TrafficTarget{
Name: "candidate",
Percent: rolloutPercent,
RevisionName: candidateRevisionName,
Name: "candidate",
Percent: rolloutPercent,
}
candidateRevisionName := service.Spec.Release.Revisions[1]
if candidateRevisionName == v1alpha1.ReleaseLatestRevisionKeyword {
ttCandidate.ConfigurationName = names.Configuration(service)
} else {
ttCandidate.RevisionName = candidateRevisionName
}
c.Spec.Traffic = append(c.Spec.Traffic, ttCandidate)
}
Expand Down
126 changes: 126 additions & 0 deletions pkg/reconciler/v1alpha1/service/resources/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package resources
import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/knative/serving/pkg/apis/serving"
"github.com/knative/serving/pkg/apis/serving/v1alpha1"
"github.com/knative/serving/pkg/reconciler/v1alpha1/service/resources/names"
)

Expand Down Expand Up @@ -159,6 +161,130 @@ func TestRouteReleaseSingleRevision(t *testing.T) {
}
}

func TestRouteLatestRevisionSplit(t *testing.T) {
const (
rolloutPercent = 42
currentPercent = 100 - rolloutPercent
)
s := createServiceWithRelease(2 /*num revisions*/, rolloutPercent)
s.Spec.Release.Revisions = []string{v1alpha1.ReleaseLatestRevisionKeyword, "juicy-revision"}
testConfigName := names.Configuration(s)
r, err := MakeRoute(s)
if err != nil {
t.Errorf("Expected nil for err got %q", err)
}
if got, want := r.Name, testServiceName; got != want {
t.Errorf("Expected %q for service name got %q", want, got)
}
if got, want := r.Namespace, testServiceNamespace; got != want {
t.Errorf("Expected %q for service namespace got %q", want, got)
}
wantT := []v1alpha1.TrafficTarget{{
Name: "current",
Percent: currentPercent,
ConfigurationName: testConfigName,
}, {
Name: "candidate",
Percent: rolloutPercent,
RevisionName: "juicy-revision",
}, {
Name: "latest",
ConfigurationName: testConfigName,
}}
if got, want := r.Spec.Traffic, wantT; !cmp.Equal(got, want) {
t.Errorf("Traffic mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
expectOwnerReferencesSetCorrectly(t, r.OwnerReferences)

wantL := map[string]string{
testLabelKey: testLabelValueRelease,
serving.ServiceLabelKey: testServiceName,
}
if got, want := r.Labels, wantL; !cmp.Equal(got, want) {
t.Errorf("Labels mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
}
func TestRouteLatestRevisionSplitCandidate(t *testing.T) {
const (
rolloutPercent = 42
currentPercent = 100 - rolloutPercent
)
s := createServiceWithRelease(2 /*num revisions*/, rolloutPercent)
s.Spec.Release.Revisions = []string{"squishy-revision", v1alpha1.ReleaseLatestRevisionKeyword}
testConfigName := names.Configuration(s)
r, err := MakeRoute(s)
if err != nil {
t.Errorf("Expected nil for err got %q", err)
}
if got, want := r.Name, testServiceName; got != want {
t.Errorf("Expected %q for service name got %q", want, got)
}
if got, want := r.Namespace, testServiceNamespace; got != want {
t.Errorf("Expected %q for service namespace got %q", want, got)
}
wantT := []v1alpha1.TrafficTarget{{
Name: "current",
Percent: currentPercent,
RevisionName: "squishy-revision",
}, {
Name: "candidate",
Percent: rolloutPercent,
ConfigurationName: testConfigName,
}, {
Name: "latest",
ConfigurationName: testConfigName,
}}
if got, want := r.Spec.Traffic, wantT; !cmp.Equal(got, want) {
t.Errorf("Traffic mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
expectOwnerReferencesSetCorrectly(t, r.OwnerReferences)

wantL := map[string]string{
testLabelKey: testLabelValueRelease,
serving.ServiceLabelKey: testServiceName,
}
if got, want := r.Labels, wantL; !cmp.Equal(got, want) {
t.Errorf("Labels mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
}
func TestRouteLatestRevisionNoSplit(t *testing.T) {
s := createServiceWithRelease(1 /*num revisions*/, 0 /*unused*/)
s.Spec.Release.Revisions = []string{v1alpha1.ReleaseLatestRevisionKeyword}
testConfigName := names.Configuration(s)
r, err := MakeRoute(s)

if err != nil {
t.Errorf("Expected nil for err got %q", err)
}
if got, want := r.Name, testServiceName; got != want {
t.Errorf("Expected %q for service name got %q", want, got)
}
if got, want := r.Namespace, testServiceNamespace; got != want {
t.Errorf("Expected %q for service namespace got %q", want, got)
}
// Should have 2 named traffic targets (current, latest)
wantT := []v1alpha1.TrafficTarget{{
Name: "current",
Percent: 100,
ConfigurationName: testConfigName,
}, {
Name: "latest",
ConfigurationName: testConfigName,
}}
if got, want := r.Spec.Traffic, wantT; !cmp.Equal(got, want) {
t.Errorf("Traffic mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
expectOwnerReferencesSetCorrectly(t, r.OwnerReferences)

wantL := map[string]string{
testLabelKey: testLabelValueRelease,
serving.ServiceLabelKey: testServiceName,
}
if got, want := r.Labels, wantL; !cmp.Equal(got, want) {
t.Errorf("Labels mismatch: diff (-got, +want): %s", cmp.Diff(got, want))
}
}

func TestRouteReleaseTwoRevisions(t *testing.T) {
rolloutPercent := 48
currentPercent := 52
Expand Down
119 changes: 119 additions & 0 deletions pkg/reconciler/v1alpha1/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ func TestReconcile(t *testing.T) {
WantServiceReadyStats: map[string]int{
"foo/pinned3": 1,
},
}, {
Name: "release - with @latest",
Objects: []runtime.Object{
svc("release", "foo", WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword)),
},
Key: "foo/release",
WantCreates: []metav1.Object{
config("release", "foo", WithReleaseRollout("release-00001")),
route("release", "foo", WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword)),
},
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: svc("release", "foo", WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword),
// The first reconciliation will initialize the status conditions.
WithInitSvcConditions),
}},
WantEvents: []string{
Eventf(corev1.EventTypeNormal, "Created", "Created Configuration %q", "release"),
Eventf(corev1.EventTypeNormal, "Created", "Created Route %q", "release"),
Eventf(corev1.EventTypeNormal, "Updated", "Updated Service %q", "release"),
},
}, {
Name: "release - create route and service",
Objects: []runtime.Object{
Expand Down Expand Up @@ -324,6 +344,105 @@ func TestReconcile(t *testing.T) {
WantEvents: []string{
Eventf(corev1.EventTypeNormal, "Updated", "Updated Service %q", "release-nr-ts2"),
},
}, {
Name: "release - route and config ready, using @latest",
Objects: []runtime.Object{
svc("release-ready-lr", "foo",
WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword), WithInitSvcConditions),
route("release-ready-lr", "foo",
WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword),
RouteReady, WithDomain, WithDomainInternal, WithAddress, WithInitRouteConditions,
WithStatusTraffic([]v1alpha1.TrafficTarget{{
Name: "current",
RevisionName: "release-ready-lr-00001",
Percent: 100,
}, {
Name: "latest",
RevisionName: "release-ready-lr-00001",
}}...), MarkTrafficAssigned, MarkIngressReady),
config("release-ready-lr", "foo", WithReleaseRollout("release-ready-lr"), WithGeneration(1),
// These turn a Configuration to Ready=true
WithLatestCreated, WithLatestReady),
},
Key: "foo/release-ready-lr",
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: svc("release-ready-lr", "foo",
WithReleaseRollout(v1alpha1.ReleaseLatestRevisionKeyword),
// The delta induced by the config object.
WithReadyConfig("release-ready-lr-00001"),
// The delta induced by route object.
WithReadyRoute, WithSvcStatusDomain, WithSvcStatusAddress,
WithSvcStatusTraffic([]v1alpha1.TrafficTarget{{
Name: "current",
RevisionName: "release-ready-lr-00001",
Percent: 100,
}, {
Name: "latest",
RevisionName: "release-ready-lr-00001",
}}...),
),
}},
WantEvents: []string{
Eventf(corev1.EventTypeNormal, "Updated", "Updated Service %q", "release-ready-lr"),
},
WantServiceReadyStats: map[string]int{
"foo/release-ready-lr": 1,
},
}, {
Name: "release - route and config ready, traffic split, using @latest",
Objects: []runtime.Object{
svc("release-ready-lr", "foo",
WithReleaseRolloutAndPercentage(
42, "release-ready-lr-00001", v1alpha1.ReleaseLatestRevisionKeyword), WithInitSvcConditions),
route("release-ready-lr", "foo",
WithReleaseRolloutAndPercentage(
42, "release-ready-lr-00001", v1alpha1.ReleaseLatestRevisionKeyword),
RouteReady, WithDomain, WithDomainInternal, WithAddress, WithInitRouteConditions,
WithStatusTraffic([]v1alpha1.TrafficTarget{{
Name: "current",
RevisionName: "release-ready-lr-00001",
Percent: 58,
}, {
Name: "candidate",
RevisionName: "release-ready-lr-00002",
Percent: 42,
}, {
Name: "latest",
RevisionName: "release-ready-lr-00002",
}}...), MarkTrafficAssigned, MarkIngressReady),
config("release-ready-lr", "foo", WithReleaseRollout("release-ready-lr"), WithGeneration(2),
// These turn a Configuration to Ready=true
WithLatestCreated, WithLatestReady),
},
Key: "foo/release-ready-lr",
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: svc("release-ready-lr", "foo",
WithReleaseRolloutAndPercentage(
42, "release-ready-lr-00001", v1alpha1.ReleaseLatestRevisionKeyword),
// The delta induced by the config object.
WithReadyConfig("release-ready-lr-00002"),
// The delta induced by route object.
WithReadyRoute, WithSvcStatusDomain, WithSvcStatusAddress,
WithSvcStatusTraffic([]v1alpha1.TrafficTarget{{
Name: "current",
RevisionName: "release-ready-lr-00001",
Percent: 58,
}, {
Name: "candidate",
RevisionName: "release-ready-lr-00002",
Percent: 42,
}, {
Name: "latest",
RevisionName: "release-ready-lr-00002",
}}...),
),
}},
WantEvents: []string{
Eventf(corev1.EventTypeNormal, "Updated", "Updated Service %q", "release-ready-lr"),
},
WantServiceReadyStats: map[string]int{
"foo/release-ready-lr": 1,
},
}, {
Name: "release - route and config ready, propagate ready, percentage set",
Objects: []runtime.Object{
Expand Down
Loading