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  }