envtest 0.1.2

A lightweight, type‑safe wrapper around the Kubernetes `envtest` Go package that lets you spin up a temporary control plane from Rust.
Documentation
package main

import (
	"fmt"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/client-go/tools/clientcmd/api/latest"
	"k8s.io/klog/v2"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/envtest"

	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

var (
	codec          runtime.Codec
	environments   Map[string, envtest.Environment]
	jsonSerializer *json.Serializer
)

type Envtest struct{}

type createErrorType uint8

const (
	createErrorTypeSetupBinaryAssetsDirectory createErrorType = iota
	createErrorTypeDecodeCRD
	createErrorTypeStartEnvironment
	createErrorTypeBuildKubeconfig
	createErrorTypeStopEnvironment
)

type destroyErrorType uint8

const (
	destroyErrorEnvMissing destroyErrorType = iota
	destroyErrorTypeStopEnvironment
)

func init() {
	EnvTestImpl = Envtest{}
}

func init() {
	ctrl.SetLogger(klog.Background())

	jsonSerializer = json.NewSerializerWithOptions(json.DefaultMetaFactory, latest.Scheme, latest.Scheme, json.SerializerOptions{})
	codec = versioning.NewDefaultingCodecForScheme(
		latest.Scheme,
		jsonSerializer,
		jsonSerializer,
		schema.GroupVersion{Version: latest.Version},
		runtime.InternalGroupVersioner,
	)
}

// FromEnvTestConfig returns a new Kubeconfig string form when running in envtest.
func FromEnvTestConfig(cfg *rest.Config) (string, error) {
	contextName := fmt.Sprintf("%s@envtest", cfg.Username)
	c := api.Config{
		Clusters: map[string]*api.Cluster{
			"envtest": {
				Server:                   cfg.Host,
				CertificateAuthorityData: cfg.CAData,
			},
		},
		Contexts: map[string]*api.Context{
			contextName: {
				Cluster:  "envtest",
				AuthInfo: cfg.Username,
			},
		},
		AuthInfos: map[string]*api.AuthInfo{
			cfg.Username: {
				ClientKeyData:         cfg.KeyData,
				ClientCertificateData: cfg.CertData,
			},
		},
		CurrentContext: contextName,
	}

	data, err := runtime.Encode(codec, &c)

	return string(data), err
}

// create implements EnvTest.
func (e Envtest) create(req *Environment) (resp CreateResponse) {
	storeErr := func(err error, errorType createErrorType) CreateResponse {
		resp.err = append(resp.err, err.Error())
		resp.error_type = append(resp.error_type, uint8(errorType))

		return resp
	}

	env := &envtest.Environment{
		DownloadBinaryAssets: req.binary_assets_settings.download_binary_assets,
		CRDInstallOptions: envtest.CRDInstallOptions{
			Paths:              req.crd_install_options.paths,
			ErrorIfPathMissing: req.crd_install_options.error_if_path_missing,
		},
	}

	if len(req.binary_assets_settings.binary_assets_directory) > 0 {
		env.BinaryAssetsDirectory = req.binary_assets_settings.binary_assets_directory[0]
	} else if binaryAssetsDirectory, err := envtest.SetupEnvtestDefaultBinaryAssetsDirectory(); err != nil {
		return storeErr(err, createErrorTypeSetupBinaryAssetsDirectory)
	} else {
		env.BinaryAssetsDirectory = binaryAssetsDirectory
	}

	if len(req.binary_assets_settings.download_binary_assets_version) > 0 {
		env.DownloadBinaryAssetsVersion = req.binary_assets_settings.download_binary_assets_version[0]
	}

	if len(req.binary_assets_settings.download_binary_assets_index_url) > 0 {
		env.DownloadBinaryAssetsIndexURL = req.binary_assets_settings.download_binary_assets_index_url[0]
	}

	destroy := func(env *envtest.Environment) {
		if err := env.Stop(); err != nil {
			storeErr(err, createErrorTypeStopEnvironment)
		}
	}

	gvk := apiextensionsv1.SchemeGroupVersion.WithKind("CustomResourceDefinition")
	for _, data := range req.crd_install_options.crds {
		crd := &apiextensionsv1.CustomResourceDefinition{}
		if _, _, err := jsonSerializer.Decode([]byte(data), &gvk, crd); err != nil {
			return storeErr(err, createErrorTypeDecodeCRD)
		}

		env.CRDs = append(env.CRDs, crd)
	}

	config, err := env.Start()
	if err != nil {
		return storeErr(err, createErrorTypeStartEnvironment)
	}

	kubeconfig, err := FromEnvTestConfig(config)
	if err != nil {
		defer destroy(env)

		return storeErr(err, createErrorTypeBuildKubeconfig)
	}

	environments.Store(kubeconfig, *env)

	resp.server = Server{kubeconfig: kubeconfig}

	return
}

// destroy implements EnvTest.
func (e Envtest) destroy(kubeconfig *string) (resp DestroyResponse) {
	if kubeconfig == nil || *kubeconfig == "" {
		return
	}

	storeErr := func(err error, errorType destroyErrorType) DestroyResponse {
		resp.err = append(resp.err, err.Error())
		resp.error_type = append(resp.error_type, uint8(errorType))

		return resp
	}

	env, ok := environments.Load(*kubeconfig)
	if !ok {
		return storeErr(fmt.Errorf("environement for the provided kubeconfig was already destroyed or does not exist"), destroyErrorEnvMissing)
	}

	err := env.Stop()
	if err != nil {
		return storeErr(err, destroyErrorTypeStopEnvironment)
	}

	return
}