github.com/vshn/k8ify@v1.1.2-0.20240502214202-6c9ed3ef0bf4/pkg/ir/ir.go (about)

     1  package ir
     2  
     3  import (
     4  	composeTypes "github.com/compose-spec/compose-go/types"
     5  	"github.com/vshn/k8ify/pkg/util"
     6  	"k8s.io/apimachinery/pkg/api/resource"
     7  	"strconv"
     8  	"strings"
     9  )
    10  
    11  type Inputs struct {
    12  	Services  map[string]*Service
    13  	Volumes   map[string]*Volume
    14  	TargetCfg TargetCfg
    15  }
    16  
    17  func NewInputs() *Inputs {
    18  	return &Inputs{
    19  		Services: make(map[string]*Service),
    20  		Volumes:  make(map[string]*Volume),
    21  	}
    22  }
    23  
    24  func FromCompose(project *composeTypes.Project) *Inputs {
    25  	inputs := NewInputs()
    26  
    27  	// first find out all the regular ("parent") services
    28  	for _, composeService := range project.Services {
    29  		if util.PartOf(composeService.Labels) != nil {
    30  			continue
    31  		}
    32  		// `project.Services` is a list, so we use the name as reported by the
    33  		// service
    34  		inputs.Services[composeService.Name] = NewService(composeService.Name, composeService)
    35  	}
    36  
    37  	// then find all the parts that belong to a parent service and attach them to their parents
    38  	for _, composeService := range project.Services {
    39  		partOf := util.PartOf(composeService.Labels)
    40  		if partOf == nil {
    41  			continue
    42  		}
    43  		parent, ok := inputs.Services[*partOf]
    44  		if ok {
    45  			service := NewService(composeService.Name, composeService)
    46  			parent.AddPart(service)
    47  		}
    48  	}
    49  
    50  	for name, composeVolume := range project.Volumes {
    51  		// `project.CollectVolumes` is a map where the key is the volume name, while
    52  		// `volume.Name` is something else (the name prefixed with `_`???). So
    53  		// we use the key as the name.
    54  		inputs.Volumes[name] = NewVolume(name, composeVolume)
    55  	}
    56  
    57  	if targetCfg, ok := project.Extensions["x-targetCfg"]; ok {
    58  		if targetCfgMap, ok := targetCfg.(map[string]interface{}); ok {
    59  			inputs.TargetCfg = targetCfgMap
    60  		}
    61  	}
    62  
    63  	return inputs
    64  }
    65  
    66  // Service provides some k8ify-specific abstractions & utility around Compose
    67  // service configurations.
    68  type Service struct {
    69  	Name string
    70  
    71  	raw composeTypes.ServiceConfig
    72  
    73  	parts []*Service
    74  }
    75  
    76  func NewService(name string, composeService composeTypes.ServiceConfig) *Service {
    77  	return &Service{Name: name, raw: composeService, parts: make([]*Service, 0)}
    78  }
    79  
    80  // AsCompose returns the underlying compose config
    81  // TODO(mh): make me obsolete!
    82  func (s *Service) AsCompose() composeTypes.ServiceConfig {
    83  	return s.raw
    84  }
    85  
    86  func (s *Service) AddPart(part *Service) {
    87  	s.parts = append(s.parts, part)
    88  }
    89  
    90  func (s *Service) GetParts() []*Service {
    91  	return s.parts
    92  }
    93  
    94  // VolumeNames lists the names of all volumes that are mounted by this service
    95  func (s *Service) VolumeNames() []string {
    96  	names := []string{}
    97  
    98  	for _, mount := range s.raw.Volumes {
    99  		if mount.Type != "volume" {
   100  			// We don't support anything else (yet)
   101  			continue
   102  		}
   103  
   104  		names = append(names, mount.Source)
   105  	}
   106  
   107  	return names
   108  }
   109  
   110  func (s *Service) Volumes(volumes map[string]*Volume) (map[string]*Volume, map[string]*Volume) {
   111  	rwoVolumes := make(map[string]*Volume)
   112  	rwxVolumes := make(map[string]*Volume)
   113  	for _, volumeName := range s.VolumeNames() {
   114  		volume := volumes[volumeName]
   115  		if volume.IsShared() {
   116  			rwxVolumes[volume.Name] = volume
   117  		} else {
   118  			rwoVolumes[volume.Name] = volume
   119  		}
   120  	}
   121  	return rwoVolumes, rwxVolumes
   122  }
   123  
   124  func (s *Service) IsSingleton() bool {
   125  	return util.IsSingleton(s.raw.Labels)
   126  }
   127  func (s *Service) Labels() map[string]string {
   128  	return s.raw.Labels
   129  }
   130  
   131  type PublishedPort struct {
   132  	ServicePort   uint16
   133  	ContainerPort uint16
   134  }
   135  
   136  func (s *Service) GetPorts() []PublishedPort {
   137  	var publishedPorts []PublishedPort
   138  	for _, port := range s.raw.Ports {
   139  		publishedPort := PublishedPort{
   140  			ServicePort:   uint16(port.Target), // fall-back
   141  			ContainerPort: uint16(port.Target),
   142  		}
   143  		// port.Published can contain a range. Since we can't use this range for k8s we always use the start of the range instead.
   144  		portRange := strings.Split(port.Published, "-")
   145  		if len(portRange) > 0 {
   146  			p, err := strconv.ParseUint(portRange[0], 10, 16)
   147  			if err == nil {
   148  				publishedPort.ServicePort = uint16(p)
   149  			}
   150  		}
   151  		publishedPorts = append(publishedPorts, publishedPort)
   152  	}
   153  	return publishedPorts
   154  }
   155  
   156  // Volume provides some k8ify-specific abstractions & utility around Compose
   157  // volume configurations.
   158  type Volume struct {
   159  	Name string
   160  
   161  	raw composeTypes.VolumeConfig
   162  }
   163  
   164  func NewVolume(name string, composeVolume composeTypes.VolumeConfig) *Volume {
   165  	return &Volume{
   166  		Name: name,
   167  		raw:  composeVolume,
   168  	}
   169  }
   170  
   171  func (v *Volume) IsShared() bool {
   172  	return util.IsShared(v.raw.Labels)
   173  }
   174  func (v *Volume) IsSingleton() bool {
   175  	return util.IsSingleton(v.raw.Labels)
   176  }
   177  func (v *Volume) Labels() map[string]string {
   178  	return v.raw.Labels
   179  }
   180  
   181  func (v *Volume) Size(fallback string) resource.Quantity {
   182  	return util.StorageSize(v.raw.Labels, fallback)
   183  }
   184  func (v *Volume) SizeIsMissing() bool {
   185  	return util.StorageSizeRaw(v.raw.Labels) == nil
   186  }
   187  
   188  type TargetCfg map[string]interface{}
   189  
   190  func (t TargetCfg) appsDomain() *string {
   191  	if value, ok := t["appsDomain"]; ok {
   192  		if domain, ok := value.(string); ok {
   193  			if strings.HasPrefix(domain, "*.") {
   194  				domain = domain[1:]
   195  			}
   196  			if !strings.HasPrefix(domain, ".") {
   197  				domain = "." + domain
   198  			}
   199  			if len(domain) < 2 {
   200  				return nil
   201  			}
   202  			return &domain
   203  		}
   204  	}
   205  	return nil
   206  }
   207  
   208  func (t TargetCfg) IsSubdomainOfAppsDomain(domain string) bool {
   209  	appsDomain := t.appsDomain()
   210  	if appsDomain == nil || domain == "" {
   211  		return false
   212  	}
   213  	domainComponents := strings.Split(domain, ".")
   214  	if len(domainComponents) < 2 {
   215  		return false
   216  	}
   217  	return domainComponents[0]+*appsDomain == domain
   218  }
   219  
   220  func (t TargetCfg) MaxExposeLength() int {
   221  	if value, ok := t["maxExposeLength"]; ok {
   222  		if length, ok := value.(int); ok {
   223  			return length
   224  		}
   225  	}
   226  	return 63
   227  }