From 5256361d8cf80af9f723a580b46b70425ea1ca0b Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 14 Apr 2026 00:02:13 +0300 Subject: [PATCH] Make plugin types public Signed-off-by: Stefan Prodan --- internal/plugin/catalog.go | 138 ++++---------------------------- internal/plugin/catalog_test.go | 12 +-- internal/plugin/install.go | 12 +-- internal/plugin/install_test.go | 38 ++++----- internal/plugin/update.go | 10 ++- pkg/plugin/types.go | 135 +++++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 153 deletions(-) create mode 100644 pkg/plugin/types.go diff --git a/internal/plugin/catalog.go b/internal/plugin/catalog.go index 1d370a81..019bfada 100644 --- a/internal/plugin/catalog.go +++ b/internal/plugin/catalog.go @@ -24,122 +24,16 @@ import ( "github.com/hashicorp/go-retryablehttp" "sigs.k8s.io/yaml" + + plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin" ) const ( // defaultCatalogBase points at the latest GitHub release of fluxcd/plugins. defaultCatalogBase = "https://github.com/fluxcd/plugins/releases/latest/download/" envCatalogBase = "FLUXCD_PLUGIN_CATALOG" - - pluginAPIVersion = "cli.fluxcd.io/v1beta1" - pluginKind = "Plugin" - catalogKind = "PluginCatalog" ) -// PluginManifest represents a single plugin's manifest from the catalog. -type PluginManifest struct { - // APIVersion is the manifest schema version (e.g. "cli.fluxcd.io/v1beta1"). - APIVersion string `json:"apiVersion"` - - // Kind is the manifest type, must be "Plugin". - Kind string `json:"kind"` - - // Name is the plugin name used in "flux " invocations. - Name string `json:"name"` - - // Description is a short human-readable summary of the plugin. - Description string `json:"description"` - - // Homepage is the URL to the plugin's documentation site. - Homepage string `json:"homepage,omitempty"` - - // Source is the URL to the plugin's source repository. - Source string `json:"source,omitempty"` - - // Bin is the binary name inside archives and the installed filename - // (e.g. "flux-operator"). On Windows ".exe" is appended automatically. - Bin string `json:"bin"` - - // Versions lists available versions, newest first. - Versions []PluginVersion `json:"versions"` -} - -// PluginVersion represents a version entry in a plugin manifest. -type PluginVersion struct { - // Version is the semantic version string (e.g. "0.45.0"). - Version string `json:"version"` - - // Platforms lists the platform-specific binaries for this version. - Platforms []PluginPlatform `json:"platforms"` -} - -// PluginPlatform represents a platform-specific binary entry. -type PluginPlatform struct { - // OS is the target operating system (e.g. "darwin", "linux", "windows"). - OS string `json:"os"` - - // Arch is the target architecture (e.g. "amd64", "arm64"). - Arch string `json:"arch"` - - // URL is the download URL for the archive or binary. - URL string `json:"url"` - - // Checksum is the expected digest in ":" format - // (e.g. "sha256:cd85d5d84d264..."). - Checksum string `json:"checksum"` - - // ExtractPath overrides the default binary lookup name inside archives. - // When set, it is matched as an exact path within the archive (e.g. - // "bin/flux-operator"). When empty, the archive is searched by the - // base name derived from the manifest's Bin field. - ExtractPath string `json:"extractPath,omitempty"` -} - -// PluginCatalog represents the generated catalog.yaml file. -type PluginCatalog struct { - // APIVersion is the catalog schema version (e.g. "cli.fluxcd.io/v1beta1"). - APIVersion string `json:"apiVersion"` - - // Kind is the catalog type, must be "PluginCatalog". - Kind string `json:"kind"` - - // Plugins lists all available plugins in the catalog. - Plugins []CatalogEntry `json:"plugins"` -} - -// CatalogEntry is a single entry in the plugin catalog. -type CatalogEntry struct { - // Name is the plugin name. - Name string `json:"name"` - - // Description is a short human-readable summary of the plugin. - Description string `json:"description"` - - // Homepage is the URL to the plugin's documentation site. - Homepage string `json:"homepage,omitempty"` - - // Source is the URL to the plugin's source repository. - Source string `json:"source,omitempty"` - - // License is the SPDX license identifier (e.g. "Apache-2.0"). - License string `json:"license,omitempty"` -} - -// Receipt records what was installed for a plugin. -type Receipt struct { - // Name is the plugin name (e.g. "operator"). - Name string `json:"name"` - - // Version is the installed semantic version. - Version string `json:"version"` - - // InstalledAt is the RFC 3339 timestamp of the installation. - InstalledAt string `json:"installedAt"` - - // Platform records the platform-specific details used for installation. - Platform PluginPlatform `json:"platform"` -} - // CatalogClient fetches plugin manifests and catalogs from a remote URL. type CatalogClient struct { // BaseURL is the catalog base URL for fetching manifests. @@ -170,46 +64,46 @@ func (c *CatalogClient) baseURL() string { } // FetchManifest fetches a single plugin manifest from the catalog. -func (c *CatalogClient) FetchManifest(name string) (*PluginManifest, error) { +func (c *CatalogClient) FetchManifest(name string) (*plugintypes.Manifest, error) { url := c.baseURL() + name + ".yaml" body, err := c.fetch(url) if err != nil { return nil, fmt.Errorf("plugin %q not found in catalog", name) } - var manifest PluginManifest + var manifest plugintypes.Manifest if err := yaml.Unmarshal(body, &manifest); err != nil { return nil, fmt.Errorf("failed to parse plugin manifest for %q: %w", name, err) } - if manifest.APIVersion != pluginAPIVersion { - return nil, fmt.Errorf("plugin %q has unsupported apiVersion %q (expected %q)", name, manifest.APIVersion, pluginAPIVersion) + if manifest.APIVersion != plugintypes.APIVersion { + return nil, fmt.Errorf("plugin %q has unsupported apiVersion %q (expected %q)", name, manifest.APIVersion, plugintypes.APIVersion) } - if manifest.Kind != pluginKind { - return nil, fmt.Errorf("plugin %q has unexpected kind %q (expected %q)", name, manifest.Kind, pluginKind) + if manifest.Kind != plugintypes.PluginKind { + return nil, fmt.Errorf("plugin %q has unexpected kind %q (expected %q)", name, manifest.Kind, plugintypes.PluginKind) } return &manifest, nil } // FetchCatalog fetches the generated catalog.yaml. -func (c *CatalogClient) FetchCatalog() (*PluginCatalog, error) { +func (c *CatalogClient) FetchCatalog() (*plugintypes.Catalog, error) { url := c.baseURL() + "catalog.yaml" body, err := c.fetch(url) if err != nil { return nil, fmt.Errorf("failed to fetch plugin catalog: %w", err) } - var catalog PluginCatalog + var catalog plugintypes.Catalog if err := yaml.Unmarshal(body, &catalog); err != nil { return nil, fmt.Errorf("failed to parse plugin catalog: %w", err) } - if catalog.APIVersion != pluginAPIVersion { - return nil, fmt.Errorf("plugin catalog has unsupported apiVersion %q (expected %q)", catalog.APIVersion, pluginAPIVersion) + if catalog.APIVersion != plugintypes.APIVersion { + return nil, fmt.Errorf("plugin catalog has unsupported apiVersion %q (expected %q)", catalog.APIVersion, plugintypes.APIVersion) } - if catalog.Kind != catalogKind { - return nil, fmt.Errorf("plugin catalog has unexpected kind %q (expected %q)", catalog.Kind, catalogKind) + if catalog.Kind != plugintypes.CatalogKind { + return nil, fmt.Errorf("plugin catalog has unexpected kind %q (expected %q)", catalog.Kind, plugintypes.CatalogKind) } return &catalog, nil @@ -243,7 +137,7 @@ func newHTTPClient(timeout time.Duration) *http.Client { // ResolveVersion finds the requested version in the manifest. // If version is empty, returns the first (latest) version. -func ResolveVersion(manifest *PluginManifest, version string) (*PluginVersion, error) { +func ResolveVersion(manifest *plugintypes.Manifest, version string) (*plugintypes.Version, error) { if len(manifest.Versions) == 0 { return nil, fmt.Errorf("plugin %q has no versions", manifest.Name) } @@ -262,7 +156,7 @@ func ResolveVersion(manifest *PluginManifest, version string) (*PluginVersion, e } // ResolvePlatform finds the platform entry matching the given OS and arch. -func ResolvePlatform(pv *PluginVersion, goos, goarch string) (*PluginPlatform, error) { +func ResolvePlatform(pv *plugintypes.Version, goos, goarch string) (*plugintypes.Platform, error) { for i := range pv.Platforms { if pv.Platforms[i].OS == goos && pv.Platforms[i].Arch == goarch { return &pv.Platforms[i], nil diff --git a/internal/plugin/catalog_test.go b/internal/plugin/catalog_test.go index 8134a010..e6134fc6 100644 --- a/internal/plugin/catalog_test.go +++ b/internal/plugin/catalog_test.go @@ -20,6 +20,8 @@ import ( "net/http" "net/http/httptest" "testing" + + plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin" ) func TestFetchManifest(t *testing.T) { @@ -168,9 +170,9 @@ plugins: [] } func TestResolveVersion(t *testing.T) { - manifest := &PluginManifest{ + manifest := &plugintypes.Manifest{ Name: "operator", - Versions: []PluginVersion{ + Versions: []plugintypes.Version{ {Version: "0.45.0"}, {Version: "0.44.0"}, }, @@ -204,7 +206,7 @@ func TestResolveVersion(t *testing.T) { }) t.Run("no versions", func(t *testing.T) { - _, err := ResolveVersion(&PluginManifest{Name: "empty"}, "") + _, err := ResolveVersion(&plugintypes.Manifest{Name: "empty"}, "") if err == nil { t.Fatal("expected error, got nil") } @@ -212,9 +214,9 @@ func TestResolveVersion(t *testing.T) { } func TestResolvePlatform(t *testing.T) { - pv := &PluginVersion{ + pv := &plugintypes.Version{ Version: "0.45.0", - Platforms: []PluginPlatform{ + Platforms: []plugintypes.Platform{ {OS: "darwin", Arch: "arm64", URL: "https://example.com/darwin_arm64.tar.gz"}, {OS: "linux", Arch: "amd64", URL: "https://example.com/linux_amd64.tar.gz"}, }, diff --git a/internal/plugin/install.go b/internal/plugin/install.go index 1589e2e2..7b2129e6 100644 --- a/internal/plugin/install.go +++ b/internal/plugin/install.go @@ -31,6 +31,8 @@ import ( "time" "sigs.k8s.io/yaml" + + plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin" ) // Installer handles downloading, verifying, and installing plugins. @@ -48,7 +50,7 @@ func NewInstaller() *Installer { // Install downloads, verifies, extracts, and installs a plugin binary // to the given plugin directory. -func (inst *Installer) Install(pluginDir string, manifest *PluginManifest, pv *PluginVersion, plat *PluginPlatform) error { +func (inst *Installer) Install(pluginDir string, manifest *plugintypes.Manifest, pv *plugintypes.Version, plat *plugintypes.Platform) error { tmpFile, err := os.CreateTemp("", "flux-plugin-*") if err != nil { return fmt.Errorf("failed to create temp file: %w", err) @@ -118,7 +120,7 @@ func (inst *Installer) Install(pluginDir string, manifest *PluginManifest, pv *P return err } - receipt := Receipt{ + receipt := plugintypes.Receipt{ Name: manifest.Name, Version: pv.Version, InstalledAt: time.Now().UTC().Format(time.RFC3339), @@ -156,13 +158,13 @@ func Uninstall(pluginDir, name string) error { // ReadReceipt reads the install receipt for a plugin. // Returns nil if no receipt exists. -func ReadReceipt(pluginDir, name string) *Receipt { +func ReadReceipt(pluginDir, name string) *plugintypes.Receipt { data, err := os.ReadFile(receiptPath(pluginDir, name)) if err != nil { return nil } - var receipt Receipt + var receipt plugintypes.Receipt if err := yaml.Unmarshal(data, &receipt); err != nil { return nil } @@ -174,7 +176,7 @@ func receiptPath(pluginDir, name string) string { return filepath.Join(pluginDir, pluginPrefix+name+".yaml") } -func writeReceipt(pluginDir, name string, receipt *Receipt) error { +func writeReceipt(pluginDir, name string, receipt *plugintypes.Receipt) error { data, err := yaml.Marshal(receipt) if err != nil { return fmt.Errorf("failed to marshal receipt: %w", err) diff --git a/internal/plugin/install_test.go b/internal/plugin/install_test.go index 43fb2845..dc2dd587 100644 --- a/internal/plugin/install_test.go +++ b/internal/plugin/install_test.go @@ -31,6 +31,8 @@ import ( "runtime" "strings" "testing" + + plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin" ) // createTestTarGz creates a tar.gz archive containing a single file. @@ -164,12 +166,12 @@ func TestInstall(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{ + manifest := &plugintypes.Manifest{ Name: "operator", Bin: "flux-operator", } - pv := &PluginVersion{Version: "0.45.0"} - plat := &PluginPlatform{ + pv := &plugintypes.Version{Version: "0.45.0"} + plat := &plugintypes.Platform{ OS: "linux", Arch: "amd64", URL: server.URL + "/flux-operator_0.45.0_linux_amd64.tar.gz", @@ -218,9 +220,9 @@ func TestInstallChecksumMismatch(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"} - pv := &PluginVersion{Version: "0.45.0"} - plat := &PluginPlatform{ + manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"} + pv := &plugintypes.Version{Version: "0.45.0"} + plat := &plugintypes.Platform{ OS: "linux", Arch: "amd64", URL: server.URL + "/archive.tar.gz", @@ -253,9 +255,9 @@ func TestInstallBinaryNotInArchive(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"} - pv := &PluginVersion{Version: "0.45.0"} - plat := &PluginPlatform{ + manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"} + pv := &plugintypes.Version{Version: "0.45.0"} + plat := &plugintypes.Platform{ OS: "linux", Arch: "amd64", URL: server.URL + "/archive.tar.gz", @@ -396,12 +398,12 @@ func TestInstallRawBinary(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{ + manifest := &plugintypes.Manifest{ Name: "validate", Bin: "flux-validate", } - pv := &PluginVersion{Version: "1.2.3"} - plat := &PluginPlatform{ + pv := &plugintypes.Version{Version: "1.2.3"} + plat := &plugintypes.Platform{ OS: runtime.GOOS, Arch: runtime.GOARCH, // URL filename deliberately differs from manifest.Bin — mimics a @@ -701,9 +703,9 @@ func TestInstallExtractPath(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"} - pv := &PluginVersion{Version: "0.45.0"} - plat := &PluginPlatform{ + manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"} + pv := &plugintypes.Version{Version: "0.45.0"} + plat := &plugintypes.Platform{ OS: "linux", Arch: "amd64", URL: server.URL + "/archive.tar.gz", @@ -746,9 +748,9 @@ func TestInstallExtractPathZip(t *testing.T) { pluginDir := t.TempDir() - manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"} - pv := &PluginVersion{Version: "0.45.0"} - plat := &PluginPlatform{ + manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"} + pv := &plugintypes.Version{Version: "0.45.0"} + plat := &plugintypes.Platform{ OS: "linux", Arch: "amd64", URL: server.URL + "/archive.zip", diff --git a/internal/plugin/update.go b/internal/plugin/update.go index 6895fbef..6ce87e79 100644 --- a/internal/plugin/update.go +++ b/internal/plugin/update.go @@ -16,6 +16,10 @@ limitations under the License. package plugin +import ( + plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin" +) + const ( SkipReasonManual = "manually installed" SkipReasonUpToDate = "already up to date" @@ -41,13 +45,13 @@ type UpdateResult struct { SkipReason string // Manifest is the resolved plugin manifest for the update. - Manifest *PluginManifest + Manifest *plugintypes.Manifest // Version is the resolved target version for the update. - Version *PluginVersion + Version *plugintypes.Version // Platform is the resolved platform entry for the update. - Platform *PluginPlatform + Platform *plugintypes.Platform // Err is set when the update check itself failed. Err error diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go new file mode 100644 index 00000000..e09d4f30 --- /dev/null +++ b/pkg/plugin/types.go @@ -0,0 +1,135 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package plugin defines the public types for the Flux CLI plugin system. +// These types represent the plugin catalog schema (cli.fluxcd.io/v1beta1) +// and are safe for use by external consumers. +package plugin + +const ( + // APIVersion is the plugin manifest schema version. + APIVersion = "cli.fluxcd.io/v1beta1" + + // PluginKind is the kind for plugin manifests. + PluginKind = "Plugin" + + // CatalogKind is the kind for the plugin catalog. + CatalogKind = "PluginCatalog" +) + +// Manifest represents a single plugin's manifest from the catalog. +type Manifest struct { + // APIVersion is the manifest schema version (e.g. "cli.fluxcd.io/v1beta1"). + APIVersion string `json:"apiVersion"` + + // Kind is the manifest type, must be "Plugin". + Kind string `json:"kind"` + + // Name is the plugin name used in "flux " invocations. + Name string `json:"name"` + + // Description is a short human-readable summary of the plugin. + Description string `json:"description"` + + // Homepage is the URL to the plugin's documentation site. + Homepage string `json:"homepage,omitempty"` + + // Source is the URL to the plugin's source repository. + Source string `json:"source,omitempty"` + + // Bin is the binary name inside archives and the installed filename + // (e.g. "flux-operator"). On Windows ".exe" is appended automatically. + Bin string `json:"bin"` + + // Versions lists available versions, newest first. + Versions []Version `json:"versions"` +} + +// Version represents a version entry in a plugin manifest. +type Version struct { + // Version is the semantic version string (e.g. "0.45.0"). + Version string `json:"version"` + + // Platforms lists the platform-specific binaries for this version. + Platforms []Platform `json:"platforms"` +} + +// Platform represents a platform-specific binary entry. +type Platform struct { + // OS is the target operating system (e.g. "darwin", "linux", "windows"). + OS string `json:"os"` + + // Arch is the target architecture (e.g. "amd64", "arm64"). + Arch string `json:"arch"` + + // URL is the download URL for the archive or binary. + URL string `json:"url"` + + // Checksum is the expected digest in ":" format + // (e.g. "sha256:cd85d5d84d264..."). + Checksum string `json:"checksum"` + + // ExtractPath overrides the default binary lookup name inside archives. + // When set, it is matched as an exact path within the archive (e.g. + // "bin/flux-operator"). When empty, the archive is searched by the + // base name derived from the manifest's Bin field. + ExtractPath string `json:"extractPath,omitempty"` +} + +// Catalog represents the generated catalog.yaml file. +type Catalog struct { + // APIVersion is the catalog schema version (e.g. "cli.fluxcd.io/v1beta1"). + APIVersion string `json:"apiVersion"` + + // Kind is the catalog type, must be "PluginCatalog". + Kind string `json:"kind"` + + // Plugins lists all available plugins in the catalog. + Plugins []CatalogEntry `json:"plugins"` +} + +// CatalogEntry is a single entry in the plugin catalog. +type CatalogEntry struct { + // Name is the plugin name. + Name string `json:"name"` + + // Description is a short human-readable summary of the plugin. + Description string `json:"description"` + + // Homepage is the URL to the plugin's documentation site. + Homepage string `json:"homepage"` + + // Source is the URL to the plugin's source repository. + Source string `json:"source"` + + // License is the SPDX license identifier (e.g. "Apache-2.0"). + License string `json:"license"` +} + +// Receipt records what was installed for a plugin. +type Receipt struct { + // Name is the plugin name (e.g. "operator"). + Name string `json:"name"` + + // Version is the installed semantic version. + Version string `json:"version"` + + // InstalledAt is the RFC 3339 timestamp of the installation. + InstalledAt string `json:"installedAt"` + + // Platform records the platform-specific details used for installation. + Platform Platform `json:"platform"` +}