Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ rules:
- pods
- pods/attach
- secrets
- serviceaccounts
- services
verbs:
- create
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/function_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type FunctionReconciler struct {
// +kubebuilder:rbac:groups=functions.dev,resources=functions,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=functions.dev,resources=functions/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=functions.dev,resources=functions/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=pods;pods/attach;secrets;services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=pods;pods/attach;secrets;serviceaccounts;services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
// +kubebuilder:rbac:groups="apps",resources=deployments;replicasets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="serving.knative.dev",resources=services;routes,verbs=get;list;watch;create;update;patch;delete
Expand Down
30 changes: 30 additions & 0 deletions internal/controller/function_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func (r *FunctionReconciler) deploy(ctx context.Context, function *v1alpha1.Func
defer os.Remove(authFile)

deployOptions.RegistryAuthFile = authFile

if err := r.ensureImagePullSecret(ctx, function); err != nil {
return fmt.Errorf("failed to ensure image pull secret: %w", err)
}
}

logger.Info("Deploying function", "deployOptions", deployOptions)
Expand All @@ -62,6 +66,32 @@ func (r *FunctionReconciler) deploy(ctx context.Context, function *v1alpha1.Func
return nil
}

func (r *FunctionReconciler) ensureImagePullSecret(ctx context.Context, function *v1alpha1.Function) error {
logger := log.FromContext(ctx)

secretName := function.Spec.Registry.AuthSecretRef.Name

sa := &v1.ServiceAccount{}
if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: function.Namespace}, sa); err != nil {
return fmt.Errorf("failed to get default service account: %w", err)
}

for _, ref := range sa.ImagePullSecrets {
if ref.Name == secretName {
logger.Info("Image pull secret already present on default ServiceAccount", "secret", secretName)
return nil
}
}

sa.ImagePullSecrets = append(sa.ImagePullSecrets, v1.LocalObjectReference{Name: secretName})
if err := r.Update(ctx, sa); err != nil {
return fmt.Errorf("failed to update default service account with image pull secret: %w", err)
}

logger.Info("Added image pull secret to default ServiceAccount", "secret", secretName)
return nil
}

func (r *FunctionReconciler) persistRegistryAuthSecret(ctx context.Context, function *v1alpha1.Function) (string, error) {
logger := log.FromContext(ctx)

Expand Down
142 changes: 142 additions & 0 deletions internal/controller/function_deploy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package controller

import (
functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/tools/events"
)

var _ = Describe("Function Deploy", func() {
Context("ensureImagePullSecret", func() {
var reconciler *FunctionReconciler
var testNamespace string

BeforeEach(func() {
testNamespace = "deploy-test-" + rand.String(6)
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}
Expect(k8sClient.Create(ctx, ns)).To(Succeed())

sa := &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: testNamespace}}
Expect(k8sClient.Create(ctx, sa)).To(Succeed())

reconciler = &FunctionReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Recorder: &events.FakeRecorder{},
}
})

It("should add the registry auth secret to the default ServiceAccount's imagePullSecrets", func() {
function := &functionsdevv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{
Name: "func-pull-secret",
Namespace: testNamespace,
},
Spec: functionsdevv1alpha1.FunctionSpec{
Repository: functionsdevv1alpha1.FunctionSpecRepository{
URL: "https://github.com/foo/bar",
},
Registry: functionsdevv1alpha1.FunctionSpecRegistry{
AuthSecretRef: &v1.LocalObjectReference{
Name: "my-registry-secret",
},
},
},
}

Expect(reconciler.ensureImagePullSecret(ctx, function)).To(Succeed())

sa := &v1.ServiceAccount{}
Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: "default",
Namespace: testNamespace,
}, sa)).To(Succeed())

Expect(sa.ImagePullSecrets).To(ContainElement(v1.LocalObjectReference{
Name: "my-registry-secret",
}))
})

It("should be idempotent and not duplicate imagePullSecrets", func() {
function := &functionsdevv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{
Name: "func-idempotent",
Namespace: testNamespace,
},
Spec: functionsdevv1alpha1.FunctionSpec{
Repository: functionsdevv1alpha1.FunctionSpecRepository{
URL: "https://github.com/foo/bar",
},
Registry: functionsdevv1alpha1.FunctionSpecRegistry{
AuthSecretRef: &v1.LocalObjectReference{
Name: "my-registry-secret",
},
},
},
}

Expect(reconciler.ensureImagePullSecret(ctx, function)).To(Succeed())
Expect(reconciler.ensureImagePullSecret(ctx, function)).To(Succeed())

sa := &v1.ServiceAccount{}
Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: "default",
Namespace: testNamespace,
}, sa)).To(Succeed())

count := 0
for _, ref := range sa.ImagePullSecrets {
if ref.Name == "my-registry-secret" {
count++
}
}
Expect(count).To(Equal(1))
})

It("should preserve existing imagePullSecrets on the ServiceAccount", func() {
sa := &v1.ServiceAccount{}
Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: "default",
Namespace: testNamespace,
}, sa)).To(Succeed())

sa.ImagePullSecrets = []v1.LocalObjectReference{
{Name: "existing-secret"},
}
Expect(k8sClient.Update(ctx, sa)).To(Succeed())

function := &functionsdevv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{
Name: "func-preserve",
Namespace: testNamespace,
},
Spec: functionsdevv1alpha1.FunctionSpec{
Repository: functionsdevv1alpha1.FunctionSpecRepository{
URL: "https://github.com/foo/bar",
},
Registry: functionsdevv1alpha1.FunctionSpecRegistry{
AuthSecretRef: &v1.LocalObjectReference{
Name: "my-registry-secret",
},
},
},
}

Expect(reconciler.ensureImagePullSecret(ctx, function)).To(Succeed())

Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: "default",
Namespace: testNamespace,
}, sa)).To(Succeed())

Expect(sa.ImagePullSecrets).To(HaveLen(2))
Expect(sa.ImagePullSecrets).To(ContainElement(v1.LocalObjectReference{Name: "existing-secret"}))
Expect(sa.ImagePullSecrets).To(ContainElement(v1.LocalObjectReference{Name: "my-registry-secret"}))
})
})
})
6 changes: 6 additions & 0 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const namespace = "func-operator-system"
// serviceAccountName created for the project
const serviceAccountName = "func-operator-controller-manager"

const (
deployerKeda = "keda"
deployerRaw = "raw"
oldFuncCLIVersion = "v1.20.2"
)

// logFailedTestDetails logs function resource and controller logs on test failure
func logFailedTestDetails(functionName, functionNamespace string) {
specReport := CurrentSpecReport()
Expand Down
120 changes: 120 additions & 0 deletions test/e2e/func_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,126 @@ var _ = Describe("Operator", func() {
sshKeyPath, sshRepoURL, functionNamespace, "my-ssh-function-")
})
})
// This test verifies that the operator adds the registry auth secret as an
// imagePullSecret to the default ServiceAccount during a redeploy.
//
// It uses a dummy dockerconfigjson secret and the unauthenticated kind-registry
// because the kind-registry's built-in htpasswd auth is all-or-nothing (no
// per-repository scoping), so enabling auth would break all other tests. Running
// a second authenticated registry adds too much infrastructure overhead for
// verifying this wiring. The unit tests in function_deploy_test.go cover the
// ensureImagePullSecret logic itself; this test confirms the operator calls it
// during a real redeploy.
Context("with a registry auth secret", func() {
var repoURL string
var repoDir string
var functionName, functionNamespace string

BeforeEach(func() { //nolint:dupl
if os.Getenv("DEFAULT_DEPLOYER") == deployerKeda ||
os.Getenv("DEFAULT_DEPLOYER") == deployerRaw {
Skip("Skipping registry auth test for Keda & raw deployer, " +
"as those are not supported on used CLI version (1.20.x) of this tests")
}

var err error

username, password, _, cleanup, err := repoProvider.CreateRandomUser()
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(cleanup)

_, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, false)
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(cleanup)

functionNamespace, err = utils.GetTestNamespace()
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(cleanupNamespaces, functionNamespace)

oldFuncVersion := oldFuncCLIVersion
repoDir, err = utils.InitializeRepoWithFunction(
repoURL,
username,
password,
"go",
utils.WithCliVersion(oldFuncVersion))
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(os.RemoveAll, repoDir)

out, err := utils.RunFuncDeploy(repoDir,
utils.WithNamespace(functionNamespace),
utils.WithDeployCliVersion(oldFuncVersion))
Expect(err).NotTo(HaveOccurred())
_, _ = fmt.Fprint(GinkgoWriter, out)

utils.DeferCleanupOnSuccess(func() {
_, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace)
})

err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", "func.yaml")
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func() {
logFailedTestDetails(functionName, functionNamespace)
})

It("should add the registry auth secret as imagePullSecret on the default ServiceAccount", func() {
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "registry-auth-",
Namespace: functionNamespace,
},
Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths":{"kind-registry:5000":{"auth":"dGVzdDp0ZXN0"}}}`),
},
}
err := k8sClient.Create(ctx, secret)
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(func() {
_ = k8sClient.Delete(ctx, secret)
})

function := &functionsdevv1alpha1.Function{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "my-function-pullsecret-",
Namespace: functionNamespace,
},
Spec: functionsdevv1alpha1.FunctionSpec{
Repository: functionsdevv1alpha1.FunctionSpecRepository{
URL: repoURL,
},
Registry: functionsdevv1alpha1.FunctionSpecRegistry{
AuthSecretRef: &v1.LocalObjectReference{
Name: secret.Name,
},
},
},
}

err = k8sClient.Create(ctx, function)
Expect(err).NotTo(HaveOccurred())
utils.DeferCleanupOnSuccess(func() {
_, _ = utils.RunCmd("kubectl", "delete", "function", function.Name, "--namespace", function.Namespace)
})

functionName = function.Name

Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed())

sa := &v1.ServiceAccount{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: "default",
Namespace: functionNamespace,
}, sa)
Expect(err).NotTo(HaveOccurred())

Expect(sa.ImagePullSecrets).To(ContainElement(v1.LocalObjectReference{
Name: secret.Name,
}))
})
})
Context("with a private SSH repository", func() {
var sshRepoURL string
var repoDir string
Expand Down
12 changes: 7 additions & 5 deletions test/e2e/func_middleware_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ var _ = Describe("Middleware Update", func() {
var repoDir string
var functionName, functionNamespace string

BeforeEach(func() {
if os.Getenv("DEFAULT_DEPLOYER") == "keda" || os.Getenv("DEFAULT_DEPLOYER") == "raw" {
BeforeEach(func() { //nolint:dupl
if os.Getenv("DEFAULT_DEPLOYER") == deployerKeda ||
os.Getenv("DEFAULT_DEPLOYER") == deployerRaw {
Skip("Skipping middleware test for Keda & raw deployer, " +
"as those are not supported on used CLI version (1.20.x) of this tests")
}
Expand All @@ -67,7 +68,7 @@ var _ = Describe("Middleware Update", func() {

// Initialize repository with function code using OLD func CLI version
// v1.20.2 has no middleware-version label and uses instance-compatible templates
oldFuncVersion := "v1.20.2"
oldFuncVersion := oldFuncCLIVersion
repoDir, err = utils.InitializeRepoWithFunction(
repoURL,
username,
Expand Down Expand Up @@ -253,7 +254,8 @@ var _ = Describe("Middleware Update", func() {
var originalConfigMapData map[string]string

BeforeEach(func() {
if os.Getenv("DEFAULT_DEPLOYER") == "keda" || os.Getenv("DEFAULT_DEPLOYER") == "raw" {
if os.Getenv("DEFAULT_DEPLOYER") == deployerKeda ||
os.Getenv("DEFAULT_DEPLOYER") == deployerRaw {
Skip("Skipping middleware test for Keda & raw deployer, " +
"as those are not supported on used CLI version (1.20.x) of this tests")
}
Expand Down Expand Up @@ -302,7 +304,7 @@ var _ = Describe("Middleware Update", func() {

// Initialize repository with function code using OLD func CLI version
// to ensure middleware will be outdated
oldFuncVersion := "v1.20.2"
oldFuncVersion := oldFuncCLIVersion
repoDir, err = utils.InitializeRepoWithFunction(
repoURL,
username,
Expand Down
Loading