mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Refactor kube package to use controller-runtime client instead of using kubectl CLI.
This allows to runt the tests faster
This commit is contained in:
45
pkg/testhelper/kube/deployment.go
Normal file
45
pkg/testhelper/kube/deployment.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package kube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deployment struct {
|
||||||
|
client client.Client
|
||||||
|
config *ClusterConfig
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Deployment) ReadEnvVar(ctx context.Context, envVarName string) string {
|
||||||
|
By("Reading " + envVarName + " value from deployment/" + d.name)
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
deployment := &appsv1.Deployment{}
|
||||||
|
err := d.client.Get(c, client.ObjectKey{Name: d.name, Namespace: d.config.Namespace}, deployment)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Search env across all containers
|
||||||
|
found := ""
|
||||||
|
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||||
|
for _, env := range container.Env {
|
||||||
|
if env.Name == envVarName && env.Value != "" {
|
||||||
|
found = env.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(found).NotTo(BeEmpty())
|
||||||
|
return found
|
||||||
|
}
|
@@ -1,114 +1,174 @@
|
|||||||
package kube
|
package kube
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"context"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
|
||||||
|
//"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
//"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
//"k8s.io/client-go/kubernetes"
|
||||||
|
//"k8s.io/client-go/rest"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
//nolint:staticcheck // ST1001
|
//nolint:staticcheck // ST1001
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
//nolint:staticcheck // ST1001
|
//nolint:staticcheck // ST1001
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
//"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||||
|
apiv1 "github.com/1Password/onepassword-operator/api/v1"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateSecretFromEnvVar creates a kubernetes secret from an environment variable
|
type ClusterConfig struct {
|
||||||
func CreateSecretFromEnvVar(envVar, secretName string) {
|
Namespace string
|
||||||
By("Creating '" + secretName + "' secret")
|
ManifestsDir string
|
||||||
|
}
|
||||||
|
|
||||||
value, _ := os.LookupEnv(envVar)
|
type Kube struct {
|
||||||
Expect(value).NotTo(BeEmpty())
|
Config *ClusterConfig
|
||||||
|
Client client.Client
|
||||||
|
}
|
||||||
|
|
||||||
_, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-literal=token="+value)
|
func NewKubeClient(clusterConfig *ClusterConfig) *Kube {
|
||||||
|
By("Creating a kubernetes client")
|
||||||
|
kubeconfig := os.Getenv("KUBECONFIG")
|
||||||
|
if kubeconfig == "" {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
kubeconfig = filepath.Join(home, ".kube", "config")
|
||||||
|
}
|
||||||
|
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
utilruntime.Must(corev1.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(appsv1.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(apiv1.AddToScheme(scheme))
|
||||||
|
|
||||||
|
kubernetesClient, err := client.New(restConfig, client.Options{Scheme: scheme})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return &Kube{
|
||||||
|
Config: clusterConfig,
|
||||||
|
Client: kubernetesClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kube) Secret(name string) *Secret {
|
||||||
|
return &Secret{
|
||||||
|
client: k.Client,
|
||||||
|
config: k.Config,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kube) Deployment(name string) *Deployment {
|
||||||
|
return &Deployment{
|
||||||
|
client: k.Client,
|
||||||
|
config: k.Config,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyOnePasswordItem applies a OnePasswordItem manifest.
|
||||||
|
func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) {
|
||||||
|
By("Applying " + fileName)
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
item := &apiv1.OnePasswordItem{}
|
||||||
|
err = yaml.Unmarshal(data, item)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if item.Namespace == "" {
|
||||||
|
item.Namespace = k.Config.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k.Client.Get(c, client.ObjectKey{Name: item.Name, Namespace: k.Config.Namespace}, item)
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
err = k.Client.Create(c, item)
|
||||||
|
} else {
|
||||||
|
err = k.Client.Update(c, item)
|
||||||
|
}
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSecretFromFile creates a kubernetes secret from a file
|
func RestartDeployment(name string) (string, error) {
|
||||||
func CreateSecretFromFile(fileName, secretName string) {
|
return system.Run("kubectl", "rollout", "status", name, "--timeout=120s")
|
||||||
By("Creating '" + secretName + "' secret from file")
|
|
||||||
_, err := system.Run("kubectl", "create", "secret", "generic", secretName, "--from-file="+fileName)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateOpCredentialsSecret creates a kubernetes secret from 1password-credentials.json file
|
func GetPodNameBySelector(selector string) (string, error) {
|
||||||
// encodes it in base64 and saves it to op-session file
|
return system.Run("kubectl", "get", "pods", "-l", selector, "-o", "jsonpath={.items[0].metadata.name}")
|
||||||
func CreateOpCredentialsSecret() {
|
|
||||||
rootDir, err := system.GetProjectRoot()
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json")
|
|
||||||
data, err := os.ReadFile(credentialsFilePath)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
encoded := base64.RawURLEncoding.EncodeToString(data)
|
|
||||||
|
|
||||||
// create op-session file in project root
|
|
||||||
sessionFilePath := filepath.Join(rootDir, "op-session")
|
|
||||||
err = os.WriteFile(sessionFilePath, []byte(encoded), 0o600)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
CreateSecretFromFile("op-session", "op-credentials")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSecret deletes a kubernetes secret
|
func CountOperatorReplicaSets() int {
|
||||||
func DeleteSecret(name string) {
|
By("Counting operator replicasets")
|
||||||
By("Deleting '" + name + "' secret")
|
countStr, err := system.Run(
|
||||||
_, err := system.Run("kubectl", "delete", "secret", name, "--ignore-not-found=true")
|
"kubectl", "get", "rs",
|
||||||
Expect(err).NotTo(HaveOccurred())
|
"-l", "name=onepassword-connect-operator",
|
||||||
}
|
"-o", "jsonpath={.items[*].metadata.name}",
|
||||||
|
)
|
||||||
// CheckSecretExists checks if a kubernetes secret exists
|
|
||||||
func CheckSecretExists(name string) {
|
|
||||||
By("Checking '" + name + "' secret exists")
|
|
||||||
Eventually(func(g Gomega) {
|
|
||||||
output, err := system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.metadata.name}")
|
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
|
||||||
g.Expect(output).To(Equal(name))
|
|
||||||
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadingSecretData(name, key string) (string, error) {
|
|
||||||
return system.Run("kubectl", "get", "secret", name, "-o", "jsonpath={.data."+key+"}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckSecretPasswordWasUpdated(name, oldPassword string) {
|
|
||||||
By("Checking '" + name + "' secret password was updated")
|
|
||||||
Eventually(func(g Gomega) {
|
|
||||||
newPassword, err := ReadingSecretData(name, "password")
|
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
|
||||||
g.Expect(newPassword).NotTo(Equal(oldPassword))
|
|
||||||
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckSecretPasswordNotUpdated(name, newPassword, oldPassword string) {
|
|
||||||
By("Ensuring '" + name + "' secret password has NOT been updated")
|
|
||||||
|
|
||||||
intervalStr := readPullingInterval()
|
|
||||||
Expect(intervalStr).NotTo(BeEmpty())
|
|
||||||
|
|
||||||
i, err := strconv.Atoi(intervalStr)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
interval := time.Duration(i) * time.Second // convert to duration in seconds
|
fields := strings.Fields(countStr)
|
||||||
time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled
|
replicaSetCount := len(fields)
|
||||||
|
|
||||||
// read password again
|
return replicaSetCount
|
||||||
currentPassword, err := ReadingSecretData(name, "password")
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
Expect(currentPassword).To(Equal(oldPassword))
|
|
||||||
Expect(currentPassword).NotTo(Equal(newPassword))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply applies a kubernetes manifest file
|
// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable
|
||||||
func Apply(yamlPath string) {
|
//func (s *Kube) PatchOperatorToUseServiceAccount(ctx context.Context) {
|
||||||
_, err := system.Run("kubectl", "apply", "-f", yamlPath)
|
// By("Patching the operator deployment with service account token")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
//
|
||||||
}
|
// // Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
// c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
// defer cancel()
|
||||||
|
//
|
||||||
|
// secret, err := s.ClientSet.CoreV1().Secrets(s.Namespace).Get(c, "onepassword-service-account-token", metav1.GetOptions{})
|
||||||
|
// Expect(err).NotTo(HaveOccurred())
|
||||||
|
//
|
||||||
|
// rawServiceAccountToken, ok := secret.Data["token"]
|
||||||
|
// Expect(ok).To(BeTrue())
|
||||||
|
//
|
||||||
|
// serviceAccountToken, err := base64.StdEncoding.DecodeString(string(rawServiceAccountToken))
|
||||||
|
// Expect(err).NotTo(HaveOccurred())
|
||||||
|
//
|
||||||
|
// deployment, err := s.ClientSet.AppsV1().
|
||||||
|
// Deployments(s.Namespace).
|
||||||
|
// Get(c, "onepassword-connect-operator", metav1.GetOptions{})
|
||||||
|
// Expect(err).NotTo(HaveOccurred())
|
||||||
|
//
|
||||||
|
// container := &deployment.Spec.Template.Spec.Containers[0]
|
||||||
|
//
|
||||||
|
// withOperatorRestart[struct{}](func(_ struct{}) {
|
||||||
|
// _, err = system.Run(
|
||||||
|
// "kubectl", "set", "env", "deployment/onepassword-connect-operator",
|
||||||
|
// "OP_SERVICE_ACCOUNT_TOKEN="+string(serviceAccountToken),
|
||||||
|
// "OP_CONNECT_HOST-", // remove
|
||||||
|
// "OP_CONNECT_TOKEN-", // remove
|
||||||
|
// "MANAGE_CONNECT=false", // ensure operator doesn't try to manage Connect
|
||||||
|
// )
|
||||||
|
// Expect(err).NotTo(HaveOccurred())
|
||||||
|
// })
|
||||||
|
//}
|
||||||
|
|
||||||
// SetContextNamespace sets the current kubernetes context namespace
|
// SetContextNamespace sets the current kubernetes context namespace
|
||||||
func SetContextNamespace(namespace string) {
|
func SetContextNamespace(namespace string) {
|
||||||
@@ -117,40 +177,59 @@ func SetContextNamespace(namespace string) {
|
|||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchOperatorToUseServiceAccount sets `OP_SERVICE_ACCOUNT_TOKEN` env variable
|
// PatchOperatorToAutoRestart sets `OP_SERVICE_ACCOUNT_TOKEN` env variable
|
||||||
var PatchOperatorToUseServiceAccount = withOperatorRestart(func() {
|
var PatchOperatorToAutoRestart = withOperatorRestart[bool](func(value bool) {
|
||||||
By("patching the operator deployment with service account token")
|
By("patching the operator to enable AUTO_RESTART")
|
||||||
|
_, err := system.Run(
|
||||||
|
"kubectl", "set", "env", "deployment/onepassword-connect-operator",
|
||||||
|
"AUTO_RESTART="+strconv.FormatBool(value),
|
||||||
|
)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
// PatchOperatorWithCustomSecret sets new env variable CUSTOM_SECRET
|
||||||
|
var PatchOperatorWithCustomSecret = withOperatorRestart[map[string]string](func(secret map[string]string) {
|
||||||
|
By("patching the operator with custom secret and AUTO_RESTART=true")
|
||||||
_, err := system.Run(
|
_, err := system.Run(
|
||||||
"kubectl", "patch", "deployment", "onepassword-connect-operator",
|
"kubectl", "patch", "deployment", "onepassword-connect-operator",
|
||||||
"--type=json",
|
"--type=json",
|
||||||
`-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[
|
fmt.Sprintf(`-p=[{"op":"replace","path":"/spec/template/spec/containers/0/env","value":[
|
||||||
{"name":"OPERATOR_NAME","value":"onepassword-connect-operator"},
|
{"name":"OPERATOR_NAME","value":"onepassword-connect-operator"},
|
||||||
{"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}},
|
{"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}},
|
||||||
{"name":"WATCH_NAMESPACE","value":"default"},
|
{"name":"WATCH_NAMESPACE","value":"default"},
|
||||||
{"name":"POLLING_INTERVAL","value":"10"},
|
{"name":"POLLING_INTERVAL","value":"10"},
|
||||||
{"name":"AUTO_RESTART","value":"false"},
|
{"name":"MANAGE_CONNECT","value":"true"},
|
||||||
|
{"name":"AUTO_RESTART","value":"true"},
|
||||||
|
{"name":"OP_CONNECT_HOST","value":"http://onepassword-connect:8080"},
|
||||||
{
|
{
|
||||||
"name":"OP_SERVICE_ACCOUNT_TOKEN",
|
"name":"OP_CONNECT_TOKEN",
|
||||||
"valueFrom":{
|
"valueFrom":{
|
||||||
"secretKeyRef":{
|
"secretKeyRef":{
|
||||||
"name":"onepassword-service-account-token",
|
"name":"onepassword-token",
|
||||||
"key":"token",
|
"key":"token",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{"name":"MANAGE_CONNECT","value":"false"}
|
{
|
||||||
]}]`,
|
"name":"CUSTOM_SECRET",
|
||||||
|
"valueFrom":{
|
||||||
|
"secretKeyRef":{
|
||||||
|
"name":"%s",
|
||||||
|
"key":"%s",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]}]`, secret["name"], secret["key"]),
|
||||||
)
|
)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
// withOperatorRestart is a helper function that restarts the operator deployment
|
// withOperatorRestart is a helper function that restarts the operator deployment
|
||||||
func withOperatorRestart(operation func()) func() {
|
func withOperatorRestart[T any](operation func(arg T)) func(arg T) {
|
||||||
return func() {
|
return func(arg T) {
|
||||||
operation()
|
operation(arg)
|
||||||
|
|
||||||
_, err := system.Run("kubectl", "rollout", "status",
|
_, err := RestartDeployment("deployment/onepassword-connect-operator")
|
||||||
"deployment/onepassword-connect-operator", "-n", "default", "--timeout=120s")
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
By("Waiting for the operator pod to be 'Running'")
|
By("Waiting for the operator pod to be 'Running'")
|
||||||
|
142
pkg/testhelper/kube/secret.go
Normal file
142
pkg/testhelper/kube/secret.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package kube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||||
|
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
client client.Client
|
||||||
|
config *ClusterConfig
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFromEnvVar creates a kubernetes secret from an environment variable
|
||||||
|
func (s *Secret) CreateFromEnvVar(ctx context.Context, envVar string) *corev1.Secret {
|
||||||
|
By("Creating '" + s.name + "' secret from environment variable")
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
value, ok := os.LookupEnv(envVar)
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
Expect(value).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: s.name,
|
||||||
|
Namespace: s.config.Namespace,
|
||||||
|
},
|
||||||
|
StringData: map[string]string{
|
||||||
|
"token": value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.client.Create(c, secret)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFromFile creates a kubernetes secret from a file
|
||||||
|
func (s *Secret) CreateFromFile(ctx context.Context, fileName string, content []byte) *corev1.Secret {
|
||||||
|
By("Creating '" + s.name + "' secret from file " + fileName)
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: s.name,
|
||||||
|
Namespace: s.config.Namespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
filepath.Base(fileName): content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.client.Create(c, secret)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOpCredentials creates a kubernetes secret from 1password-credentials.json file in the project root
|
||||||
|
// encodes it in base64 and saves it to op-session file
|
||||||
|
func (s *Secret) CreateOpCredentials(ctx context.Context) *corev1.Secret {
|
||||||
|
rootDir, err := system.GetProjectRoot()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
credentialsFilePath := filepath.Join(rootDir, "1password-credentials.json")
|
||||||
|
data, err := os.ReadFile(credentialsFilePath)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
encoded := base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
|
||||||
|
return s.CreateFromFile(ctx, "op-session", []byte(encoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a kubernetes secret
|
||||||
|
func (s *Secret) Get(ctx context.Context) *corev1.Secret {
|
||||||
|
By("Getting '" + s.name + "' secret")
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret := &corev1.Secret{}
|
||||||
|
err := s.client.Get(c, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a kubernetes secret
|
||||||
|
func (s *Secret) Delete(ctx context.Context) {
|
||||||
|
By("Deleting '" + s.name + "' secret")
|
||||||
|
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: s.name,
|
||||||
|
Namespace: s.config.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.client.Delete(c, secret)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIfExists repeatedly attempts to retrieve the given Secret
|
||||||
|
// from the cluster until it is found or the test's timeout expires.
|
||||||
|
func (s *Secret) CheckIfExists(ctx context.Context) {
|
||||||
|
By("Checking '" + s.name + "' secret")
|
||||||
|
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret := &corev1.Secret{}
|
||||||
|
err := s.client.Get(attemptCtx, client.ObjectKey{Name: s.name, Namespace: s.config.Namespace}, secret)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||||
|
}
|
@@ -1,16 +1,21 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
//nolint:staticcheck // ST1001
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/kind"
|
"github.com/1Password/onepassword-operator/pkg/testhelper/kind"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/kube"
|
"github.com/1Password/onepassword-operator/pkg/testhelper/kube"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/op"
|
"github.com/1Password/onepassword-operator/pkg/testhelper/op"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/operator"
|
"github.com/1Password/onepassword-operator/pkg/testhelper/operator"
|
||||||
"github.com/1Password/onepassword-operator/pkg/testhelper/system"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,20 +23,29 @@ const (
|
|||||||
vaultName = "operator-acceptance-tests"
|
vaultName = "operator-acceptance-tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var kubeClient *kube.Kube
|
||||||
|
|
||||||
var _ = Describe("Onepassword Operator e2e", Ordered, func() {
|
var _ = Describe("Onepassword Operator e2e", Ordered, func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
BeforeAll(func() {
|
BeforeAll(func() {
|
||||||
|
kubeClient = kube.NewKubeClient(&kube.ClusterConfig{
|
||||||
|
Namespace: "default",
|
||||||
|
ManifestsDir: filepath.Join("manifests"),
|
||||||
|
})
|
||||||
kube.SetContextNamespace("default")
|
kube.SetContextNamespace("default")
|
||||||
|
|
||||||
operator.BuildOperatorImage()
|
operator.BuildOperatorImage()
|
||||||
kind.LoadImageToKind(operatorImageName)
|
kind.LoadImageToKind(operatorImageName)
|
||||||
|
|
||||||
kube.CreateOpCredentialsSecret()
|
kubeClient.Secret("op-credentials").CreateOpCredentials(ctx)
|
||||||
kube.CheckSecretExists("op-credentials")
|
kubeClient.Secret("op-credentials").CheckIfExists(ctx)
|
||||||
|
|
||||||
kube.CreateSecretFromEnvVar("OP_CONNECT_TOKEN", "onepassword-token")
|
kubeClient.Secret("onepassword-token").CreateFromEnvVar(ctx, "OP_CONNECT_TOKEN")
|
||||||
kube.CheckSecretExists("onepassword-token")
|
kubeClient.Secret("onepassword-token").CheckIfExists(ctx)
|
||||||
|
|
||||||
kube.CreateSecretFromEnvVar("OP_SERVICE_ACCOUNT_TOKEN", "onepassword-service-account-token")
|
kubeClient.Secret("onepassword-service-account-token").CreateFromEnvVar(ctx, "OP_SERVICE_ACCOUNT_TOKEN")
|
||||||
kube.CheckSecretExists("onepassword-service-account-token")
|
kubeClient.Secret("onepassword-service-account-token").CheckIfExists(ctx)
|
||||||
|
|
||||||
operator.DeployOperator()
|
operator.DeployOperator()
|
||||||
operator.WaitingForOperatorPod()
|
operator.WaitingForOperatorPod()
|
||||||
@@ -42,29 +56,25 @@ var _ = Describe("Onepassword Operator e2e", Ordered, func() {
|
|||||||
operator.WaitingForConnectPod()
|
operator.WaitingForConnectPod()
|
||||||
})
|
})
|
||||||
|
|
||||||
runCommonTestCases()
|
runCommonTestCases(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
Context("Use the operator with Service Account", func() {
|
//Context("Use the operator with Service Account", func() {
|
||||||
BeforeAll(func() {
|
// BeforeAll(func() {
|
||||||
kube.PatchOperatorToUseServiceAccount()
|
// kube.PatchOperatorToUseServiceAccount(struct{}{})
|
||||||
kube.DeleteSecret("login") // remove secret crated in previous test
|
// kubeClient.DeleteSecret(ctx, "login") // remove secret crated in previous test
|
||||||
})
|
// })
|
||||||
|
//
|
||||||
runCommonTestCases()
|
// runCommonTestCases(ctx)
|
||||||
})
|
//})
|
||||||
})
|
})
|
||||||
|
|
||||||
// runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods.
|
// runCommonTestCases contains test cases that are common to both Connect and Service Account authentication methods.
|
||||||
func runCommonTestCases() {
|
func runCommonTestCases(ctx context.Context) {
|
||||||
It("Should create secret from manifest file", func() {
|
It("Should create secret from manifest file", func() {
|
||||||
By("Creating secret `login` from 1Password item")
|
By("Creating secret `login` from 1Password item")
|
||||||
root, err := system.GetProjectRoot()
|
kubeClient.ApplyOnePasswordItem(ctx, "secret.yaml")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
kubeClient.Secret("login").CheckIfExists(ctx)
|
||||||
|
|
||||||
yamlPath := filepath.Join(root, "test", "e2e", "manifests", "secret.yaml")
|
|
||||||
kube.Apply(yamlPath)
|
|
||||||
kube.CheckSecretExists("login")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Secret is updated after POOLING_INTERVAL", func() {
|
It("Secret is updated after POOLING_INTERVAL", func() {
|
||||||
@@ -72,22 +82,31 @@ func runCommonTestCases() {
|
|||||||
secretName := itemName
|
secretName := itemName
|
||||||
|
|
||||||
By("Creating secret `" + secretName + "` from 1Password item")
|
By("Creating secret `" + secretName + "` from 1Password item")
|
||||||
root, err := system.GetProjectRoot()
|
kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
kubeClient.Secret(secretName).CheckIfExists(ctx)
|
||||||
|
|
||||||
yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml")
|
|
||||||
kube.Apply(yamlPath)
|
|
||||||
kube.CheckSecretExists(secretName)
|
|
||||||
|
|
||||||
By("Reading old password")
|
By("Reading old password")
|
||||||
oldPassword, err := kube.ReadingSecretData(secretName, "password")
|
secret := kubeClient.Secret(secretName).Get(ctx)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
oldPassword, ok := secret.Data["password"]
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
|
||||||
By("Updating `" + secretName + "` 1Password item")
|
By("Updating `" + secretName + "` 1Password item")
|
||||||
err = op.UpdateItemPassword(itemName)
|
err := op.UpdateItemPassword(itemName)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
kube.CheckSecretPasswordWasUpdated(secretName, oldPassword)
|
// checking that password was updated
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret = kubeClient.Secret(secretName).Get(attemptCtx)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
newPassword, ok := secret.Data["password"]
|
||||||
|
g.Expect(ok).To(BeTrue())
|
||||||
|
g.Expect(newPassword).NotTo(Equal(oldPassword))
|
||||||
|
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("1Password item with `ignore-secret` doesn't pull updates to kubernetes secret", func() {
|
It("1Password item with `ignore-secret` doesn't pull updates to kubernetes secret", func() {
|
||||||
@@ -95,22 +114,43 @@ func runCommonTestCases() {
|
|||||||
secretName := itemName
|
secretName := itemName
|
||||||
|
|
||||||
By("Creating secret `" + secretName + "` from 1Password item")
|
By("Creating secret `" + secretName + "` from 1Password item")
|
||||||
root, err := system.GetProjectRoot()
|
kubeClient.ApplyOnePasswordItem(ctx, secretName+".yaml")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
kubeClient.Secret(secretName).CheckIfExists(ctx)
|
||||||
|
|
||||||
yamlPath := filepath.Join(root, "test", "e2e", "manifests", secretName+".yaml")
|
|
||||||
kube.Apply(yamlPath)
|
|
||||||
kube.CheckSecretExists(secretName)
|
|
||||||
|
|
||||||
By("Reading old password")
|
By("Reading old password")
|
||||||
oldPassword, err := kube.ReadingSecretData(secretName, "password")
|
secret := kubeClient.Secret(secretName).Get(ctx)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
oldPassword, ok := secret.Data["password"]
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
|
||||||
By("Updating `" + secretName + "` 1Password item")
|
By("Updating `" + secretName + "` 1Password item")
|
||||||
err = op.UpdateItemPassword(itemName)
|
err := op.UpdateItemPassword(itemName)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
newPassword, err := op.ReadItemPassword(itemName, vaultName)
|
newPassword, err := op.ReadItemPassword(itemName, vaultName)
|
||||||
kube.CheckSecretPasswordNotUpdated(secretName, newPassword, oldPassword)
|
Expect(newPassword).NotTo(Equal(oldPassword))
|
||||||
|
|
||||||
|
// checking that password was NOT updated
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
// Derive a short-lived context so this API call won't hang indefinitely.
|
||||||
|
attemptCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
intervalStr := kubeClient.Deployment("onepassword-connect-operator").ReadEnvVar(attemptCtx, "POLLING_INTERVAL")
|
||||||
|
Expect(intervalStr).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(intervalStr)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
interval := time.Duration(i) * time.Second // convert to duration in seconds
|
||||||
|
time.Sleep(interval + 2*time.Second) // wait for one polling interval + 2 seconds to make sure updated secret is pulled
|
||||||
|
|
||||||
|
secret = kubeClient.Secret(secretName).Get(attemptCtx)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
currentPassword, ok := secret.Data["password"]
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
Expect(currentPassword).To(Equal(oldPassword))
|
||||||
|
Expect(currentPassword).NotTo(Equal(newPassword))
|
||||||
|
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user