Skip to content
Closed
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
4 changes: 3 additions & 1 deletion cmd/queue/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ var (
servingRevisionKey string
servingAutoscaler string
servingAutoscalerPort string
userTargetPort string
statChan = make(chan *autoscaler.Stat, statReportingQueueLength)
reqChan = make(chan queue.ReqEvent, requestCountingQueueLength)
statSink *websocket.ManagedConnection
Expand All @@ -84,6 +85,7 @@ func initEnv() {
servingRevision = util.GetRequiredEnvOrFatal("SERVING_REVISION", logger)
servingAutoscaler = util.GetRequiredEnvOrFatal("SERVING_AUTOSCALER", logger)
servingAutoscalerPort = util.GetRequiredEnvOrFatal("SERVING_AUTOSCALER_PORT", logger)
userTargetPort = util.GetRequiredEnvOrFatal("USER_PORT", logger)

// TODO(mattmoor): Move this key to be in terms of the KPA.
servingRevisionKey = autoscaler.NewKpaKey(servingNamespace, servingRevision)
Expand Down Expand Up @@ -225,7 +227,7 @@ func main() {
zap.String(logkey.Key, servingRevisionKey),
zap.String(logkey.Pod, podName))

target, err := url.Parse("http://localhost:8080")
target, err := url.Parse(fmt.Sprintf("http://localhost:%s", userTargetPort))
if err != nil {
logger.Fatal("Failed to parse localhost url", zap.Error(err))
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/serving/v1alpha1/revision_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ const (
RevisionContainerConcurrencyMax RevisionContainerConcurrencyType = 1000
)

const (
RevisionContainerUserPortName = "user-port"
RevisionContainerUserPortDefaultValue = 8080
)

// RevisionSpec holds the desired state of the Revision (from the client).
type RevisionSpec struct {
// TODO: Generation does not work correctly with CRD. They are scrubbed
Expand Down
29 changes: 26 additions & 3 deletions pkg/apis/serving/v1alpha1/revision_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1

import (
"fmt"
"strconv"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -109,9 +110,6 @@ func validateContainer(container corev1.Container) *apis.FieldError {
if !equality.Semantic.DeepEqual(container.Resources, corev1.ResourceRequirements{}) {
ignoredFields = append(ignoredFields, "resources")
}
if len(container.Ports) > 0 {
ignoredFields = append(ignoredFields, "ports")
}
if len(container.VolumeMounts) > 0 {
ignoredFields = append(ignoredFields, "volumeMounts")
}
Expand All @@ -123,6 +121,9 @@ func validateContainer(container corev1.Container) *apis.FieldError {
// Complain about all ignored fields so that user can remove them all at once.
errs = errs.Also(apis.ErrDisallowedFields(ignoredFields...))
}
if err := validateContainerPorts(container.Ports); err != nil {
errs = errs.Also(err.ViaField("ports"))
}
// Validate our probes
if err := validateProbe(container.ReadinessProbe).ViaField("readinessProbe"); err != nil {
errs = errs.Also(err)
Expand All @@ -133,6 +134,28 @@ func validateContainer(container corev1.Container) *apis.FieldError {
return errs
}

func validateContainerPorts(ports []corev1.ContainerPort) *apis.FieldError {
// user can set container port which names "user-port" to define application's port.
// Queue-proxy will use it to send requests to application
// if user didn't set any port, it will set default port user-port=8080.
if len(ports) > 1 {
return &apis.FieldError{
Message: "More than one container ports is set",
Paths: []string{apis.CurrentField},
Details: "Only a single port named \"user-port\" is allowed",
}
}

if len(ports) == 1 && ports[0].Name != "user-port" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should actually enforce that there is no name on the port for now. We can preserve the name 'user-port' on the deployment; however, the name is intended to be used for HTTP negotiation when specified on Configuration. Allowing it or requiring it to be 'user-port' would not allow us to implement the runtime contract. See https://github.com/knative/serving/blob/master/docs/runtime-contract.md#inbound-network-connectivity

Copy link
Copy Markdown
Author

@Leon-Liangming Leon-Liangming Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it means accept any name of the port ?
hmm, it's about how to set user-port by user, it seems we didn't discuss about that clearly. @mattmoor @evankanderson Do you have any suggestions here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify my understanding of the contract, there should only ever be 1 port in the Configuration Spec (or configuration section of Service Spec). Configuration.Spec.Container.Ports[0].Name should either be "" or "http1" or "h2c". Since we currently only implement automatic detection it probably makes sense to only accept "" for Name.

When the revision is created by Knative, the revision creates a K8s Deployment object from the spec. The port name on the Deployment is currently being set to 'user-port', and I think it makes sense to keep this set for clarity that the 'user-port' containerPort on the deployment was set from Configuration.Spec.Container.Ports[0].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To quote the spec (because apparently I had deep discussions about this 6 months ago, and was smarter than I am when looking at this issue for 10 minutes today):

Only one inbound containerPort SHALL be specified in the core.v1.Container specification. The hostPort parameter SHOULD NOT be set by the developer or the platform provider, as it can interfere with ingress autoscaling. Regardless of its source, the selected port will be made available in the PORT environment variable.

The platform provider SHOULD configure the platform to perform HTTPS termination and protocol transformation e.g. between QUIC or HTTP/2 and HTTP/1.1. Developers should not need to implement multiple transports between the platform and their code. Unless overridden by setting the name field on the inbound port, the platform will perform automatic detection as described above. If the core.v1.Container.ports[0].name is set to one of the following values, HTTP negotiation will be disabled and the following protocol will be used:

  • http1: HTTP/1.1 transport and will not attempt to upgrade to h2c..
  • h2c: HTTP/2 transport, as described in section 3.4 of the HTTP2 spec (Starting HTTP/2 with Prior Knowledge)

Developers SHOULD prefer to use automatic content negotiation where available, and MUST NOT set the name field to arbitrary values, as additional transports may be defined in the future.

All of the above is about the Container object specified in the Revision. The underlying implementation (e.g. creating a Deployment to back the Revision) can use whatever names it wants. The content-negotiation information needs to be forwarded to queue-proxy (this is not done today).

return &apis.FieldError{
Message: fmt.Sprintf("Port name %v is not allowed", ports[0].Name),
Paths: []string{apis.CurrentField},
Details: "Only a single port named \"user-port\" is allowed",
}
}
return nil
}

func validateBuildRef(buildRef *corev1.ObjectReference) *apis.FieldError {
if buildRef == nil {
return nil
Expand Down
30 changes: 27 additions & 3 deletions pkg/apis/serving/v1alpha1/revision_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,34 @@ func TestContainerValidation(t *testing.T) {
},
want: apis.ErrDisallowedFields("resources"),
}, {
name: "has ports",
name: "has more than one port",
c: corev1.Container{
Ports: []corev1.ContainerPort{{
Name: "http",
ContainerPort: 8080,
},{
Name: "http2",
ContainerPort: 8181,
}},
},
want: apis.ErrDisallowedFields("ports"),
want: &apis.FieldError{
Message: "More than one container ports is set",
Paths: []string{"ports"},
Details: "Only a single port named \"user-port\" is allowed",
},
}, {
name: "set wrong name port",
c: corev1.Container{
Ports: []corev1.ContainerPort{{
Name: "http",
ContainerPort: 8080,
}},
},
want: &apis.FieldError{
Message: fmt.Sprintf("Port name %v is not allowed", "http"),
Paths: []string{"ports"},
Details: "Only a single port named \"user-port\" is allowed",
},
}, {
name: "has volumeMounts",
c: corev1.Container{
Expand Down Expand Up @@ -151,7 +171,11 @@ func TestContainerValidation(t *testing.T) {
}},
Lifecycle: &corev1.Lifecycle{},
},
want: apis.ErrDisallowedFields("name", "resources", "ports", "volumeMounts", "lifecycle"),
want: apis.ErrDisallowedFields("name", "resources", "volumeMounts", "lifecycle").Also(&apis.FieldError{
Message: fmt.Sprintf("Port name %v is not allowed", "http"),
Paths: []string{"ports"},
Details: "Only a single port named \"user-port\" is allowed",
}),
}}

for _, test := range tests {
Expand Down
2 changes: 0 additions & 2 deletions pkg/reconciler/v1alpha1/revision/resources/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ const (
// TODO(mattmoor): Make this private once we remove revision_test.go
IstioOutboundIPRangeAnnotation = "traffic.sidecar.istio.io/includeOutboundIPRanges"

userPortName = "user-port"
userPort = 8080
userPortEnvName = "PORT"

autoscalerPort = 8080
Expand Down
53 changes: 37 additions & 16 deletions pkg/reconciler/v1alpha1/revision/resources/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,6 @@ var (
MountPath: "/var/log",
}

userPorts = []corev1.ContainerPort{{
Name: userPortName,
ContainerPort: int32(userPort),
}}

// Expose containerPort as env PORT.
userEnv = corev1.EnvVar{
Name: userPortEnvName,
Value: strconv.Itoa(userPort),
}

userResources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: userContainerCPU,
Expand All @@ -81,7 +70,7 @@ var (
}
)

func rewriteUserProbe(p *corev1.Probe) {
func rewriteUserProbe(p *corev1.Probe, userPort int) {
if p == nil {
return
}
Expand All @@ -101,19 +90,24 @@ func makePodSpec(rev *v1alpha1.Revision, loggingConfig *logging.Config, observab
// update the validations in pkg/webhook.validateContainer.
userContainer.Name = userContainerName
userContainer.Resources = userResources
userContainer.Ports = userPorts
userContainer.VolumeMounts = append(userContainer.VolumeMounts, varLogVolumeMount)
userContainer.Lifecycle = userLifecycle
userContainer.Env = append(userContainer.Env, userEnv)

userPort, found := getUserPort(rev)
if !found {
createAndSetDefaultUserPort(userContainer)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here seems a bit strange that you return the default port when not found, but then don't use in createAndSetDefaultUserPort.

To me it would feel a bit more natural if getUserPort returned 0 for userPort when false, and createAndSetDefaultUserPort returned the default userPort. Otherwise you are relying on userPort to be equal to what createAndSetDefaultUserPort set which could easily become a bug.

Copy link
Copy Markdown
Author

@Leon-Liangming Leon-Liangming Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your point.
According to @mattmoor's comment,
#discussion_r227053050

Instead of passing in additional EnvVars can we instead factor a function getUserPort(Revision) -> int64 and call it both here and queue.go?

get port from revision, used in two places, userPort need to get default port if can't find userport.
I change it to use getUserPort's result in createAndSetDefaultUserPort to resolve this pb.

}
userContainer.Env = append(userContainer.Env, buildUserPortEnv(userPort))
userContainer.Env = append(userContainer.Env, getKnativeEnvVar(rev)...)

// Prefer imageDigest from revision if available
if rev.Status.ImageDigest != "" {
userContainer.Image = rev.Status.ImageDigest
}

// If the client provides probes, we should fill in the port for them.
rewriteUserProbe(userContainer.ReadinessProbe)
rewriteUserProbe(userContainer.LivenessProbe)
rewriteUserProbe(userContainer.ReadinessProbe, int(userPort))
rewriteUserProbe(userContainer.LivenessProbe, int(userPort))

podSpec := &corev1.PodSpec{
Containers: []corev1.Container{
Expand All @@ -133,6 +127,33 @@ func makePodSpec(rev *v1alpha1.Revision, loggingConfig *logging.Config, observab
return podSpec
}

func createAndSetDefaultUserPort(userContainer *corev1.Container) {
defaultUserPort := &corev1.ContainerPort{
Name: v1alpha1.RevisionContainerUserPortName,
ContainerPort: int32(v1alpha1.RevisionContainerUserPortDefaultValue),
}
userContainer.Ports = append(userContainer.Ports, *defaultUserPort)
}

func getUserPort(rev *v1alpha1.Revision) (int32, bool) {
for _, port := range rev.Spec.Container.Ports {
if port.Name == v1alpha1.RevisionContainerUserPortName {
return port.ContainerPort, true
}
}

return v1alpha1.RevisionContainerUserPortDefaultValue, false
}

func buildUserPortEnv(userPort int32) corev1.EnvVar {
// Expose containerPort as env PORT.
userPortEnv := corev1.EnvVar{
Name: userPortEnvName,
Value: strconv.Itoa(int(userPort)),
}
return userPortEnv
}

func MakeDeployment(rev *v1alpha1.Revision,
loggingConfig *logging.Config, networkConfig *config.Network, observabilityConfig *config.Observability,
autoscalerConfig *autoscaler.Config, controllerConfig *config.Controller) *appsv1.Deployment {
Expand Down
Loading