github.com/etecs-ru/gnomock@v0.13.2/preset/k3s/preset.go (about)

     1  // Package k3s provides a Gnomock Preset for lightweight kubernetes (k3s). This
     2  // preset by no means should be used in any kind of deployment, and no other
     3  // presets are supposed to be deployed in it. The goal of this preset is to
     4  // allow easier testing of Kubernetes automation tools.
     5  //
     6  // This preset does not use a well-known docker image like most other presets
     7  // do. Instead, it uses a custom built and adapted image that runs lightweight
     8  // Kubernetes (k3s) in a docker container:
     9  // https://github.com/orlangure/k3s-dind.
    10  //
    11  // Please make sure to pick a version here:
    12  // https://hub.docker.com/repository/docker/orlangure/k3s. At some point k3s
    13  // version tags should start to appear. If only `latest` tag is available, it
    14  // comes with k3s v1.18.4.
    15  //
    16  // Keep in mind that k3s runs in a single docker container, meaning it might be
    17  // limited in memory, CPU and storage. Also remember that this cluster always
    18  // runs on a single node.
    19  //
    20  // To connect to this cluster, use `Config` function that can be used together
    21  // with Kubernetes client for Go, or `ConfigBytes` that can be saved as
    22  // `kubeconfig` file and used by `kubectl`.
    23  package k3s
    24  
    25  import (
    26  	"context"
    27  	"crypto/tls"
    28  	"fmt"
    29  	"io/ioutil"
    30  	"net/http"
    31  	"time"
    32  
    33  	"github.com/etecs-ru/gnomock"
    34  	"github.com/etecs-ru/gnomock/internal/registry"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/client-go/kubernetes"
    37  	"k8s.io/client-go/rest"
    38  	"k8s.io/client-go/tools/clientcmd"
    39  )
    40  
    41  const (
    42  	defaultPort     = 48443
    43  	defaultVersion  = "latest"
    44  	defaultHostname = "localhost"
    45  )
    46  
    47  // KubeconfigPort is a port that exposes a single `/kubeconfig` endpoint. It
    48  // can be used to retrieve a configured kubeconfig file to use to connect to
    49  // this container using kubectl.
    50  const (
    51  	KubeconfigPort = "kubeconfig"
    52  	kubeconfigPort = 80
    53  )
    54  
    55  func init() {
    56  	registry.Register("kubernetes", func() gnomock.Preset { return &P{} })
    57  }
    58  
    59  // Preset creates a new Gmomock k3s preset. This preset includes a
    60  // k3s specific healthcheck function and default k3s image and port. Please
    61  // note that this preset launches a privileged docker container.
    62  //
    63  // By default, this preset sets up k3s v1.19.3.
    64  func Preset(opts ...Option) gnomock.Preset {
    65  	p := &P{}
    66  
    67  	for _, opt := range opts {
    68  		opt(p)
    69  	}
    70  
    71  	return p
    72  }
    73  
    74  // P is a Gnomock Preset implementation of lightweight kubernetes (k3s).
    75  type P struct {
    76  	Version string `json:"version"`
    77  	Port    int
    78  }
    79  
    80  // Image returns an image that should be pulled to create this container.
    81  func (p *P) Image() string {
    82  	return fmt.Sprintf("docker.io/orlangure/k3s:%s", p.Version)
    83  }
    84  
    85  // Ports returns ports that should be used to access this container.
    86  func (p *P) Ports() gnomock.NamedPorts {
    87  	port := gnomock.TCP(p.Port)
    88  	port.HostPort = p.Port
    89  
    90  	return gnomock.NamedPorts{
    91  		gnomock.DefaultPort: port,
    92  		KubeconfigPort:      gnomock.TCP(kubeconfigPort),
    93  	}
    94  }
    95  
    96  // Options returns a list of options to configure this container.
    97  func (p *P) Options() []gnomock.Option {
    98  	p.setDefaults()
    99  
   100  	opts := []gnomock.Option{
   101  		gnomock.WithHealthCheck(p.healthcheck),
   102  		gnomock.WithPrivileged(),
   103  		gnomock.WithEnv(fmt.Sprintf("K3S_API_HOST=%s", defaultHostname)),
   104  		gnomock.WithEnv(fmt.Sprintf("K3S_API_PORT=%d", p.Port)),
   105  	}
   106  
   107  	return opts
   108  }
   109  
   110  func (p *P) healthcheck(ctx context.Context, c *gnomock.Container) (err error) {
   111  	kubeconfig, err := Config(c)
   112  	if err != nil {
   113  		return fmt.Errorf("failed to get kubeconfig: %w", err)
   114  	}
   115  
   116  	// this is valid only for health checks, and solves a problem where
   117  	// gnomockd performs these calls from within its own container by accessing
   118  	// the cluster at 172.0.0.1, which is not one of the addresses in the
   119  	// certificate
   120  	kubeconfig.Host = c.DefaultAddress()
   121  
   122  	client, err := kubernetes.NewForConfig(kubeconfig)
   123  	if err != nil {
   124  		return fmt.Errorf("failed to create kubernetes client from kubeconfig: %w", err)
   125  	}
   126  
   127  	nodes, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
   128  	if err != nil {
   129  		return fmt.Errorf("failed to list cluster nodes: %w", err)
   130  	}
   131  
   132  	if len(nodes.Items) == 0 {
   133  		return fmt.Errorf("no nodes found in cluster")
   134  	}
   135  
   136  	sas, err := client.CoreV1().ServiceAccounts(metav1.NamespaceDefault).List(ctx, metav1.ListOptions{})
   137  	if err != nil {
   138  		return fmt.Errorf("failed to list service accounts: %w", err)
   139  	}
   140  
   141  	if len(sas.Items) == 0 {
   142  		return fmt.Errorf("no service accounts found in cluster")
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (p *P) setDefaults() {
   149  	if p.Version == "" {
   150  		p.Version = defaultVersion
   151  	}
   152  
   153  	if p.Port == 0 {
   154  		p.Port = defaultPort
   155  	}
   156  }
   157  
   158  // ConfigBytes returns file contents of kubeconfig file that should be used to
   159  // connect to the cluster running in the provided container.
   160  func ConfigBytes(c *gnomock.Container) (configBytes []byte, err error) {
   161  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   162  	defer cancel()
   163  
   164  	client := &http.Client{
   165  		Transport: &http.Transport{
   166  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint:gosec // allow for tests
   167  		},
   168  	}
   169  
   170  	addr := fmt.Sprintf("http://%s/kubeconfig", c.Address(KubeconfigPort))
   171  
   172  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("kubeconfig unavailable: %w", err)
   175  	}
   176  
   177  	res, err := client.Do(req)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("kubeconfig unavailable: %w", err)
   180  	}
   181  
   182  	defer func() {
   183  		closeErr := res.Body.Close()
   184  		if err == nil && closeErr != nil {
   185  			err = closeErr
   186  		}
   187  	}()
   188  
   189  	if res.StatusCode != http.StatusOK {
   190  		return nil, fmt.Errorf("invalid kubeconfig response code '%d'", res.StatusCode)
   191  	}
   192  
   193  	configBytes, err = ioutil.ReadAll(res.Body)
   194  	if err != nil {
   195  		return nil, fmt.Errorf("can't read kubeconfig body: %w", err)
   196  	}
   197  
   198  	return configBytes, nil
   199  }
   200  
   201  // Config returns `*rest.Config` instance of Kubernetes client-go package. This
   202  // config can be used to create a new client that will work against k3s cluster
   203  // running in the provided container.
   204  func Config(c *gnomock.Container) (*rest.Config, error) {
   205  	configBytes, err := ConfigBytes(c)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("can't get kubeconfig bytes: %w", err)
   208  	}
   209  
   210  	kubeconfig, err := clientcmd.RESTConfigFromKubeConfig(configBytes)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("can't create kubeconfig from bytes: %w", err)
   213  	}
   214  
   215  	return kubeconfig, nil
   216  }