Skip to content

Commit a60d015

Browse files
authored
Merge branch 'main' into use-builder-from-func-config
2 parents b5d3baa + 8847627 commit a60d015

10 files changed

Lines changed: 444 additions & 39 deletions

File tree

.github/workflows/lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Lint
33
on:
44
push:
55
pull_request:
6+
merge_group:
67

78
jobs:
89
lint:

.github/workflows/test-e2e-bundle.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Bundle E2E Tests
33
on:
44
push:
55
pull_request:
6+
merge_group:
67

78
concurrency:
89
group: ${{ github.workflow }}-${{ github.ref }}

.github/workflows/test-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: E2E Tests
33
on:
44
push:
55
pull_request:
6+
merge_group:
67

78
concurrency:
89
group: ${{ github.workflow }}-${{ github.ref }}

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Tests
33
on:
44
push:
55
pull_request:
6+
merge_group:
67

78
jobs:
89
test:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests.
120120

121121
.PHONY: test-e2e ## Run e2e tests.
122122
test-e2e:
123-
go test ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="!bundle"
123+
go test -timeout 1h ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="!bundle"
124124

125125
.PHONY: test-e2e-bundle ## Run bundle e2e tests.
126126
test-e2e-bundle: operator-sdk docker-build docker-push bundle bundle-build bundle-push install-olm-in-cluster

test/e2e/e2e_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,30 @@ func getMetricsOutput() string {
217217
Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
218218
return metricsOutput
219219
}
220+
221+
// logFailedTestDetails logs function resource and controller logs on test failure
222+
func logFailedTestDetails(functionName, functionNamespace string) {
223+
specReport := CurrentSpecReport()
224+
if !specReport.Failed() {
225+
return
226+
}
227+
228+
if functionName != "" {
229+
cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml")
230+
function, err := utils.Run(cmd)
231+
if err == nil {
232+
_, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function)
233+
} else {
234+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err)
235+
}
236+
}
237+
238+
By("Fetching controller manager pod logs")
239+
cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace)
240+
controllerLogs, err := utils.Run(cmd)
241+
if err == nil {
242+
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
243+
} else {
244+
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
245+
}
246+
}

test/e2e/func_deploy_test.go

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,17 @@ var _ = Describe("Operator", func() {
6363
DeferCleanup(cleanupNamespaces, functionNamespace)
6464

6565
// Deploy function using func CLI
66-
cmd := exec.Command("func", "deploy",
66+
out, err := utils.RunFunc("deploy",
6767
"--namespace", functionNamespace,
6868
"--path", repoDir,
6969
"--registry", registry,
7070
"--registry-insecure", strconv.FormatBool(registryInsecure))
71-
out, err := utils.Run(cmd)
7271
Expect(err).NotTo(HaveOccurred())
7372
_, _ = fmt.Fprint(GinkgoWriter, out)
7473

7574
// Cleanup func deployment
7675
DeferCleanup(func() {
77-
cmd := exec.Command("func", "delete", "--path", repoDir, "--namespace", functionNamespace)
78-
_, _ = utils.Run(cmd)
76+
_, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace)
7977
})
8078

8179
// Commit func.yaml changes
@@ -84,27 +82,7 @@ var _ = Describe("Operator", func() {
8482
})
8583

8684
AfterEach(func() {
87-
specReport := CurrentSpecReport()
88-
if specReport.Failed() {
89-
if functionName != "" {
90-
cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml")
91-
function, err := utils.Run(cmd)
92-
if err == nil {
93-
_, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function)
94-
} else {
95-
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err)
96-
}
97-
}
98-
99-
By("Fetching controller manager pod logs")
100-
cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace)
101-
controllerLogs, err := utils.Run(cmd)
102-
if err == nil {
103-
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
104-
} else {
105-
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
106-
}
107-
}
85+
logFailedTestDetails(functionName, functionNamespace)
10886

10987
// Cleanup function resource
11088
if functionName != "" {
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"os"
23+
"os/exec"
24+
"strconv"
25+
"strings"
26+
"time"
27+
28+
functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1"
29+
"github.com/functions-dev/func-operator/internal/function"
30+
"github.com/functions-dev/func-operator/test/utils"
31+
. "github.com/onsi/ginkgo/v2"
32+
. "github.com/onsi/gomega"
33+
"gopkg.in/yaml.v3"
34+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+
"k8s.io/apimachinery/pkg/types"
36+
funcfn "knative.dev/func/pkg/functions"
37+
)
38+
39+
var _ = Describe("Middleware Update", func() {
40+
41+
SetDefaultEventuallyTimeout(2 * time.Minute)
42+
SetDefaultEventuallyPollingInterval(time.Second)
43+
44+
Context("with a function deployed using old func CLI", func() {
45+
var repoURL string
46+
var repoDir string
47+
var functionName, functionNamespace string
48+
49+
BeforeEach(func() {
50+
var err error
51+
52+
// Create repository provider resources with automatic cleanup
53+
username, password, _, cleanup, err := repoProvider.CreateRandomUser()
54+
Expect(err).NotTo(HaveOccurred())
55+
DeferCleanup(cleanup)
56+
57+
_, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, false)
58+
Expect(err).NotTo(HaveOccurred())
59+
DeferCleanup(cleanup)
60+
61+
functionNamespace, err = utils.GetTestNamespace()
62+
Expect(err).NotTo(HaveOccurred())
63+
DeferCleanup(cleanupNamespaces, functionNamespace)
64+
65+
// Initialize repository with function code using OLD func CLI version
66+
// v1.20.1 has no middleware-version label and uses instance-compatible templates
67+
oldFuncVersion := "v1.20.1"
68+
repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, username, password, "go", oldFuncVersion)
69+
Expect(err).NotTo(HaveOccurred())
70+
DeferCleanup(os.RemoveAll, repoDir)
71+
72+
// Deploy function using the same OLD func CLI version
73+
out, err := utils.RunFuncWithVersion(oldFuncVersion, "deploy",
74+
"--namespace", functionNamespace,
75+
"--path", repoDir,
76+
"--registry", registry,
77+
"--registry-insecure", strconv.FormatBool(registryInsecure))
78+
Expect(err).NotTo(HaveOccurred())
79+
_, _ = fmt.Fprint(GinkgoWriter, out)
80+
81+
// Cleanup func deployment
82+
DeferCleanup(func() {
83+
_, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace)
84+
})
85+
86+
// Commit func.yaml changes
87+
err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", "func.yaml")
88+
Expect(err).NotTo(HaveOccurred())
89+
})
90+
91+
AfterEach(func() {
92+
logFailedTestDetails(functionName, functionNamespace)
93+
94+
// Cleanup function resource
95+
if functionName != "" {
96+
cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found")
97+
_, err := utils.Run(cmd)
98+
Expect(err).NotTo(HaveOccurred())
99+
}
100+
})
101+
102+
It("should update the middleware and mark the function as ready", func() {
103+
// Get function metadata to retrieve the deployed function name
104+
funcMetadata, err := function.Metadata(repoDir)
105+
Expect(err).NotTo(HaveOccurred())
106+
deployedFunctionName := funcMetadata.Name
107+
108+
// NOTE: We use skopeo to verify the middleware version because func describe
109+
// cannot access the image registry in our test environment.
110+
//
111+
// func describe calls MiddlewareVersion() which uses go-containerregistry's
112+
// remote.Get() to fetch the image manifest and read the middleware-version label.
113+
// However, the image reference is kind-registry:5000/..., where "kind-registry"
114+
// is a Docker container name, not a real hostname.
115+
//
116+
// Docker CLI can resolve container names on Docker networks (which is why
117+
// func deploy --registry kind-registry:5000 works for pushing images), but
118+
// go-containerregistry is not Docker-aware and tries regular DNS resolution,
119+
// which fails. The error is silently ignored in describers.
120+
//
121+
// We use skopeo with localhost:5001 (port-forward to the registry) to
122+
// directly inspect the OCI image labels and verify the middleware was updated.
123+
124+
// Get initial image digest from func describe (deployed with v1.20.1)
125+
out, err := utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml")
126+
Expect(err).NotTo(HaveOccurred())
127+
128+
var initialInstance funcfn.Instance
129+
err = yaml.Unmarshal([]byte(out), &initialInstance)
130+
Expect(err).NotTo(HaveOccurred())
131+
132+
initialImage := initialInstance.Image
133+
Expect(initialImage).NotTo(BeEmpty(), "Initial image should be available from func describe")
134+
_, _ = fmt.Fprintf(GinkgoWriter, "Initial image (deployed with v1.20.1): %s\n", initialImage)
135+
136+
// Verify initial image has no middleware-version label (v1.20.1 doesn't set it)
137+
initialImageLocal := strings.Replace(initialImage, "kind-registry:5000", "localhost:5001", 1)
138+
// Remove tag if both tag and digest are present (skopeo doesn't support this format)
139+
if strings.Contains(initialImageLocal, "@") {
140+
atIndex := strings.Index(initialImageLocal, "@")
141+
slashIndex := strings.LastIndex(initialImageLocal[:atIndex], "/")
142+
if slashIndex != -1 {
143+
betweenSlashAndAt := initialImageLocal[slashIndex+1 : atIndex]
144+
if strings.Contains(betweenSlashAndAt, ":") {
145+
colonIndex := strings.Index(betweenSlashAndAt, ":")
146+
initialImageLocal = initialImageLocal[:slashIndex+1+colonIndex] + initialImageLocal[atIndex:]
147+
}
148+
}
149+
}
150+
cmd := exec.Command("skopeo",
151+
"inspect",
152+
"--tls-verify=false",
153+
"--no-tags",
154+
"docker://"+initialImageLocal)
155+
skopeoOutput, err := utils.Run(cmd)
156+
Expect(err).NotTo(HaveOccurred())
157+
158+
var initialImageLabels struct {
159+
Labels map[string]string `json:"Labels"`
160+
}
161+
err = json.Unmarshal([]byte(skopeoOutput), &initialImageLabels)
162+
Expect(err).NotTo(HaveOccurred())
163+
164+
initialMiddlewareVersion := initialImageLabels.Labels["middleware-version"]
165+
_, _ = fmt.Fprintf(GinkgoWriter, "Initial middleware-version label: '%s' (expected empty for v1.20.1)\n",
166+
initialMiddlewareVersion)
167+
168+
// Create a Function resource
169+
fn := &functionsdevv1alpha1.Function{
170+
ObjectMeta: metav1.ObjectMeta{
171+
GenerateName: "my-function-",
172+
Namespace: functionNamespace,
173+
},
174+
Spec: functionsdevv1alpha1.FunctionSpec{
175+
Source: functionsdevv1alpha1.FunctionSpecSource{
176+
RepositoryURL: repoURL,
177+
},
178+
Registry: functionsdevv1alpha1.FunctionSpecRegistry{
179+
Path: registry,
180+
Insecure: registryInsecure,
181+
},
182+
},
183+
}
184+
185+
err = k8sClient.Create(ctx, fn)
186+
Expect(err).NotTo(HaveOccurred())
187+
188+
functionName = fn.Name
189+
190+
funcBecomeReady := func(g Gomega) {
191+
f := &functionsdevv1alpha1.Function{}
192+
err := k8sClient.Get(ctx, types.NamespacedName{Name: fn.Name, Namespace: fn.Namespace}, f)
193+
g.Expect(err).NotTo(HaveOccurred())
194+
195+
for _, cond := range f.Status.Conditions {
196+
if cond.Type == functionsdevv1alpha1.TypeReady {
197+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
198+
return
199+
}
200+
}
201+
g.Expect(false).To(BeTrue(), "Ready condition not found")
202+
}
203+
204+
// Middleware update could take a bit longer therefore give more time
205+
Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed())
206+
207+
// Verify middleware was actually updated by inspecting the new image
208+
out, err = utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml")
209+
Expect(err).NotTo(HaveOccurred())
210+
211+
var updatedInstance funcfn.Instance
212+
err = yaml.Unmarshal([]byte(out), &updatedInstance)
213+
Expect(err).NotTo(HaveOccurred())
214+
215+
updatedImage := updatedInstance.Image
216+
Expect(updatedImage).NotTo(BeEmpty(), "Updated image should be available from func describe")
217+
_, _ = fmt.Fprintf(GinkgoWriter, "Updated image (redeployed by operator): %s\n", updatedImage)
218+
219+
// Verify the image actually changed
220+
Expect(updatedImage).NotTo(Equal(initialImage), "Image should have changed after operator redeploy")
221+
222+
// Verify updated image has middleware-version label set
223+
updatedImageLocal := strings.Replace(updatedImage, "kind-registry:5000", "localhost:5001", 1)
224+
// Remove tag if both tag and digest are present (skopeo doesn't support this format)
225+
// Format: registry/name:tag@digest -> registry/name@digest
226+
if strings.Contains(updatedImageLocal, "@") {
227+
atIndex := strings.Index(updatedImageLocal, "@")
228+
slashIndex := strings.LastIndex(updatedImageLocal[:atIndex], "/")
229+
if slashIndex != -1 {
230+
// Check if there's a colon between last slash and @
231+
betweenSlashAndAt := updatedImageLocal[slashIndex+1 : atIndex]
232+
if strings.Contains(betweenSlashAndAt, ":") {
233+
// Remove the :tag part
234+
colonIndex := strings.Index(betweenSlashAndAt, ":")
235+
updatedImageLocal = updatedImageLocal[:slashIndex+1+colonIndex] + updatedImageLocal[atIndex:]
236+
}
237+
}
238+
}
239+
cmd = exec.Command("skopeo", "inspect", "--tls-verify=false", "--no-tags", "docker://"+updatedImageLocal)
240+
skopeoOutput, err = utils.Run(cmd)
241+
Expect(err).NotTo(HaveOccurred())
242+
243+
var updatedImageLabels struct {
244+
Labels map[string]string `json:"Labels"`
245+
}
246+
err = json.Unmarshal([]byte(skopeoOutput), &updatedImageLabels)
247+
Expect(err).NotTo(HaveOccurred())
248+
249+
updatedMiddlewareVersion := updatedImageLabels.Labels["middleware-version"]
250+
_, _ = fmt.Fprintf(GinkgoWriter, "Updated middleware-version label: '%s'\n", updatedMiddlewareVersion)
251+
252+
// The operator should have set a middleware version
253+
Expect(updatedMiddlewareVersion).NotTo(BeEmpty(), "Operator should have deployed with middleware-version label set")
254+
255+
// Verify MiddlewareUpToDate condition is True
256+
updatedFunction := &functionsdevv1alpha1.Function{}
257+
err = k8sClient.Get(ctx, types.NamespacedName{Name: fn.Name, Namespace: fn.Namespace}, updatedFunction)
258+
Expect(err).NotTo(HaveOccurred())
259+
260+
var middlewareUpToDate bool
261+
for _, cond := range updatedFunction.Status.Conditions {
262+
if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate {
263+
middlewareUpToDate = cond.Status == metav1.ConditionTrue
264+
_, _ = fmt.Fprintf(GinkgoWriter, "MiddlewareUpToDate condition: status=%s, reason=%s\n",
265+
cond.Status, cond.Reason)
266+
break
267+
}
268+
}
269+
Expect(middlewareUpToDate).To(BeTrue(), "MiddlewareUpToDate condition should be True")
270+
})
271+
})
272+
})

0 commit comments

Comments
 (0)