github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/registry.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"sync"
     8  
     9  	"k8s.io/apimachinery/pkg/api/errors"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	apiv1 "k8s.io/client-go/kubernetes/typed/core/v1"
    12  
    13  	"github.com/tilt-dev/clusterid"
    14  	"github.com/tilt-dev/localregistry-go"
    15  	"github.com/tilt-dev/tilt/internal/container"
    16  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    17  	"github.com/tilt-dev/tilt/pkg/logger"
    18  )
    19  
    20  // Recommended in Tilt-specific scripts
    21  const tiltAnnotationRegistry = "tilt.dev/registry"
    22  const tiltAnnotationRegistryFromCluster = "tilt.dev/registry-from-cluster"
    23  
    24  // Recommended in Kind's scripts
    25  // https://kind.sigs.k8s.io/docs/user/local-registry/
    26  // There's active work underway to standardize this.
    27  const kindAnnotationRegistry = "kind.x-k8s.io/registry"
    28  
    29  const microk8sRegistryNamespace = "container-registry"
    30  const microk8sRegistryName = "registry"
    31  
    32  type RuntimeSource interface {
    33  	Runtime(ctx context.Context) container.Runtime
    34  }
    35  
    36  type NaiveRuntimeSource struct {
    37  	runtime container.Runtime
    38  }
    39  
    40  func NewNaiveRuntimeSource(r container.Runtime) NaiveRuntimeSource {
    41  	return NaiveRuntimeSource{runtime: r}
    42  }
    43  
    44  func (s NaiveRuntimeSource) Runtime(ctx context.Context) container.Runtime {
    45  	return s.runtime
    46  }
    47  
    48  type registryAsync struct {
    49  	env           clusterid.Product
    50  	core          apiv1.CoreV1Interface
    51  	runtimeSource RuntimeSource
    52  	registry      *v1alpha1.RegistryHosting
    53  	once          sync.Once
    54  }
    55  
    56  func newRegistryAsync(env clusterid.Product, core apiv1.CoreV1Interface, runtimeSource RuntimeSource) *registryAsync {
    57  	return &registryAsync{
    58  		env:           env,
    59  		core:          core,
    60  		runtimeSource: runtimeSource,
    61  	}
    62  }
    63  
    64  func (r *registryAsync) inferRegistryFromMicrok8s(ctx context.Context) *v1alpha1.RegistryHosting {
    65  	// If Microk8s is using the docker runtime, we can just use the microk8s docker daemon
    66  	// instead of the registry.
    67  	runtime := r.runtimeSource.Runtime(ctx)
    68  	if runtime == container.RuntimeDocker {
    69  		return nil
    70  	}
    71  
    72  	// Microk8s might have a registry enabled.
    73  	// https://microk8s.io/docs/working
    74  	svc, err := r.core.Services(microk8sRegistryNamespace).Get(ctx, microk8sRegistryName, metav1.GetOptions{})
    75  	if err != nil {
    76  		if errors.IsNotFound(err) {
    77  			logger.Get(ctx).Warnf("You are running microk8s without a local image registry.\n" +
    78  				"Run: `sudo microk8s.enable registry`\n" +
    79  				"Tilt will use the local registry to speed up builds")
    80  		} else {
    81  			logger.Get(ctx).Debugf("Error fetching services: %v", err)
    82  		}
    83  		return nil
    84  	}
    85  
    86  	portSpecs := svc.Spec.Ports
    87  	if len(portSpecs) == 0 {
    88  		return nil
    89  	}
    90  
    91  	// Check to make sure localhost resolves to an IPv4 address. If it doesn't,
    92  	// then we won't be able to connect to the registry. See:
    93  	// https://github.com/tilt-dev/tilt/issues/2369
    94  	ips, err := net.LookupIP("localhost")
    95  	if err != nil || len(ips) == 0 || ips[0].To4() == nil {
    96  		logger.Get(ctx).Warnf("Your /etc/hosts is resolving localhost to ::1 (IPv6).\n" +
    97  			"This breaks the microk8s image registry.\n" +
    98  			"Please fix your /etc/hosts to default to IPv4. This will make image pushes much faster.")
    99  		return nil
   100  	}
   101  
   102  	portSpec := portSpecs[0]
   103  	host := fmt.Sprintf("localhost:%d", portSpec.NodePort)
   104  	reg := v1alpha1.RegistryHosting{Host: host}
   105  	if err := reg.Validate(ctx); err != nil {
   106  		logger.Get(ctx).Warnf("Error validating private registry host %q: %v", host, err.ToAggregate())
   107  		return nil
   108  	}
   109  
   110  	return &reg
   111  }
   112  
   113  // If this node has the Tilt registry annotations on it, then we can
   114  // infer it was set up with a Tilt script and thus has a local registry.
   115  func (r *registryAsync) inferRegistryFromNodeAnnotations(ctx context.Context) *v1alpha1.RegistryHosting {
   116  	nodeList, err := r.core.Nodes().List(ctx, metav1.ListOptions{Limit: 1})
   117  	if err != nil || len(nodeList.Items) == 0 {
   118  		return nil
   119  	}
   120  
   121  	node := nodeList.Items[0]
   122  	annotations := node.Annotations
   123  
   124  	var reg *v1alpha1.RegistryHosting
   125  	if tiltReg := annotations[tiltAnnotationRegistry]; tiltReg != "" {
   126  		reg = &v1alpha1.RegistryHosting{
   127  			Host:                     annotations[tiltAnnotationRegistry],
   128  			HostFromContainerRuntime: annotations[tiltAnnotationRegistryFromCluster],
   129  		}
   130  	} else if kindReg := annotations[kindAnnotationRegistry]; kindReg != "" {
   131  		reg = &v1alpha1.RegistryHosting{Host: kindReg}
   132  	}
   133  
   134  	if reg != nil {
   135  		if err := reg.Validate(ctx); err != nil {
   136  			logger.Get(ctx).Warnf("Local registry read from node failed: %v", err.ToAggregate())
   137  			return nil
   138  		}
   139  	}
   140  
   141  	return reg
   142  }
   143  
   144  // Implements the local registry discovery standard.
   145  func (r *registryAsync) inferRegistryFromConfigMap(ctx context.Context) (registry *v1alpha1.RegistryHosting, help string) {
   146  	hosting, err := localregistry.Discover(ctx, r.core)
   147  	if err != nil {
   148  		logger.Get(ctx).Debugf("Local registry discovery error: %v", err)
   149  		return nil, ""
   150  	}
   151  
   152  	if hosting.Host == "" {
   153  		return nil, hosting.Help
   154  	}
   155  
   156  	registry = &v1alpha1.RegistryHosting{
   157  		Host:                     hosting.Host,
   158  		HostFromClusterNetwork:   hosting.HostFromClusterNetwork,
   159  		HostFromContainerRuntime: hosting.HostFromContainerRuntime,
   160  		Help:                     hosting.Help,
   161  	}
   162  
   163  	if err := registry.Validate(ctx); err != nil {
   164  		logger.Get(ctx).Debugf("Local registry discovery error: %v", err.ToAggregate())
   165  		return nil, hosting.Help
   166  	}
   167  	return registry, hosting.Help
   168  }
   169  
   170  func (r *registryAsync) Registry(ctx context.Context) *v1alpha1.RegistryHosting {
   171  	r.once.Do(func() {
   172  		reg, help := r.inferRegistryFromConfigMap(ctx)
   173  		if !container.IsEmptyRegistry(reg) {
   174  			r.registry = reg
   175  			return
   176  		}
   177  
   178  		// Auto-infer the microk8s local registry.
   179  		if r.env == clusterid.ProductMicroK8s {
   180  			reg := r.inferRegistryFromMicrok8s(ctx)
   181  			if !container.IsEmptyRegistry(reg) {
   182  				r.registry = reg
   183  				return
   184  			}
   185  		}
   186  
   187  		reg = r.inferRegistryFromNodeAnnotations(ctx)
   188  		if !container.IsEmptyRegistry(reg) {
   189  			r.registry = reg
   190  		}
   191  
   192  		if container.IsEmptyRegistry(r.registry) {
   193  			if help != "" {
   194  				logger.Get(ctx).Warnf("You are running without a local image registry.\n"+
   195  					"Tilt can use the local registry to speed up builds.\n"+
   196  					"Instructions: %s", help)
   197  			} else if r.env == clusterid.ProductKIND {
   198  				logger.Get(ctx).Warnf("You are running Kind without a local image registry.\n" +
   199  					"Tilt can use the local registry to speed up builds.\n" +
   200  					"Instructions: https://github.com/tilt-dev/kind-local")
   201  			}
   202  		}
   203  	})
   204  	return r.registry
   205  }
   206  
   207  func (c K8sClient) LocalRegistry(ctx context.Context) *v1alpha1.RegistryHosting {
   208  	return c.registryAsync.Registry(ctx)
   209  }