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 }