Repository: syself/cluster-api-provider-hetzner
Pull request #604 (2023-02-25 22:08:34)

:seedling: fixing unit test
**What this PR does / why we need it**:
Fixes unit test

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._

**TODOs**:

- [x] squashed commits
- [ ] includes documentation
- [ ] adds unit tests




Patch:
diff --git a/.gitignore b/.gitignore
index 578d6233..5d693731 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,6 @@ test/e2e/data/infrastructure-hetzner/v1beta1/env*
 *.out
 
 # Kubernetes Generated files - skip generated files, except for vendored files
-
 !vendor/**/zz_generated.*
 
 # editor and IDE paraphernalia
@@ -38,9 +37,9 @@ tilt_config.json
 baremetalhosts.yaml
 
 # kubeconfigs
-kind.kubeconfig
-kubeconfig
-.kubeconfig
+./kind.kubeconfig
+./kubeconfig
+./.kubeconfig
 
 # Common editor / temporary files
 *~
diff --git a/Makefile b/Makefile
index 1741e421..32f1c0cf 100644
--- a/Makefile
+++ b/Makefile
@@ -91,12 +91,12 @@ export KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT ?= 60s
 CONTROLLER_GEN := $(abspath $(TOOLS_BIN_DIR)/controller-gen)
 controller-gen: $(CONTROLLER_GEN) ## Build a local copy of controller-gen
 $(CONTROLLER_GEN): $(TOOLS_DIR)/go.mod # Build controller-gen from tools folder.
-	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen
+	cd $(TOOLS_DIR); go build -mod=vendor -tags=tools -o $(BIN_DIR)/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen
 
 KUSTOMIZE := $(abspath $(TOOLS_BIN_DIR)/kustomize)
 kustomize: $(KUSTOMIZE) ## Build a local copy of kustomize
 $(KUSTOMIZE): # Build kustomize from tools folder.
-	cd $(TOOLS_DIR) && go build -tags=tools -o $(KUSTOMIZE) sigs.k8s.io/kustomize/kustomize/v4
+	cd $(TOOLS_DIR) && go build -mod=vendor -tags=tools -o $(KUSTOMIZE) sigs.k8s.io/kustomize/kustomize/v4
 
 GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/golangci-lint)
 golangci-lint: $(GOLANGCI_LINT) ## Build a local copy of golangci-lint. After running this command do: BUILD_IN_CONTAINER=false make lint
@@ -114,27 +114,27 @@ $(TILT):
 ENVSUBST := $(abspath $(TOOLS_BIN_DIR)/envsubst)
 envsubst: $(ENVSUBST) ## Build a local copy of envsubst
 $(ENVSUBST): $(TOOLS_DIR)/go.mod # Build envsubst from tools folder.
-	cd $(TOOLS_DIR) && go build -tags=tools -o $(ENVSUBST) github.com/drone/envsubst/v2/cmd/envsubst
+	cd $(TOOLS_DIR) && go build -mod=vendor -tags=tools -o $(ENVSUBST) github.com/drone/envsubst/v2/cmd/envsubst
 
 SETUP_ENVTEST := $(abspath $(TOOLS_BIN_DIR)/setup-envtest)
 setup-envtest: $(SETUP_ENVTEST) ## Build a local copy of setup-envtest
 $(SETUP_ENVTEST): $(TOOLS_DIR)/go.mod # Build setup-envtest from tools folder.
-	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/setup-envtest sigs.k8s.io/controller-runtime/tools/setup-envtest
+	cd $(TOOLS_DIR); go build -mod=vendor -tags=tools -o $(BIN_DIR)/setup-envtest sigs.k8s.io/controller-runtime/tools/setup-envtest
 
 CTLPTL := $(abspath $(TOOLS_BIN_DIR)/ctlptl)
 ctlptl: $(CTLPTL) ## Build a local copy of ctlptl
 $(CTLPTL): 
-	cd $(TOOLS_DIR) && go build -tags=tools -o $(CTLPTL) github.com/tilt-dev/ctlptl/cmd/ctlptl
+	cd $(TOOLS_DIR) && go build -mod=vendor -tags=tools -o $(CTLPTL) github.com/tilt-dev/ctlptl/cmd/ctlptl
 
 CLUSTERCTL := $(abspath $(TOOLS_BIN_DIR)/clusterctl)
 clusterctl: $(CLUSTERCTL) ## Build a local copy of clusterctl
 $(CLUSTERCTL): $(TOOLS_DIR)/go.mod 
-	cd $(TOOLS_DIR) && go build -tags=tools -o $(CLUSTERCTL) sigs.k8s.io/cluster-api/cmd/clusterctl
+	cd $(TOOLS_DIR) && go build -mod=vendor -tags=tools -o $(CLUSTERCTL) sigs.k8s.io/cluster-api/cmd/clusterctl
 
 KIND := $(abspath $(TOOLS_BIN_DIR)/kind)
 kind: $(KIND) ## Build a local copy of kind
 $(KIND): $(TOOLS_DIR)/go.mod
-	cd $(TOOLS_DIR) && go build -tags=tools -o $(KIND) sigs.k8s.io/kind
+	cd $(TOOLS_DIR) && go build -mod=vendor -tags=tools -o $(KIND) sigs.k8s.io/kind
 
 go-binsize-treemap := $(abspath $(TOOLS_BIN_DIR)/go-binsize-treemap)
 go-binsize-treemap: $(go-binsize-treemap) # Build go-binsize-treemap from tools folder.
@@ -427,7 +427,7 @@ ARTIFACTS ?= _artifacts
 $(ARTIFACTS):
 	mkdir -p $(ARTIFACTS)/
 
-KUBEBUILDER_ASSETS ?= $(shell $(SETUP_ENVTEST) use --use-env -p path $(KUBEBUILDER_ENVTEST_KUBERNETES_VERSION))
+KUBEBUILDER_ASSETS ?= $(shell $(SETUP_ENVTEST) use --use-env --bin-dir $(abspath $(TOOLS_BIN_DIR)) -p path $(KUBEBUILDER_ENVTEST_KUBERNETES_VERSION))
 
 E2E_DIR ?= $(ROOT_DIR)/test/e2e
 E2E_CONF_FILE_SOURCE ?= $(E2E_DIR)/config/hetzner.yaml
@@ -436,7 +436,7 @@ E2E_CONF_FILE ?= $(E2E_DIR)/config/hetzner-ci-envsubst.yaml
 .PHONY: test-unit
 test-unit: $(SETUP_ENVTEST) $(GOTESTSUM) ## Run unit and integration tests
 	@mkdir -p $(shell pwd)/.coverage
-	KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -covermode=atomic -coverprofile=.coverage/cover.out -p=4 ./controllers/... ./pkg/...
+	KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -covermode=atomic -mod=vendor -coverprofile=.coverage/cover.out -p=4 ./controllers/... ./pkg/...
 
 .PHONY: e2e-image
 e2e-image: ## Build the e2e manager image
diff --git a/hack/golang-modules-update.sh b/hack/golang-modules-update.sh
index 5f20b899..5bb3506c 100755
--- a/hack/golang-modules-update.sh
+++ b/hack/golang-modules-update.sh
@@ -21,7 +21,7 @@ set -o pipefail
 REPO_ROOT=$(realpath $(dirname "${BASH_SOURCE[0]}")/..)
 cd "${REPO_ROOT}" || exit 1
 
-DIRS="./ ./hack/tools"
+DIRS="./ ./hack/tools ./test"
 for DIR in ${DIRS}; do
 	cd ${REPO_ROOT}/${DIR} && GO111MODULE=on GOPROXY=https://proxy.golang.org go mod download
 	cd ${REPO_ROOT}/${DIR} && GO111MODULE=on GOPROXY=https://proxy.golang.org go mod verify
diff --git a/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/kubeconfig.go b/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/kubeconfig.go
new file mode 100644
index 00000000..2c8872bb
--- /dev/null
+++ b/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/kubeconfig.go
@@ -0,0 +1,245 @@
+/*
+Copyright 2019 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package kubeconfig implements utilities for working with kubeconfigs.
+package kubeconfig
+
+import (
+	"context"
+	"crypto"
+	"crypto/x509"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/cluster-api/util"
+	"sigs.k8s.io/cluster-api/util/certs"
+	"sigs.k8s.io/cluster-api/util/secret"
+)
+
+var (
+	// ErrDependentCertificateNotFound signals that a CA secret could not be found.
+	ErrDependentCertificateNotFound = errors.New("could not find secret ca")
+)
+
+// FromSecret fetches the Kubeconfig for a Cluster.
+func FromSecret(ctx context.Context, c client.Reader, cluster client.ObjectKey) ([]byte, error) {
+	out, err := secret.Get(ctx, c, cluster, secret.Kubeconfig)
+	if err != nil {
+		return nil, err
+	}
+	return toKubeconfigBytes(out)
+}
+
+// New creates a new Kubeconfig using the cluster name and specified endpoint.
+func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Signer) (*api.Config, error) {
+	cfg := &certs.Config{
+		CommonName:   "kubernetes-admin",
+		Organization: []string{"system:masters"},
+		Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+	}
+
+	clientKey, err := certs.NewPrivateKey()
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to create private key")
+	}
+
+	clientCert, err := cfg.NewSignedCert(clientKey, caCert, caKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to sign certificate")
+	}
+
+	userName := fmt.Sprintf("%s-admin", clusterName)
+	contextName := fmt.Sprintf("%s@%s", userName, clusterName)
+
+	return &api.Config{
+		Clusters: map[string]*api.Cluster{
+			clusterName: {
+				Server:                   endpoint,
+				CertificateAuthorityData: certs.EncodeCertPEM(caCert),
+			},
+		},
+		Contexts: map[string]*api.Context{
+			contextName: {
+				Cluster:  clusterName,
+				AuthInfo: userName,
+			},
+		},
+		AuthInfos: map[string]*api.AuthInfo{
+			userName: {
+				ClientKeyData:         certs.EncodePrivateKeyPEM(clientKey),
+				ClientCertificateData: certs.EncodeCertPEM(clientCert),
+			},
+		},
+		CurrentContext: contextName,
+	}, nil
+}
+
+// CreateSecret creates the Kubeconfig secret for the given cluster.
+func CreateSecret(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) error {
+	name := util.ObjectKey(cluster)
+	return CreateSecretWithOwner(ctx, c, name, cluster.Spec.ControlPlaneEndpoint.String(), metav1.OwnerReference{
+		APIVersion: clusterv1.GroupVersion.String(),
+		Kind:       "Cluster",
+		Name:       cluster.Name,
+		UID:        cluster.UID,
+	})
+}
+
+// CreateSecretWithOwner creates the Kubeconfig secret for the given cluster name, namespace, endpoint, and owner reference.
+func CreateSecretWithOwner(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, owner metav1.OwnerReference) error {
+	server := fmt.Sprintf("https://%s", endpoint)
+	out, err := generateKubeconfig(ctx, c, clusterName, server)
+	if err != nil {
+		return err
+	}
+
+	return c.Create(ctx, GenerateSecretWithOwner(clusterName, out, owner))
+}
+
+// GenerateSecret returns a Kubernetes secret for the given Cluster and kubeconfig data.
+func GenerateSecret(cluster *clusterv1.Cluster, data []byte) *corev1.Secret {
+	name := util.ObjectKey(cluster)
+	return GenerateSecretWithOwner(name, data, metav1.OwnerReference{
+		APIVersion: clusterv1.GroupVersion.String(),
+		Kind:       "Cluster",
+		Name:       cluster.Name,
+		UID:        cluster.UID,
+	})
+}
+
+// GenerateSecretWithOwner returns a Kubernetes secret for the given Cluster name, namespace, kubeconfig data, and ownerReference.
+func GenerateSecretWithOwner(clusterName client.ObjectKey, data []byte, owner metav1.OwnerReference) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secret.Name(clusterName.Name, secret.Kubeconfig),
+			Namespace: clusterName.Namespace,
+			Labels: map[string]string{
+				clusterv1.ClusterLabelName: clusterName.Name,
+			},
+			OwnerReferences: []metav1.OwnerReference{
+				owner,
+			},
+		},
+		Data: map[string][]byte{
+			secret.KubeconfigDataName: data,
+		},
+		Type: clusterv1.ClusterSecretType,
+	}
+}
+
+// NeedsClientCertRotation returns whether any of the Kubeconfig secret's client certificates will expire before the given threshold.
+func NeedsClientCertRotation(configSecret *corev1.Secret, threshold time.Duration) (bool, error) {
+	now := time.Now()
+
+	data, err := toKubeconfigBytes(configSecret)
+	if err != nil {
+		return false, err
+	}
+
+	config, err := clientcmd.Load(data)
+	if err != nil {
+		return false, errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config")
+	}
+
+	for _, authInfo := range config.AuthInfos {
+		cert, err := certs.DecodeCertPEM(authInfo.ClientCertificateData)
+		if err != nil {
+			return false, errors.Wrap(err, "failed to decode kubeconfig client certificate")
+		}
+		if cert.NotAfter.Sub(now) < threshold {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+// RegenerateSecret creates and stores a new Kubeconfig in the given secret.
+func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1.Secret) error {
+	clusterName, _, err := secret.ParseSecretName(configSecret.Name)
+	if err != nil {
+		return errors.Wrap(err, "failed to parse secret name")
+	}
+	data, err := toKubeconfigBytes(configSecret)
+	if err != nil {
+		return err
+	}
+
+	config, err := clientcmd.Load(data)
+	if err != nil {
+		return errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config")
+	}
+	endpoint := config.Clusters[clusterName].Server
+	key := client.ObjectKey{Name: clusterName, Namespace: configSecret.Namespace}
+	out, err := generateKubeconfig(ctx, c, key, endpoint)
+	if err != nil {
+		return err
+	}
+	configSecret.Data[secret.KubeconfigDataName] = out
+	return c.Update(ctx, configSecret)
+}
+
+func generateKubeconfig(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string) ([]byte, error) {
+	clusterCA, err := secret.GetFromNamespacedName(ctx, c, clusterName, secret.ClusterCA)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, ErrDependentCertificateNotFound
+		}
+		return nil, err
+	}
+
+	cert, err := certs.DecodeCertPEM(clusterCA.Data[secret.TLSCrtDataName])
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode CA Cert")
+	} else if cert == nil {
+		return nil, errors.New("certificate not found in config")
+	}
+
+	key, err := certs.DecodePrivateKeyPEM(clusterCA.Data[secret.TLSKeyDataName])
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to decode private key")
+	} else if key == nil {
+		return nil, errors.New("CA private key not found")
+	}
+
+	cfg, err := New(clusterName.Name, endpoint, cert, key)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to generate a kubeconfig")
+	}
+
+	out, err := clientcmd.Write(*cfg)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to serialize config to yaml")
+	}
+	return out, nil
+}
+
+func toKubeconfigBytes(out *corev1.Secret) ([]byte, error) {
+	data, ok := out.Data[secret.KubeconfigDataName]
+	if !ok {
+		return nil, errors.Errorf("missing key %q in secret data", secret.KubeconfigDataName)
+	}
+	return data, nil
+}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c5664fa7..84e24e3d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -11,9 +11,9 @@ on: # yamllint disable-line rule:truthy
       - "**.go"
       - "**go.mod"
       - "**go.sum"
+      - ".github/workflows/**"
       - "images/builder/**"
       - "images/caph/**"
-      - "!**/vendor/**"
   push:
     branches:
       - main
@@ -29,7 +29,6 @@ jobs:
     steps:
       - name: Checkout repository
         uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
-
       - name: Coverage result name
         id: name
         run: |
@@ -48,7 +47,7 @@ jobs:
           cache-dependency-path: go.sum
 
       - name: Install dependencies
-        run: make gotestsum go-cover-treemap
+        run: make gotestsum go-cover-treemap setup-envtest
 
       - name: Running tests
         run: make test-unit
diff --git a/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/testing.go b/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/testing.go
new file mode 100644
index 00000000..72444d5b
--- /dev/null
+++ b/vendor/sigs.k8s.io/cluster-api/util/kubeconfig/testing.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2019 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package kubeconfig
+
+import (
+	"fmt"
+
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// FromEnvTestConfig returns a new Kubeconfig in byte form when running in envtest.
+func FromEnvTestConfig(cfg *rest.Config, cluster *clusterv1.Cluster) []byte {
+	contextName := fmt.Sprintf("%s@%s", cfg.Username, cluster.Name)
+	c := api.Config{
+		Clusters: map[string]*api.Cluster{
+			cluster.Name: {
+				Server:                   cfg.Host,
+				CertificateAuthorityData: cfg.CAData,
+			},
+		},
+		Contexts: map[string]*api.Context{
+			contextName: {
+				Cluster:  cluster.Name,
+				AuthInfo: cfg.Username,
+			},
+		},
+		AuthInfos: map[string]*api.AuthInfo{
+			cfg.Username: {
+				ClientKeyData:         cfg.KeyData,
+				ClientCertificateData: cfg.CertData,
+			},
+		},
+		CurrentContext: contextName,
+	}
+	data, _ := clientcmd.Write(c)
+	return data
+}