Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 0be0a4c

Browse files
authored
Prevent Acorn from assuming Docker Hub for auto-upgrade apps with no specified registry (#1427) (#1823)
Signed-off-by: Grant Linville <[email protected]>
1 parent 7195323 commit 0be0a4c

File tree

11 files changed

+197
-9
lines changed

11 files changed

+197
-9
lines changed

integration/client/images/images_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ func TestImageDetails(t *testing.T) {
325325
}
326326

327327
assert.True(t, strings.Contains(details.AppImage.Acornfile, "nginx"))
328+
329+
// Test an auto-upgrade pattern that matches no local images, and make sure the proper error is returned
330+
_, err = c.ImageDetails(ctx, "dne:v#.#.#", nil)
331+
if err == nil {
332+
t.Fatal("expected error for auto-upgrade pattern that matches no local images")
333+
}
334+
assert.ErrorContains(t, err, "unable to find an image for dne:v#.#.# matching pattern v#.#.# - if you are trying to use a remote image, specify the full registry")
328335
}
329336

330337
func TestImageDeleteTwoTags(t *testing.T) {

integration/run/run_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,3 +1662,39 @@ func TestEnforcedQuota(t *testing.T) {
16621662
return obj.Status.Condition(v1.AppInstanceConditionQuotaAllocated).Success
16631663
})
16641664
}
1665+
1666+
func TestAutoUpgradeImageValidation(t *testing.T) {
1667+
ctx := helper.GetCTX(t)
1668+
1669+
helper.StartController(t)
1670+
restConfig, err := restconfig.New(scheme.Scheme)
1671+
if err != nil {
1672+
t.Fatal("error while getting rest config:", err)
1673+
}
1674+
kclient := helper.MustReturn(kclient.Default)
1675+
project := helper.TempProject(t, kclient)
1676+
1677+
c, err := client.New(restConfig, project.Name, project.Name)
1678+
if err != nil {
1679+
t.Fatal(err)
1680+
}
1681+
1682+
app, err := c.AppRun(ctx, "ghcr.io/acorn-io/library/nginx:latest", &client.AppRunOptions{
1683+
Name: "myapp",
1684+
AutoUpgrade: &[]bool{true}[0],
1685+
})
1686+
if err != nil {
1687+
t.Fatal(err)
1688+
}
1689+
1690+
// Attempt to update the app to "myimage:latest".
1691+
// Since no image exists with this tag, it should fail.
1692+
// Auto-upgrade apps are not supposed to implicitly use Docker Hub when no registry is specified.
1693+
_, err = c.AppUpdate(ctx, app.Name, &client.AppUpdateOptions{
1694+
Image: "myimage:latest",
1695+
})
1696+
if err == nil {
1697+
t.Fatal("expected error when failing to find local image for auto-upgrade app, got no error")
1698+
}
1699+
assert.ErrorContains(t, err, "could not find local image for myimage:latest - if you are trying to use a remote image, specify the full registry")
1700+
}

pkg/controller/appdefinition/pullappimage.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/acorn-io/runtime/pkg/event"
1414
"github.com/acorn-io/runtime/pkg/images"
1515
"github.com/acorn-io/runtime/pkg/tags"
16+
imagename "github.com/google/go-containerregistry/pkg/name"
1617
"github.com/google/go-containerregistry/pkg/v1/remote"
1718
"github.com/sirupsen/logrus"
1819
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -56,9 +57,10 @@ func pullAppImage(transport http.RoundTripper, client pullClient) router.Handler
5657

5758
// Skip the attempt to locally resolve if we already know that the image will be remote
5859
var (
59-
resolved string
60-
err error
61-
isLocal bool
60+
_, autoUpgradeOn = autoupgrade.Mode(appInstance.Spec)
61+
resolved string
62+
err error
63+
isLocal bool
6264
)
6365
if !appInstance.Status.AvailableAppImageRemote {
6466
resolved, isLocal, err = client.resolve(req.Ctx, req.Client, appInstance.Namespace, target)
@@ -67,6 +69,17 @@ func pullAppImage(transport http.RoundTripper, client pullClient) router.Handler
6769
return nil
6870
}
6971
if !isLocal {
72+
if autoUpgradeOn && !tags.IsLocalReference(target) {
73+
ref, err := imagename.ParseReference(target, imagename.WithDefaultRegistry(images.NoDefaultRegistry))
74+
if err != nil {
75+
return err
76+
}
77+
if ref.Context().RegistryStr() == images.NoDefaultRegistry {
78+
// Prevent this from being resolved remotely, as we should never assume Docker Hub for auto-upgrade apps
79+
return fmt.Errorf("no local image found for %v - if you are trying to use a remote image, specify the full registry", target)
80+
}
81+
}
82+
7083
// Force pull from remote, since the only local image we found was marked remote, and there might be a newer version
7184
resolved = target
7285
}
@@ -75,9 +88,8 @@ func pullAppImage(transport http.RoundTripper, client pullClient) router.Handler
7588
}
7689

7790
var (
78-
_, autoUpgradeOn = autoupgrade.Mode(appInstance.Spec)
79-
previousImage = appInstance.Status.AppImage
80-
targetImage *v1.AppImage
91+
previousImage = appInstance.Status.AppImage
92+
targetImage *v1.AppImage
8193
)
8294
defer func() {
8395
// Record the results as an event

pkg/controller/appdefinition/pullappimage_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package appdefinition
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67
"testing"
78

9+
"github.com/acorn-io/baaah/pkg/router"
810
"github.com/acorn-io/baaah/pkg/router/tester"
911
apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
1012
v1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
1113
"github.com/acorn-io/runtime/pkg/event"
1214
"github.com/acorn-io/runtime/pkg/scheme"
15+
"github.com/acorn-io/runtime/pkg/tags"
1316
"github.com/google/go-containerregistry/pkg/v1/remote"
1417
"github.com/stretchr/testify/assert"
1518
"github.com/stretchr/testify/require"
@@ -270,3 +273,49 @@ func testRecordPullEvent(t *testing.T, testName string, appInstance *v1.AppInsta
270273
assert.EqualValues(t, expect, recording[0])
271274
})
272275
}
276+
277+
func TestAutoUpgradeImageResolution(t *testing.T) {
278+
// Auto-upgrade apps are not supposed to use Docker Hub implicitly.
279+
// In this test, we create an auto-upgrade App with the image "myimage:latest".
280+
// In the first test case, this image exists locally and should be resolved properly.
281+
// In the second test case, no such image exists locally, and Acorn should not reach out to Docker Hub, and should instead return an error.
282+
283+
fakeRecorder := func(_ context.Context, _ *apiv1.Event) error {
284+
return nil
285+
}
286+
287+
// First, test to make sure that the local image is properly resolved
288+
tester.DefaultTest(t, scheme.Scheme, "testdata/autoupgrade/with-local-image", testPullAppImage(mockRoundTripper{}, event.RecorderFunc(fakeRecorder)))
289+
290+
// Next, test to make sure that Docker Hub is not implicitly used when no local image is found
291+
// There should be a helpful error message instead
292+
harness, obj, err := tester.FromDir(scheme.Scheme, "testdata/autoupgrade/without-local-image")
293+
if err != nil {
294+
t.Fatal(err)
295+
}
296+
_, err = harness.InvokeFunc(t, obj, testPullAppImage(mockRoundTripper{}, event.RecorderFunc(fakeRecorder)))
297+
if err == nil {
298+
t.Fatalf("expected error when no local image was found for auto-upgrade app without a specified registry")
299+
}
300+
assert.ErrorContains(t, err, "no local image found for myimage:latest - if you are trying to use a remote image, specify the full registry")
301+
}
302+
303+
func testPullAppImage(transport http.RoundTripper, recorder event.Recorder) router.HandlerFunc {
304+
return pullAppImage(transport, pullClient{
305+
recorder: recorder,
306+
resolve: tags.ResolveLocal,
307+
pull: func(_ context.Context, _ kclient.Reader, _ string, _ string, _ string, _ ...remote.Option) (*v1.AppImage, error) {
308+
return &v1.AppImage{
309+
Name: "myimage:latest",
310+
Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
311+
}, nil
312+
},
313+
now: metav1.NowMicro,
314+
})
315+
}
316+
317+
type mockRoundTripper struct{}
318+
319+
func (m mockRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
320+
return nil, nil
321+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
apiVersion: api.acorn.io/v1
3+
kind: Image
4+
metadata:
5+
name: myimage:latest
6+
namespace: app-namespace
7+
digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
8+
tags:
9+
- myimage:latest
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
`apiVersion: internal.acorn.io/v1
2+
kind: AppInstance
3+
metadata:
4+
creationTimestamp: null
5+
name: app-name
6+
namespace: app-namespace
7+
uid: 1234567890abcdef
8+
spec:
9+
autoUpgrade: true
10+
image: myimage:latest
11+
status:
12+
appImage:
13+
digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
14+
imageData: {}
15+
name: myimage:latest
16+
vcs: {}
17+
appSpec: {}
18+
appStatus: {}
19+
columns: {}
20+
conditions:
21+
reason: Success
22+
status: "True"
23+
success: true
24+
type: image-pull
25+
defaults: {}
26+
namespace: app-created-namespace
27+
`
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
apiVersion: internal.acorn.io/v1
3+
kind: AppInstance
4+
metadata:
5+
name: app-name
6+
namespace: app-namespace
7+
uid: 1234567890abcdef
8+
spec:
9+
autoUpgrade: true
10+
image: myimage:latest
11+
status:
12+
namespace: app-created-namespace
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
apiVersion: internal.acorn.io/v1
3+
kind: AppInstance
4+
metadata:
5+
name: app-name
6+
namespace: app-namespace
7+
uid: 1234567890abcdef
8+
spec:
9+
autoUpgrade: true
10+
image: myimage:latest
11+
status:
12+
namespace: app-created-namespace

pkg/imagedetails/imagedetails.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/acorn-io/runtime/pkg/autoupgrade"
1111
"github.com/acorn-io/runtime/pkg/images"
1212
"github.com/acorn-io/runtime/pkg/tags"
13+
imagename "github.com/google/go-containerregistry/pkg/name"
1314
"github.com/google/go-containerregistry/pkg/v1/remote"
1415
apierror "k8s.io/apimachinery/pkg/api/errors"
1516
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,6 +25,13 @@ func GetImageDetails(ctx context.Context, c kclient.Client, namespace, imageName
2425
if latestImage, found, err := autoupgrade.FindLatestTagForImageWithPattern(ctx, c, "", namespace, imageName, tagPattern); err != nil {
2526
return nil, err
2627
} else if !found {
28+
// Check and see if no registry was specified on the image.
29+
// If this is the case, notify the user that they need to explicitly specify docker.io if that is what they are trying to use.
30+
ref, err := imagename.ParseReference(strings.TrimSuffix(imageName, ":"+tagPattern), imagename.WithDefaultRegistry(images.NoDefaultRegistry))
31+
if err == nil && ref.Context().Registry.Name() == images.NoDefaultRegistry {
32+
return nil, fmt.Errorf("unable to find an image for %v matching pattern %v - if you are trying to use a remote image, specify the full registry", imageName, tagPattern)
33+
}
34+
2735
return nil, fmt.Errorf("unable to find an image for %v matching pattern %v", imageName, tagPattern)
2836
} else {
2937
imageName = latestImage

pkg/images/operations.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ func PullAppImage(ctx context.Context, c client.Reader, namespace, image, nested
8787
return appImage, nil
8888
}
8989

90-
const DefaultRegistry = "NO_DEFAULT"
90+
const NoDefaultRegistry = "xxx-no-reg"
9191

9292
func ParseReferenceNoDefault(name string) (imagename.Reference, error) {
93-
ref, err := imagename.ParseReference(name, imagename.WithDefaultRegistry(DefaultRegistry))
93+
ref, err := imagename.ParseReference(name, imagename.WithDefaultRegistry(NoDefaultRegistry))
9494
if err != nil {
9595
return nil, err
9696
}
9797

98-
if ref.Context().RegistryStr() == DefaultRegistry {
98+
if ref.Context().RegistryStr() == NoDefaultRegistry {
9999
return nil, fmt.Errorf("missing registry host in the tag [%s] (ie ghcr.io or docker.io)", name)
100100
}
101101
return ref, nil

0 commit comments

Comments
 (0)