client/pkg/serving/config_changes.go

278 lines
8.8 KiB
Go

// 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 serving
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/serving/pkg/apis/autoscaling"
servingv1alpha1 "knative.dev/serving/pkg/apis/serving/v1alpha1"
servingv1beta1 "knative.dev/serving/pkg/apis/serving/v1beta1"
)
var UserImageAnnotationKey = "client.knative.dev/user-image"
// UpdateEnvVars gives the configuration all the env var values listed in the given map of
// vars. Does not touch any environment variables not mentioned, but it can add
// new env vars and change the values of existing ones, then sort by env key name.
func UpdateEnvVars(template *servingv1alpha1.RevisionTemplateSpec, toUpdate map[string]string, toRemove []string) error {
container, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
updated := updateEnvVarsFromMap(container.Env, toUpdate)
updated = removeEnvVars(updated, toRemove)
// Sort by env key name
sort.SliceStable(updated, func(i, j int) bool {
return updated[i].Name < updated[j].Name
})
container.Env = updated
return nil
}
// UpdateMinScale updates min scale annotation
func UpdateMinScale(template *servingv1alpha1.RevisionTemplateSpec, min int) error {
return UpdateAnnotation(template, autoscaling.MinScaleAnnotationKey, strconv.Itoa(min))
}
// UpdatMaxScale updates max scale annotation
func UpdateMaxScale(template *servingv1alpha1.RevisionTemplateSpec, max int) error {
return UpdateAnnotation(template, autoscaling.MaxScaleAnnotationKey, strconv.Itoa(max))
}
// UpdateConcurrencyTarget updates container concurrency annotation
func UpdateConcurrencyTarget(template *servingv1alpha1.RevisionTemplateSpec, target int) error {
// TODO(toVersus): Remove the following validation once serving library is updated to v0.8.0
// and just rely on ValidateAnnotations method.
if target < autoscaling.TargetMin {
return fmt.Errorf("invalid 'concurrency-target' value: must be an integer greater than 0: %s",
autoscaling.TargetAnnotationKey)
}
return UpdateAnnotation(template, autoscaling.TargetAnnotationKey, strconv.Itoa(target))
}
// UpdateConcurrencyLimit updates container concurrency limit
func UpdateConcurrencyLimit(template *servingv1alpha1.RevisionTemplateSpec, limit int) error {
cc := servingv1beta1.RevisionContainerConcurrencyType(limit)
// Validate input limit
ctx := context.Background()
if err := cc.Validate(ctx).ViaField("spec.containerConcurrency"); err != nil {
return fmt.Errorf("invalid 'concurrency-limit' value: %s", err)
}
template.Spec.ContainerConcurrency = cc
return nil
}
// UpdateAnnotation updates (or adds) an annotation to the given service
func UpdateAnnotation(template *servingv1alpha1.RevisionTemplateSpec, annotation string, value string) error {
annoMap := template.Annotations
if annoMap == nil {
annoMap = make(map[string]string)
template.Annotations = annoMap
}
// Validate autoscaling annotations and returns error if invalid input provided
// without changing the existing spec
in := make(map[string]string)
in[annotation] = value
if err := autoscaling.ValidateAnnotations(in); err != nil {
return err
}
annoMap[annotation] = value
return nil
}
var ApiTooOldError = errors.New("the service is using too old of an API format for the operation")
// UpdateName updates the revision name.
func UpdateName(template *servingv1alpha1.RevisionTemplateSpec, name string) error {
if template.Spec.DeprecatedContainer != nil {
return ApiTooOldError
}
template.Name = name
return nil
}
// EnvToMap is an utility function to translate between the API list form of env vars, and the
// more convenient map form.
func EnvToMap(vars []corev1.EnvVar) (map[string]string, error) {
result := map[string]string{}
for _, envVar := range vars {
_, present := result[envVar.Name]
if present {
return nil, fmt.Errorf("env var name present more than once: %v", envVar.Name)
}
result[envVar.Name] = envVar.Value
}
return result, nil
}
// UpdateImage a given image
func UpdateImage(template *servingv1alpha1.RevisionTemplateSpec, image string) error {
// When not setting the image to a digest, add the user image annotation.
container, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
container.Image = image
return nil
}
// UnsetUserImageAnnot removes the user image annotation
func UnsetUserImageAnnot(template *servingv1alpha1.RevisionTemplateSpec) {
delete(template.Annotations, UserImageAnnotationKey)
}
// SetUserImageAnnot sets the user image annotation if the image isn't by-digest already.
func SetUserImageAnnot(template *servingv1alpha1.RevisionTemplateSpec) {
// If the current image isn't by-digest, set the user-image annotation to it
// so we remember what it was.
currentContainer, _ := ContainerOfRevisionTemplate(template)
ui := currentContainer.Image
if strings.Contains(ui, "@") {
prev, ok := template.Annotations[UserImageAnnotationKey]
if ok {
ui = prev
}
}
if template.Annotations == nil {
template.Annotations = make(map[string]string)
}
template.Annotations[UserImageAnnotationKey] = ui
}
// FreezeImageToDigest sets the image on the template to the image digest of the base revision.
func FreezeImageToDigest(template *servingv1alpha1.RevisionTemplateSpec, baseRevision *servingv1alpha1.Revision) error {
if baseRevision == nil {
return nil
}
currentContainer, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
baseContainer, err := ContainerOfRevisionSpec(&baseRevision.Spec)
if err != nil {
return err
}
if currentContainer.Image != baseContainer.Image {
return fmt.Errorf("could not freeze image to digest since current revision contains unexpected image.")
}
if baseRevision.Status.ImageDigest != "" {
return UpdateImage(template, baseRevision.Status.ImageDigest)
}
return nil
}
// UpdateContainerPort updates container with a give port
func UpdateContainerPort(template *servingv1alpha1.RevisionTemplateSpec, port int32) error {
container, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
container.Ports = []corev1.ContainerPort{{
ContainerPort: port,
}}
return nil
}
// UpdateResources updates resources as requested
func UpdateResources(template *servingv1alpha1.RevisionTemplateSpec, requestsResourceList corev1.ResourceList, limitsResourceList corev1.ResourceList) error {
container, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
if container.Resources.Requests == nil {
container.Resources.Requests = corev1.ResourceList{}
}
for k, v := range requestsResourceList {
container.Resources.Requests[k] = v
}
if container.Resources.Limits == nil {
container.Resources.Limits = corev1.ResourceList{}
}
for k, v := range limitsResourceList {
container.Resources.Limits[k] = v
}
return nil
}
// UpdateLabels updates the labels identically on a service and template.
// Does not overwrite the entire Labels field, only makes the requested updates
func UpdateLabels(service *servingv1alpha1.Service, template *servingv1alpha1.RevisionTemplateSpec, toUpdate map[string]string, toRemove []string) error {
if service.ObjectMeta.Labels == nil {
service.ObjectMeta.Labels = make(map[string]string)
}
if template.ObjectMeta.Labels == nil {
template.ObjectMeta.Labels = make(map[string]string)
}
for key, value := range toUpdate {
service.ObjectMeta.Labels[key] = value
template.ObjectMeta.Labels[key] = value
}
for _, key := range toRemove {
delete(service.ObjectMeta.Labels, key)
delete(template.ObjectMeta.Labels, key)
}
return nil
}
// =======================================================================================
func updateEnvVarsFromMap(env []corev1.EnvVar, toUpdate map[string]string) []corev1.EnvVar {
set := sets.NewString()
for i := range env {
envVar := &env[i]
if val, ok := toUpdate[envVar.Name]; ok {
envVar.Value = val
set.Insert(envVar.Name)
}
}
for name, val := range toUpdate {
if !set.Has(name) {
env = append(env, corev1.EnvVar{Name: name, Value: val})
}
}
return env
}
func removeEnvVars(env []corev1.EnvVar, toRemove []string) []corev1.EnvVar {
for _, name := range toRemove {
for i, envVar := range env {
if envVar.Name == name {
env = append(env[:i], env[i+1:]...)
break
}
}
}
return env
}