mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 04:34:17 +00:00
167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
package deploy
|
|
|
|
import (
|
|
"maps"
|
|
"sort"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/psviderski/uncloud/pkg/api"
|
|
)
|
|
|
|
type ContainerSpecStatus string
|
|
|
|
const (
|
|
ContainerUpToDate ContainerSpecStatus = "up-to-date"
|
|
// ContainerNeedsUpdate means the container can be updated in place without recreating it.
|
|
ContainerNeedsUpdate ContainerSpecStatus = "needs-update"
|
|
ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate"
|
|
)
|
|
|
|
func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) ContainerSpecStatus {
|
|
current = current.SetDefaults()
|
|
new = new.SetDefaults()
|
|
|
|
if current.Mode != new.Mode {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
if current.Name != new.Name {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
// If pull policy is set to always, the container needs to be recreated.
|
|
if new.Container.PullPolicy == api.PullPolicyAlways {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
new.Container.PullPolicy = current.Container.PullPolicy
|
|
|
|
// Save mutable container resources that can be updated without recreation.
|
|
newResources := new.Container.Resources
|
|
// Temporarily set mutable container resources to current values to check if other properties changed.
|
|
new.Container.Resources = current.Container.Resources
|
|
|
|
// Check if immutable container properties changed.
|
|
if !current.Container.Equals(new.Container) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
if !cmp.Equal(current.Placement, new.Placement, cmpopts.EquateEmpty()) {
|
|
// TODO: this could be just an in-place spec update when available.
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
// TODO: change ports check to ContainerNeedsUpdate when ingress ports are stored only in the machine DB instead
|
|
// of as labels ans synced to the cluster store. Host ports changes should be handled as ContainerNeedsRecreate.
|
|
if !api.PortsEqual(current.Ports, new.Ports) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
// Compare volumes.
|
|
if len(current.Volumes) != len(new.Volumes) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
sortVolumes(current.Volumes)
|
|
sortVolumes(new.Volumes)
|
|
for i := range current.Volumes {
|
|
if !current.Volumes[i].Equals(new.Volumes[i]) {
|
|
// TODO: require only spec update for cases (see TODOs in tests):
|
|
// * bind volumes with different CreateHostPath
|
|
// * changed reference name in spec but preserved original volume name
|
|
// TODO: should defined but not used (no corresponding mounts) volumes be simply ignored?
|
|
return ContainerNeedsRecreate
|
|
}
|
|
}
|
|
|
|
// Compare configs.
|
|
if len(current.Configs) != len(new.Configs) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
sortConfigs(current.Configs)
|
|
sortConfigs(new.Configs)
|
|
for i := range current.Configs {
|
|
if !current.Configs[i].Equals(new.Configs[i]) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
}
|
|
|
|
if current.Container.Resources.ValidateUpdateInPlace(newResources) != nil {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
// Check if any mutable properties changed.
|
|
if !current.Caddy.Equals(new.Caddy) {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
|
|
// CPU and memory limits are updated on the running container. Service labels are only stored in the service spec
|
|
// and not applied to Docker containers.
|
|
if !current.Container.Resources.Equals(newResources) ||
|
|
!cmp.Equal(current.Labels, new.Labels, cmpopts.EquateEmpty()) {
|
|
return ContainerNeedsUpdate
|
|
}
|
|
|
|
return ContainerUpToDate
|
|
}
|
|
|
|
func sortVolumes(volumes []api.VolumeSpec) {
|
|
sort.Slice(volumes, func(i, j int) bool {
|
|
return volumes[i].Name < volumes[j].Name
|
|
})
|
|
}
|
|
|
|
func sortConfigs(configs []api.ConfigSpec) {
|
|
sort.Slice(configs, func(i, j int) bool {
|
|
return configs[i].Name < configs[j].Name
|
|
})
|
|
}
|
|
|
|
// EvalContainerChange evaluates whether the running container matches the new spec, can be updated in place to
|
|
// match it, or needs to be recreated. Unlike EvalContainerSpecChange, it also compares the new spec with the CPU and
|
|
// memory limits that Docker applies to the container, so the container is updated when they drifted from its spec.
|
|
func EvalContainerChange(current api.ServiceContainer, new api.ServiceSpec) ContainerSpecStatus {
|
|
status := EvalContainerSpecChange(current.ServiceSpec, new)
|
|
if status == ContainerNeedsRecreate {
|
|
return status
|
|
}
|
|
|
|
applied := current.AppliedResources()
|
|
newResources := new.Container.Resources
|
|
if applied.ValidateUpdateInPlace(newResources) != nil {
|
|
return ContainerNeedsRecreate
|
|
}
|
|
// Only CPU and memory limits can differ after the in-place update check above.
|
|
if !applied.Equals(newResources) {
|
|
return ContainerNeedsUpdate
|
|
}
|
|
return status
|
|
}
|
|
|
|
// ContainerUpdateOptions returns the container properties that can be updated in place and differ between the new
|
|
// spec and either the container spec or the limits Docker applies to the container. It should only be used when
|
|
// EvalContainerChange returns ContainerNeedsUpdate.
|
|
func ContainerUpdateOptions(current api.ServiceContainer, new api.ServiceSpec) api.UpdateContainerOptions {
|
|
var opts api.UpdateContainerOptions
|
|
|
|
stored, applied := current.ServiceSpec.Container.Resources, current.AppliedResources()
|
|
res := new.Container.Resources
|
|
if res.CPU != stored.CPU || res.CPU != applied.CPU {
|
|
opts.CPU = &res.CPU
|
|
}
|
|
if res.Memory != stored.Memory || res.Memory != applied.Memory {
|
|
opts.Memory = &res.Memory
|
|
}
|
|
if res.MemoryReservation != stored.MemoryReservation || res.MemoryReservation != applied.MemoryReservation {
|
|
opts.MemoryReservation = &res.MemoryReservation
|
|
}
|
|
|
|
if !cmp.Equal(current.ServiceSpec.Labels, new.Labels, cmpopts.EquateEmpty()) {
|
|
labels := maps.Clone(new.Labels)
|
|
if labels == nil {
|
|
labels = map[string]string{}
|
|
}
|
|
opts.ServiceLabels = &labels
|
|
}
|
|
|
|
return opts
|
|
}
|