Skip to content

Commit

Permalink
deploy: handle config change trigger in deployment config controller
Browse files Browse the repository at this point in the history
  • Loading branch information
mfojtik committed Jun 28, 2017
1 parent 4664e87 commit 32425bb
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type DeploymentConfigController struct {
func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig) error {
glog.V(5).Infof("Reconciling %s/%s", config.Namespace, config.Name)
// There's nothing to reconcile until the version is nonzero.
if config.Status.LatestVersion == 0 {
if config.Status.LatestVersion == 0 && !deployutil.HasTrigger(config) {
return c.updateStatus(config, []*v1.ReplicationController{})
}

Expand Down Expand Up @@ -181,6 +181,16 @@ func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig)
c.recorder.Eventf(config, v1.EventTypeNormal, "DeploymentAwaitingCancellation", "Deployment of version %d awaiting cancellation of older running deployments", config.Status.LatestVersion)
return fmt.Errorf("found previous inflight deployment for %s - requeuing", deployutil.LabelForDeploymentConfig(config))
}
// Process triggers and start an initial rollouts
shouldTrigger, shouldSkip := triggerActivated(config, latestIsDeployed, latestDeployment)
if !shouldSkip && shouldTrigger {
config.Status.LatestVersion++
return c.updateStatus(config, existingDeployments)
}
// Have to wait for the image trigger to get the image before proceeding.
if shouldSkip && deployutil.IsInitialDeployment(config) {
return c.updateStatus(config, existingDeployments)
}
// If the latest deployment already exists, reconcile existing deployments
// and return early.
if latestIsDeployed {
Expand Down Expand Up @@ -251,6 +261,78 @@ func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig)
return c.updateStatus(config, existingDeployments, *cond)
}

// triggerActivated indicates whether we should proceed with new rollout as one of the
// triggers were activated (config change or image change). The first bool indicates that
// the triggers are active and second indicates if we should skip the rollout because we
// are waiting for the trigger to complete update (waiting for image for example).
func triggerActivated(config *deployapi.DeploymentConfig, latestIsDeployed bool, latestDeployment *v1.ReplicationController) (bool, bool) {
if config.Spec.Paused {
return false, false
}
imageTrigger := deployutil.HasImageChangeTrigger(config)
configTrigger := deployutil.HasChangeTrigger(config)
hasTrigger := imageTrigger || configTrigger

// no-op when no triggers are defined.
if !hasTrigger {
return false, false
}

// Handle initial rollouts
if deployutil.IsInitialDeployment(config) {
// Rollout if image change triggers are present and all triggers have their images
// available.
if imageTrigger && deployutil.HasLastTriggeredImage(config) {
glog.Infof("Rolling out initial deployment for %s/%s as it have its image available", config.Namespace, config.Name)
return true, false
}
// Skip rollout if image change triggers are present but the images are not ready yet.
if imageTrigger && !deployutil.HasLastTriggeredImage(config) {
glog.Infof("Rolling out initial deployment for %s/%s paused until the images are available", config.Namespace, config.Name)
return false, true
}
// Rollout if we only have config change trigger.
if configTrigger && !imageTrigger {
glog.Infof("Rolling out initial deployment for %s/%s", config.Namespace, config.Name)
return true, false
}
// We waiting for the initial RC to be created.
return false, false
}

// Wait for the RC to be created
if !latestIsDeployed {
return false, true
}

// We need existing deployment at this point to compare its template with current config template.
if latestDeployment == nil {
return false, false
}

if imageTrigger {
if ok, name := deployutil.HasUpdatedImages(config, latestDeployment); ok {
glog.V(4).Infof("Rolling out #%d deployment for %s/%s caused by image change (%s)", config.Status.LatestVersion+1, config.Namespace, config.Name, name)
deployutil.RecordImageChangeCause(config, name)
return true, false
}
}

if configTrigger {
isLatest, changes, err := deployutil.HasLatestPodTemplate(config, latestDeployment)
if err != nil {
glog.Errorf("Error while checking for latest pod template in replication controller: %v", err)
return false, true
}
if !isLatest {
glog.V(4).Infof("Rolling out #%d deployment for %s/%s caused by config change, diff: %s", config.Status.LatestVersion+1, config.Namespace, config.Name, changes)
deployutil.RecordConfigChangeCause(config)
return true, false
}
}
return false, false
}

// reconcileDeployments reconciles existing deployment replica counts which
// could have diverged outside the deployment process (e.g. due to auto or
// manual scaling, or partial deployments). The active deployment is the last
Expand Down
133 changes: 133 additions & 0 deletions pkg/deploy/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package util
import (
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
Expand All @@ -12,6 +13,8 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
Expand All @@ -28,6 +31,19 @@ var (
// deployment config. This is used in the ownerRef and GC client picks the appropriate
// client to get the deployment config.
DeploymentConfigControllerRefKind = deployapiv1.SchemeGroupVersion.WithKind("DeploymentConfig")

// annotationsToSkip is list of replication controller pod template annotations we skip
annotationsToSkip = sets.NewString(
deployapi.DeploymentVersionAnnotation,
deployapi.DeploymentConfigAnnotation,
deployapi.DeploymentAnnotation,
)

// labelsToSkip is list of replication controller pod template labels we skip
labelsToSkip = sets.NewString(
deployapi.DeploymentLabel,
deployapi.DeploymentConfigLabel,
)
)

// NewDeploymentCondition creates a new deployment condition.
Expand Down Expand Up @@ -210,6 +226,53 @@ func HasImageChangeTrigger(config *deployapi.DeploymentConfig) bool {
return false
}

// HasTrigger returns whether the provided deployment configuration has any trigger
// defined or not.
func HasTrigger(config *deployapi.DeploymentConfig) bool {
return HasChangeTrigger(config) || HasImageChangeTrigger(config)
}

// HasLastTriggeredImage returns whether all image change triggers in provided deployment
// configuration has the lastTriggerImage field set (iow. all images were updated for
// them). Returns false if deployment configuration has no image change trigger defined.
func HasLastTriggeredImage(config *deployapi.DeploymentConfig) bool {
hasImageTrigger := false
for _, trigger := range config.Spec.Triggers {
if trigger.Type == deployapi.DeploymentTriggerOnImageChange {
hasImageTrigger = true
if len(trigger.ImageChangeParams.LastTriggeredImage) == 0 {
return false
}
}
}
return hasImageTrigger
}

// IsInitialDeployment returns whether the deployment configuration is the first version
// of this configuration.
func IsInitialDeployment(config *deployapi.DeploymentConfig) bool {
return config.Status.LatestVersion == 0
}

// RecordConfigChangeCause sets a deployment config cause for config change.
func RecordConfigChangeCause(config *deployapi.DeploymentConfig) {
config.Status.Details = new(deployapi.DeploymentDetails)
config.Status.Details.Causes = []deployapi.DeploymentCause{{
Type: deployapi.DeploymentTriggerOnConfigChange,
}}
config.Status.Details.Message = "config change"
}

// RecordImageChangeCause sets a deployment config cause for image change.
func RecordImageChangeCause(config *deployapi.DeploymentConfig, name string) {
config.Status.Details = new(deployapi.DeploymentDetails)
config.Status.Details.Causes = []deployapi.DeploymentCause{{
Type: deployapi.DeploymentTriggerOnImageChange,
ImageTrigger: &deployapi.DeploymentCauseImageTrigger{From: api.ObjectReference{Kind: "DockerImage", Name: name}},
}}
config.Status.Details.Message = "image change"
}

func DeploymentConfigDeepCopy(dc *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
objCopy, err := api.Scheme.DeepCopy(dc)
if err != nil {
Expand Down Expand Up @@ -274,6 +337,72 @@ func CopyApiEnvVarToV1EnvVar(in []api.EnvVar) []v1.EnvVar {
return out
}

func CopyPodTemplateSpecToV1PodTemplateSpec(spec *api.PodTemplateSpec) *v1.PodTemplateSpec {
copied, err := api.Scheme.DeepCopy(spec)
if err != nil {
panic(err)
}
in := copied.(*api.PodTemplateSpec)
out := &v1.PodTemplateSpec{}
if err := v1.Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in, out, nil); err != nil {
panic(err)
}
return out
}

// HasLatestPodTemplate checks if the provided replication controller has the latest
// deployment config pod template. If not it will return false and the string diff.
func HasLatestPodTemplate(dc *deployapi.DeploymentConfig, rc *v1.ReplicationController) (bool, string, error) {
configTemplate := CopyPodTemplateSpecToV1PodTemplateSpec(dc.Spec.Template)
configTemplate = filterTemplateAnnotationsAndLabels(configTemplate)
rcCopy, err := DeploymentDeepCopyV1(rc)
if err != nil {
return true, "", err
}
rcTemplate := filterTemplateAnnotationsAndLabels(rcCopy.Spec.Template)
if reflect.DeepEqual(configTemplate, rcTemplate) {
return true, "", nil
}
return false, diff.ObjectReflectDiff(configTemplate, rcTemplate), nil
}

// HasUpdatedImages indicates that deployment configuration images were updated.
func HasUpdatedImages(dc *deployapi.DeploymentConfig, rc *v1.ReplicationController) (bool, string) {
rcImages := sets.NewString()
for _, c := range rc.Spec.Template.Spec.Containers {
rcImages.Insert(c.Image)
}
for _, c := range dc.Spec.Template.Spec.Containers {
if !rcImages.Has(c.Image) {
return true, c.Image
}
}
return false, ""
}

// filterTemplateAnnotationsAndLabels filters out annotations and labels we skip when
// comparing replication controller template with deployment config template.
func filterTemplateAnnotationsAndLabels(in *v1.PodTemplateSpec) *v1.PodTemplateSpec {
out := *in
out.Annotations = map[string]string{}
out.Labels = map[string]string{}
if in.Annotations != nil {
for k, v := range in.Annotations {
if !annotationsToSkip.Has(k) {
out.Annotations[k] = v
}
}
}
if in.Labels != nil {
for k, v := range in.Labels {
if !labelsToSkip.Has(k) {
out.Labels[k] = v
}
}
}
return &out
}

// DecodeDeploymentConfig decodes a DeploymentConfig from controller using codec. An error is returned
// if the controller doesn't contain an encoded config.
func DecodeDeploymentConfig(controller runtime.Object, decoder runtime.Decoder) (*deployapi.DeploymentConfig, error) {
Expand Down Expand Up @@ -375,13 +504,17 @@ func MakeDeploymentV1(config *deployapi.DeploymentConfig, codec runtime.Codec) (
for k, v := range config.Spec.Template.Labels {
podLabels[k] = v
}
// NOTE: You need to update the labelsToSkip variable in pkg/deploy/util when
// adding a new label here.
podLabels[deployapi.DeploymentConfigLabel] = config.Name
podLabels[deployapi.DeploymentLabel] = deploymentName

podAnnotations := make(labels.Set)
for k, v := range config.Spec.Template.Annotations {
podAnnotations[k] = v
}
// NOTE: You need to update the annotationsToSkip variable in pkg/deploy/util when
// adding a new annotation here.
podAnnotations[deployapi.DeploymentAnnotation] = deploymentName
podAnnotations[deployapi.DeploymentConfigAnnotation] = config.Name
podAnnotations[deployapi.DeploymentVersionAnnotation] = strconv.FormatInt(config.Status.LatestVersion, 10)
Expand Down
1 change: 0 additions & 1 deletion test/integration/authorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,6 @@ var globalDeploymentConfigGetterUsers = sets.NewString(
"system:serviceaccount:kube-system:generic-garbage-collector",
"system:serviceaccount:kube-system:namespace-controller",
"system:serviceaccount:openshift-infra:image-trigger-controller",
"system:serviceaccount:openshift-infra:deployment-trigger-controller",
"system:serviceaccount:openshift-infra:deploymentconfig-controller",
"system:serviceaccount:openshift-infra:template-instance-controller",
"system:serviceaccount:openshift-infra:unidling-controller",
Expand Down
14 changes: 0 additions & 14 deletions test/testdata/bootstrappolicy/bootstrap_cluster_role_bindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -694,20 +694,6 @@ items:
namespace: openshift-infra
userNames:
- system:serviceaccount:openshift-infra:deploymentconfig-controller
- apiVersion: v1
groupNames: null
kind: ClusterRoleBinding
metadata:
creationTimestamp: null
name: system:openshift:controller:deployment-trigger-controller
roleRef:
name: system:openshift:controller:deployment-trigger-controller
subjects:
- kind: ServiceAccount
name: deployment-trigger-controller
namespace: openshift-infra
userNames:
- system:serviceaccount:openshift-infra:deployment-trigger-controller
- apiVersion: v1
groupNames: null
kind: ClusterRoleBinding
Expand Down
46 changes: 0 additions & 46 deletions test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3218,52 +3218,6 @@ items:
- get
- list
- watch
- apiGroups:
- ""
attributeRestrictions: null
resources:
- events
verbs:
- create
- patch
- update
- apiVersion: v1
kind: ClusterRole
metadata:
annotations:
authorization.openshift.io/system-only: "true"
creationTimestamp: null
name: system:openshift:controller:deployment-trigger-controller
rules:
- apiGroups:
- ""
attributeRestrictions: null
resources:
- replicationcontrollers
verbs:
- get
- list
- watch
- apiGroups:
- ""
- apps.openshift.io
attributeRestrictions: null
resources:
- deploymentconfigs
verbs:
- get
- list
- watch
- apiGroups:
- ""
- image.openshift.io
attributeRestrictions: null
resources:
- imagestreams
verbs:
- get
- list
- watch
- apiGroups:
- ""
- apps.openshift.io
Expand Down

0 comments on commit 32425bb

Please sign in to comment.