1
0
mirror of synced 2026-05-02 17:53:33 +00:00

Make plugin types public

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan
2026-04-14 00:02:13 +03:00
parent c0938d351f
commit 5256361d8c
6 changed files with 192 additions and 153 deletions

View File

@@ -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 <name>" 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 "<algorithm>:<hex>" 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

View File

@@ -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"},
},

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

135
pkg/plugin/types.go Normal file
View File

@@ -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 <name>" 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 "<algorithm>:<hex>" 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"`
}