Skip to content

Commit 92016c7

Browse files
feat: adding support digest pinning for flux plugin install
Signed-off-by: iam-karan-suresh <karansuresh.info@gmail.com>
1 parent 4e78a9d commit 92016c7

5 files changed

Lines changed: 159 additions & 11 deletions

File tree

cmd/flux/plugin.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ func parseNameVersion(s string) (string, string) {
9898
return s, ""
9999
}
100100

101+
// isDigestRef reports whether ref is a content-addressable digest
102+
// (e.g. "sha256:06e0a38...").
103+
func isDigestRef(ref string) bool {
104+
for _, prefix := range []string{"sha256:", "sha384:", "sha512:"} {
105+
if strings.HasPrefix(ref, prefix) {
106+
return true
107+
}
108+
}
109+
return false
110+
}
111+
101112
// newCatalogClient creates a CatalogClient that respects FLUXCD_PLUGIN_CATALOG.
102113
func newCatalogClient() *plugin.CatalogClient {
103114
client := plugin.NewCatalogClient()

cmd/flux/plugin_install.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import (
2323
"github.com/spf13/cobra"
2424

2525
"github.com/fluxcd/flux2/v2/internal/plugin"
26+
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
2627
)
2728

2829
var pluginInstallCmd = &cobra.Command{
29-
Use: "install <name>[@<version>]",
30+
Use: "install <name>[@<version>|@<digest>]",
3031
Short: "Install a plugin from the catalog",
3132
Long: `The plugin install command downloads and installs a plugin from the Flux plugin catalog.
3233
@@ -35,7 +36,10 @@ Examples:
3536
flux plugin install operator
3637
3738
# Install a specific version
38-
flux plugin install operator@0.45.0`,
39+
flux plugin install operator@0.45.0
40+
41+
# Install pinned to a specific digest
42+
flux plugin install operator@sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a`,
3943
Args: cobra.ExactArgs(1),
4044
RunE: pluginInstallCmdRun,
4145
}
@@ -46,22 +50,34 @@ func init() {
4650

4751
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
4852
nameVersion := args[0]
49-
name, version := parseNameVersion(nameVersion)
53+
name, ref := parseNameVersion(nameVersion)
5054

5155
catalogClient := newCatalogClient()
5256
manifest, err := catalogClient.FetchManifest(name)
5357
if err != nil {
5458
return err
5559
}
5660

57-
pv, err := plugin.ResolveVersion(manifest, version)
58-
if err != nil {
59-
return err
60-
}
61-
62-
plat, err := plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
63-
if err != nil {
64-
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
61+
var pv *plugintypes.Version
62+
var plat *plugintypes.Platform
63+
64+
if isDigestRef(ref) {
65+
dm, err := plugin.ResolveByDigest(manifest, ref, runtime.GOOS, runtime.GOARCH)
66+
if err != nil {
67+
return err
68+
}
69+
pv = dm.Version
70+
plat = dm.Platform
71+
} else {
72+
pv, err = plugin.ResolveVersion(manifest, ref)
73+
if err != nil {
74+
return err
75+
}
76+
77+
plat, err = plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
78+
if err != nil {
79+
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
80+
}
6581
}
6682

6783
pluginDir := pluginHandler.EnsurePluginDir()

cmd/flux/plugin_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ func TestParseNameVersion(t *testing.T) {
211211
{"operator@0.45.0", "operator", "0.45.0"},
212212
{"my-tool@1.0.0", "my-tool", "1.0.0"},
213213
{"plugin@", "plugin", ""},
214+
{"operator@sha256:abc123", "operator", "sha256:abc123"},
214215
}
215216

216217
for _, tt := range tests {
@@ -226,6 +227,29 @@ func TestParseNameVersion(t *testing.T) {
226227
}
227228
}
228229

230+
func TestIsDigestRef(t *testing.T) {
231+
tests := []struct {
232+
input string
233+
want bool
234+
}{
235+
{"sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a", true},
236+
{"sha384:abc123", true},
237+
{"sha512:def456", true},
238+
{"0.45.0", false},
239+
{"", false},
240+
{"sha256", false},
241+
{"SHA256:abc", false}, // case-sensitive
242+
}
243+
244+
for _, tt := range tests {
245+
t.Run(tt.input, func(t *testing.T) {
246+
if got := isDigestRef(tt.input); got != tt.want {
247+
t.Errorf("isDigestRef(%q) = %v, want %v", tt.input, got, tt.want)
248+
}
249+
})
250+
}
251+
}
252+
229253
func TestPluginDiscoverSkipsBuiltins(t *testing.T) {
230254
origHandler := pluginHandler
231255
defer func() { pluginHandler = origHandler }()

internal/plugin/catalog.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,33 @@ func ResolvePlatform(pv *plugintypes.Version, goos, goarch string) (*plugintypes
165165

166166
return nil, fmt.Errorf("no binary for %s/%s", goos, goarch)
167167
}
168+
169+
// DigestMatch holds the version and platform resolved from a digest lookup.
170+
type DigestMatch struct {
171+
Version *plugintypes.Version
172+
Platform *plugintypes.Platform
173+
}
174+
175+
// ResolveByDigest scans all versions and platforms for a checksum matching
176+
// digest. The digest must be in "algorithm:hex" format (e.g.
177+
// "sha256:06e0a38..."). Only platforms matching goos/goarch are considered.
178+
// Returns the first match (versions are ordered newest-first in the manifest).
179+
func ResolveByDigest(manifest *plugintypes.Manifest, digest, goos, goarch string) (*DigestMatch, error) {
180+
if len(manifest.Versions) == 0 {
181+
return nil, fmt.Errorf("plugin %q has no versions", manifest.Name)
182+
}
183+
184+
for i := range manifest.Versions {
185+
for j := range manifest.Versions[i].Platforms {
186+
p := &manifest.Versions[i].Platforms[j]
187+
if p.OS == goos && p.Arch == goarch && p.Checksum == digest {
188+
return &DigestMatch{
189+
Version: &manifest.Versions[i],
190+
Platform: p,
191+
}, nil
192+
}
193+
}
194+
}
195+
196+
return nil, fmt.Errorf("digest %q not found for plugin %q on %s/%s", digest, manifest.Name, goos, goarch)
197+
}

internal/plugin/catalog_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,70 @@ func TestResolvePlatform(t *testing.T) {
239239
}
240240
})
241241
}
242+
243+
func TestResolveByDigest(t *testing.T) {
244+
manifest := &plugintypes.Manifest{
245+
Name: "operator",
246+
Versions: []plugintypes.Version{
247+
{
248+
Version: "0.46.0",
249+
Platforms: []plugintypes.Platform{
250+
{OS: "linux", Arch: "amd64", URL: "https://example.com/v46_linux.tar.gz", Checksum: "sha256:aaaa"},
251+
{OS: "darwin", Arch: "arm64", URL: "https://example.com/v46_darwin.tar.gz", Checksum: "sha256:bbbb"},
252+
},
253+
},
254+
{
255+
Version: "0.45.0",
256+
Platforms: []plugintypes.Platform{
257+
{OS: "linux", Arch: "amd64", URL: "https://example.com/v45_linux.tar.gz", Checksum: "sha256:cccc"},
258+
{OS: "darwin", Arch: "arm64", URL: "https://example.com/v45_darwin.tar.gz", Checksum: "sha256:dddd"},
259+
},
260+
},
261+
},
262+
}
263+
264+
t.Run("found in latest version", func(t *testing.T) {
265+
dm, err := ResolveByDigest(manifest, "sha256:aaaa", "linux", "amd64")
266+
if err != nil {
267+
t.Fatalf("unexpected error: %v", err)
268+
}
269+
if dm.Version.Version != "0.46.0" {
270+
t.Errorf("expected version '0.46.0', got %q", dm.Version.Version)
271+
}
272+
if dm.Platform.Checksum != "sha256:aaaa" {
273+
t.Errorf("expected checksum 'sha256:aaaa', got %q", dm.Platform.Checksum)
274+
}
275+
})
276+
277+
t.Run("found in older version", func(t *testing.T) {
278+
dm, err := ResolveByDigest(manifest, "sha256:cccc", "linux", "amd64")
279+
if err != nil {
280+
t.Fatalf("unexpected error: %v", err)
281+
}
282+
if dm.Version.Version != "0.45.0" {
283+
t.Errorf("expected version '0.45.0', got %q", dm.Version.Version)
284+
}
285+
})
286+
287+
t.Run("wrong platform", func(t *testing.T) {
288+
// sha256:bbbb exists for darwin/arm64, not linux/amd64.
289+
_, err := ResolveByDigest(manifest, "sha256:bbbb", "linux", "amd64")
290+
if err == nil {
291+
t.Fatal("expected error, got nil")
292+
}
293+
})
294+
295+
t.Run("not found", func(t *testing.T) {
296+
_, err := ResolveByDigest(manifest, "sha256:nonexistent", "linux", "amd64")
297+
if err == nil {
298+
t.Fatal("expected error, got nil")
299+
}
300+
})
301+
302+
t.Run("no versions", func(t *testing.T) {
303+
_, err := ResolveByDigest(&plugintypes.Manifest{Name: "empty"}, "sha256:abc", "linux", "amd64")
304+
if err == nil {
305+
t.Fatal("expected error, got nil")
306+
}
307+
})
308+
}

0 commit comments

Comments
 (0)