github.com/docker/compose-on-kubernetes@v0.5.0/internal/convert/stack.go (about) 1 package convert 2 3 import ( 4 "errors" 5 "sort" 6 "strconv" 7 8 "github.com/docker/compose-on-kubernetes/api/compose/latest" 9 "github.com/docker/compose-on-kubernetes/api/labels" 10 "github.com/docker/compose-on-kubernetes/internal/stackresources" 11 log "github.com/sirupsen/logrus" 12 apiequality "k8s.io/apimachinery/pkg/api/equality" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 ) 15 16 const ( 17 expectedGenerationAnnotation = "com.docker.stack.expected-generation" 18 ) 19 20 // IsStackDirty indicates if the stack is pending reconciliation 21 func IsStackDirty(stack *latest.Stack) bool { 22 if stack.Status == nil { 23 return true 24 } 25 return stack.Status.Phase == latest.StackReconciliationPending || 26 stack.Status.Phase == latest.StackFailure 27 } 28 29 // StackToStack converts a latest.Stack to a StackDefinition 30 func StackToStack(stack latest.Stack, strategy ServiceStrategy, original *stackresources.StackState) (*stackresources.StackState, error) { 31 if stack.Spec == nil { 32 return nil, errors.New("stack spec is nil") 33 } 34 composeServices := stack.Spec.Services 35 // in future we might support stacks with no compose service but only helm or such deployments 36 // then we'll need to update this code 37 if len(composeServices) == 0 { 38 return nil, errors.New("this stack has no service") 39 } 40 stackDirty := IsStackDirty(&stack) 41 42 log.Debugf("Stack dirtyness check: %v\nStack object: %#v", stackDirty, stack) 43 44 sort.Slice(composeServices, func(i int, j int) bool { return composeServices[i].Name < composeServices[j].Name }) 45 46 var resources []interface{} 47 for _, srv := range composeServices { 48 svcResources, err := toStackResources(stack.Name, stack.Namespace, srv, stack.Spec, strategy, original, stackDirty) 49 if err != nil { 50 return nil, err 51 } 52 53 resources = append(resources, svcResources...) 54 } 55 56 return stackresources.NewStackState(resources...) 57 } 58 59 // toStackService creates a Kubernetes stack service out of a swarm service. 60 func toStackResources(stackName, stackNamespace string, srv latest.ServiceConfig, configuration *latest.StackSpec, 61 strategy ServiceStrategy, original *stackresources.StackState, stackDirty bool) ([]interface{}, error) { 62 labelSelector := labels.ForService(stackName, srv.Name) 63 objectMeta := objectMeta(srv, labelSelector, stackNamespace) 64 65 var resources []interface{} 66 headlessService, publishedService, randomPortsService := toServices(srv, objectMeta, labelSelector, strategy, original) 67 if headlessService != nil { 68 resources = append(resources, headlessService) 69 } 70 if publishedService != nil { 71 resources = append(resources, publishedService) 72 } 73 if randomPortsService != nil { 74 resources = append(resources, randomPortsService) 75 } 76 objKey := stackresources.ObjKey(objectMeta.Namespace, objectMeta.Name) 77 78 if isGlobal(srv) { 79 if hasPersistentVolumes(srv) { 80 return nil, errors.New("using persistent volumes in a global service is not supported yet") 81 } 82 originalSvc := original.Daemonsets[objKey] 83 if !stackDirty && generationMatchesExpected(originalSvc.ObjectMeta) { 84 log.Debugf("Generation match for daemonset %s, skipping", objKey) 85 resources = append(resources, &originalSvc) 86 } else { 87 podTemplate, err := toPodTemplate(srv, objectMeta.Labels, configuration, originalSvc.Spec.Template) 88 if err != nil { 89 return nil, err 90 } 91 res := toDaemonSet(objectMeta, podTemplate, labelSelector, originalSvc) 92 setExpectedGeneration(originalSvc.ObjectMeta, &res.ObjectMeta, newSpecPair(&originalSvc.Spec, &res.Spec)) 93 resources = append(resources, res) 94 } 95 } else if hasPersistentVolumes(srv) { 96 originalSvc := original.Statefulsets[objKey] 97 if !stackDirty && generationMatchesExpected(originalSvc.ObjectMeta) { 98 log.Debugf("Generation match for statefulset %s, skipping", objKey) 99 resources = append(resources, &originalSvc) 100 } else { 101 podTemplate, err := toPodTemplate(srv, objectMeta.Labels, configuration, originalSvc.Spec.Template) 102 if err != nil { 103 return nil, err 104 } 105 res := toStatefulSet(srv, objectMeta, podTemplate, labelSelector, originalSvc) 106 setExpectedGeneration(originalSvc.ObjectMeta, &res.ObjectMeta, newSpecPair(&originalSvc.Spec, &res.Spec)) 107 resources = append(resources, res) 108 } 109 } else { 110 originalSvc := original.Deployments[objKey] 111 if !stackDirty && generationMatchesExpected(originalSvc.ObjectMeta) { 112 log.Debugf("Generation match for deployment %s, skipping", objKey) 113 resources = append(resources, &originalSvc) 114 } else { 115 podTemplate, err := toPodTemplate(srv, objectMeta.Labels, configuration, originalSvc.Spec.Template) 116 if err != nil { 117 return nil, err 118 } 119 res := toDeployment(srv, objectMeta, podTemplate, labelSelector, originalSvc) 120 // the deployment api also increment expectedGeneration on annotations changes 121 // first we compare specs, and then we check that this could result in a modified annotations map 122 setExpectedGeneration(originalSvc.ObjectMeta, &res.ObjectMeta, newSpecPair(&originalSvc.Spec, &res.Spec)) 123 setExpectedGeneration(originalSvc.ObjectMeta, &res.ObjectMeta, newSpecPair(originalSvc.Annotations, res.Annotations)) 124 resources = append(resources, res) 125 } 126 } 127 128 return resources, nil 129 } 130 131 func objectMeta(srv latest.ServiceConfig, labels map[string]string, namespace string) metav1.ObjectMeta { 132 return metav1.ObjectMeta{ 133 Name: srv.Name, 134 Labels: mergeLabels(labels, srv.Deploy.Labels), 135 Namespace: namespace, 136 } 137 } 138 139 func mergeLabels(labelmaps ...map[string]string) map[string]string { 140 m := map[string]string{} 141 for _, l := range labelmaps { 142 for key, value := range l { 143 m[key] = value 144 } 145 } 146 return m 147 } 148 149 func generationMatchesExpected(meta metav1.ObjectMeta) bool { 150 if meta.Generation < 1 { 151 return false 152 } 153 if meta.Annotations == nil { 154 return false 155 } 156 expected, ok := meta.Annotations[expectedGenerationAnnotation] 157 if !ok { 158 return false 159 } 160 expectedValue, err := strconv.ParseInt(expected, 10, 64) 161 if err != nil { 162 return false 163 } 164 return expectedValue == meta.Generation 165 } 166 167 type specPair struct { 168 original, desired interface{} 169 } 170 171 func newSpecPair(original, desired interface{}) specPair { 172 return specPair{ 173 original: original, 174 desired: desired, 175 } 176 } 177 178 func setExpectedGeneration(originalMeta metav1.ObjectMeta, meta *metav1.ObjectMeta, specsToCompare specPair) { 179 if meta.Annotations == nil { 180 meta.Annotations = make(map[string]string) 181 } 182 183 if !apiequality.Semantic.DeepEqual(specsToCompare.original, specsToCompare.desired) { 184 meta.Annotations[expectedGenerationAnnotation] = strconv.FormatInt(originalMeta.Generation+1, 10) 185 } else { 186 meta.Annotations[expectedGenerationAnnotation] = strconv.FormatInt(originalMeta.Generation, 10) 187 } 188 }