hack/deployer/runner/kind.go (233 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package runner
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"text/template"
"time"
"github.com/blang/semver/v4"
"github.com/elastic/cloud-on-k8s/v3/hack/deployer/exec"
"github.com/elastic/cloud-on-k8s/v3/hack/deployer/runner/env"
"github.com/elastic/cloud-on-k8s/v3/hack/deployer/runner/kyverno"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/vault"
)
const (
KindDriverID = "kind"
DefaultKindRunConfigTemplate = `id: kind-dev
overrides:
clusterName: %s-dev-cluster
`
// manifest is a kind cluster template
// the explicit podSubnet definition can be removed as soon as https://github.com/kubernetes-sigs/kind/commit/60074a9e67ddc8d35d3468ab137358b62a4cf723
// will be available in a released version of kind and we don't rely on older versions anymore
manifest = `---
kind: Cluster
apiVersion: {{.APIVersion}}
networking:
ipFamily: {{.IPFamily}}
{{- if eq .IPFamily "ipv6" }}
podSubnet: "fd00:10:244::/56"
{{- end}}
nodes:
- role: control-plane
{{- range .WorkerNames }}
- role: worker
{{- end}}
`
storageClassFileName = "storageclass.yaml"
storageClass = `apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
storageclass.beta.kubernetes.io/is-default-class: "true"
name: e2e-default
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
`
)
func init() {
drivers[KindDriverID] = &KindDriverFactory{}
}
type KindDriverFactory struct{}
var _ DriverFactory = &KindDriverFactory{}
func (k KindDriverFactory) Create(plan Plan) (Driver, error) {
c, err := vault.NewClient()
if err != nil {
return nil, err
}
return &KindDriver{
plan: plan,
vaultClient: c,
}, nil
}
type KindDriver struct {
plan Plan
clientImage string
vaultClient vault.Client
}
func (k *KindDriver) Execute() error {
if err := k.ensureClientImage(); err != nil {
return err
}
switch k.plan.Operation {
case CreateAction:
return k.create()
case DeleteAction:
return k.delete()
}
return nil
}
func (k *KindDriver) create() error {
// Write manifest to temporary file
tmpManifest, err := k.createTmpManifest()
if err != nil {
return err
}
defer os.Remove(tmpManifest.Name())
// Delete any previous e2e kind cluster with the same name
err = k.delete()
if err != nil {
return err
}
err = k.cmd("create", "cluster", "--config", k.inContainerName(tmpManifest), "--retain", "--image", k.plan.Kind.NodeImage).Run()
if err != nil {
return err
}
// Get kubeconfig from kind
kubeCfg, err := k.getKubeConfig()
if err != nil {
return err
}
defer os.Remove(kubeCfg.Name())
// Delete standard storage class but ignore error if not found
if err := kubectl("--kubeconfig", kubeCfg.Name(), "delete", "storageclass", "standard"); err != nil {
return err
}
tmpStorageClass, err := k.createTmpStorageClass()
if err != nil {
return err
}
if k.plan.EnforceSecurityPolicies {
if err := kyverno.Install("--kubeconfig", kubeCfg.Name()); err != nil {
return err
}
}
return kubectl("--kubeconfig", kubeCfg.Name(), "apply", "-f", tmpStorageClass)
}
func (k *KindDriver) inContainerName(file *os.File) string {
return filepath.Join("/home", filepath.Base(file.Name()))
}
func kubectl(arg ...string) error {
output, err := exec.NewCommand(`kubectl {{Join .Args " "}}`).AsTemplate(map[string]interface{}{"Args": arg}).Output()
if err != nil && strings.Contains(output, "Error from server (NotFound)") {
log.Printf("Ignoring NotFound error for command: %v\n", arg)
return nil // ignore not found errors
}
return err
}
func (k *KindDriver) delete() error {
return k.cmd("delete", "cluster").Run()
}
func (k *KindDriver) createTmpManifest() (*os.File, error) {
// HOME is shared between CI container and Kind container
f, err := os.CreateTemp(os.Getenv("HOME"), "kind-cluster")
if err != nil {
return nil, err
}
tmpl, err := template.New("cluster.yaml").Parse(manifest)
if err != nil {
return nil, err
}
type tplArgs struct {
APIVersion string
IPFamily string
WorkerNames []string
}
ta := tplArgs{
APIVersion: k.apiVersion(),
IPFamily: k.plan.Kind.IPFamily,
WorkerNames: k.workerNames(),
}
return f, tmpl.Execute(f, ta)
}
func (k *KindDriver) workerNames() []string {
var names []string
// kind has the following naming scheme <cluster-name>-worker, <cluster-name>-worker2 etc
// this is not configurable and thus explains the awkward name construction here
for i := 0; i < k.plan.Kind.NodeCount; i++ {
suffix := ""
if i > 0 {
suffix = strconv.Itoa(i + 1)
}
names = append(names, fmt.Sprintf("%s-worker%s", k.plan.ClusterName, suffix))
}
return names
}
func (k *KindDriver) cmd(args ...string) *exec.Command {
params := map[string]interface{}{
"SharedVolume": env.SharedVolumeName(),
"KindClientImage": k.clientImage,
"ClusterName": k.plan.ClusterName,
"Args": args,
}
// on macOS, the docker socket is located in $HOME
dockerSocket := "/var/run/docker.sock"
if runtime.GOOS == "darwin" {
dockerSocket = "$HOME/.docker/run/docker.sock"
}
// We need the docker socket so that kind can bootstrap
// --userns=host to support Docker daemon host configured to run containers only in user namespaces
cmd := exec.NewCommand(`docker run --rm \
--userns=host \
-v {{.SharedVolume}}:/home \
-v /var/run/docker.sock:` + dockerSocket + ` \
-e HOME=/home \
-e PATH=/ \
{{.KindClientImage}} \
/kind {{Join .Args " "}} --name {{.ClusterName}}`)
return cmd.AsTemplate(params)
}
func (k *KindDriver) apiVersion() string {
apiVersion := "kind.sigs.k8s.io/v1alpha3"
v := semver.MustParse(k.plan.ClientVersion)
if v.GTE(semver.MustParse("0.9.0")) {
apiVersion = "kind.x-k8s.io/v1alpha4"
}
return apiVersion
}
func (k *KindDriver) getKubeConfig() (*os.File, error) {
// Get kubeconfig from kind
output, err := k.cmd("get", "kubeconfig").WithoutStreaming().Output()
if err != nil {
return nil, err
}
// Persist kubeconfig for reliability in following kubectl commands
kubeCfg, err := os.CreateTemp("", "kubeconfig")
if err != nil {
return nil, err
}
_, err = kubeCfg.Write([]byte(output))
if err != nil {
return nil, err
}
return kubeCfg, nil
}
func (k *KindDriver) GetCredentials() error {
if err := k.ensureClientImage(); err != nil {
return err
}
config, err := k.getKubeConfig()
if err != nil {
return err
}
defer os.Remove(config.Name())
return mergeKubeconfig(config.Name())
}
func (k *KindDriver) createTmpStorageClass() (string, error) {
tmpFile := filepath.Join(os.Getenv("HOME"), storageClassFileName)
err := os.WriteFile(tmpFile, []byte(storageClass), fs.ModePerm)
return tmpFile, err
}
func (k *KindDriver) ensureClientImage() error {
image, err := ensureClientImage(KindDriverID, k.vaultClient, k.plan.ClientVersion, k.plan.ClientBuildDefDir)
if err != nil {
return err
}
k.clientImage = image
return nil
}
func (k *KindDriver) Cleanup(string, time.Duration) error {
return fmt.Errorf("unimplemented")
}
var _ Driver = &KindDriver{}