Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 62 additions & 11 deletions cmd/diff/client/crossplane/composition_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,27 @@ func (c *DefaultCompositionClient) GetComposition(ctx context.Context, name stri
// getCompositionRevisionRef reads the compositionRevisionRef from an XR/Claim spec.
// Returns the revision name and whether it was found.
func (c *DefaultCompositionClient) getCompositionRevisionRef(xrd, res *un.Unstructured) (string, bool) {
revisionRefName, found, _ := un.NestedString(res.Object, makeCrossplaneRefPath(xrd.GetAPIVersion(), "compositionRevisionRef", "name")...)
return revisionRefName, found && revisionRefName != ""
// Try all possible paths for compositionRevisionRef (v2 path first, then v1 fallback)
for _, path := range getCrossplaneRefPaths(xrd.GetAPIVersion(), "compositionRevisionRef", "name") {
revisionRefName, found, _ := un.NestedString(res.Object, path...)
if found && revisionRefName != "" {
return revisionRefName, true
}
}
return "", false
}

// getCompositionUpdatePolicy reads the compositionUpdatePolicy from an XR/Claim.
// Returns the policy value and whether it was found. Defaults to "Automatic" if not found.
func (c *DefaultCompositionClient) getCompositionUpdatePolicy(xrd, res *un.Unstructured) string {
policy, found, _ := un.NestedString(res.Object, makeCrossplaneRefPath(xrd.GetAPIVersion(), "compositionUpdatePolicy")...)
if !found || policy == "" {
return "Automatic" // Default policy
// Try all possible paths for compositionUpdatePolicy (v2 path first, then v1 fallback)
for _, path := range getCrossplaneRefPaths(xrd.GetAPIVersion(), "compositionUpdatePolicy") {
policy, found, _ := un.NestedString(res.Object, path...)
if found && policy != "" {
return policy
}
}

return policy
return "Automatic" // Default policy
}

// resolveCompositionFromRevisions determines which composition to use based on revision logic.
Expand Down Expand Up @@ -470,6 +478,25 @@ func (c *DefaultCompositionClient) labelsMatch(labels, selector map[string]strin
return true
}

// getCrossplaneRefPaths returns possible paths for crossplane spec fields.
// For v2 XRDs, returns both the new path (spec.crossplane.x) and legacy path (spec.x)
// to maintain backward compatibility with XRs that use the v1-style paths.
func getCrossplaneRefPaths(apiVersion string, path ...string) [][]string {
v1Path := append([]string{"spec"}, path...)
v2Path := append([]string{"spec", "crossplane"}, path...)

switch apiVersion {
case "apiextensions.crossplane.io/v1":
// Crossplane v1 keeps these under spec.x
return [][]string{v1Path}
default:
// Crossplane v2 prefers spec.crossplane.x but also supports legacy spec.x
// Try v2 path first, then fall back to v1 path for compatibility
return [][]string{v2Path, v1Path}
}
}

// Deprecated: Use getCrossplaneRefPaths instead for better v2 compatibility.
func makeCrossplaneRefPath(apiVersion string, path ...string) []string {
var specCrossplane []string

Expand All @@ -487,8 +514,20 @@ func makeCrossplaneRefPath(apiVersion string, path ...string) []string {

// findByDirectReference attempts to find a composition directly referenced by name.
func (c *DefaultCompositionClient) findByDirectReference(ctx context.Context, xrd, res *un.Unstructured, targetGVK schema.GroupVersionKind, resourceID string) (*apiextensionsv1.Composition, error) {
compositionRefName, compositionRefFound, err := un.NestedString(res.Object, makeCrossplaneRefPath(xrd.GetAPIVersion(), "compositionRef", "name")...)
if err == nil && compositionRefFound && compositionRefName != "" {
// Try all possible paths for compositionRef (v2 path first, then v1 fallback)
var compositionRefName string
var compositionRefFound bool
for _, path := range getCrossplaneRefPaths(xrd.GetAPIVersion(), "compositionRef", "name") {
name, found, err := un.NestedString(res.Object, path...)
if err == nil && found && name != "" {
compositionRefName = name
compositionRefFound = true
c.logger.Debug("Found compositionRef at path", "path", path, "name", name)
break
}
}

if compositionRefFound && compositionRefName != "" {
c.logger.Debug("Found direct composition reference",
"resource", resourceID,
"compositionName", compositionRefName)
Expand Down Expand Up @@ -533,8 +572,20 @@ func (c *DefaultCompositionClient) findByDirectReference(ctx context.Context, xr

// findByLabelSelector attempts to find compositions that match label selectors.
func (c *DefaultCompositionClient) findByLabelSelector(ctx context.Context, xrd, res *un.Unstructured, targetGVK schema.GroupVersionKind, resourceID string) (*apiextensionsv1.Composition, error) {
matchLabels, selectorFound, err := un.NestedMap(res.Object, makeCrossplaneRefPath(xrd.GetAPIVersion(), "compositionSelector", "matchLabels")...)
if err == nil && selectorFound && len(matchLabels) > 0 {
// Try all possible paths for compositionSelector (v2 path first, then v1 fallback)
var matchLabels map[string]any
var selectorFound bool
for _, path := range getCrossplaneRefPaths(xrd.GetAPIVersion(), "compositionSelector", "matchLabels") {
labels, found, err := un.NestedMap(res.Object, path...)
if err == nil && found && len(labels) > 0 {
matchLabels = labels
selectorFound = true
c.logger.Debug("Found compositionSelector at path", "path", path)
break
}
}

if selectorFound && len(matchLabels) > 0 {
c.logger.Debug("Found composition selector",
"resource", resourceID,
"matchLabels", matchLabels)
Expand Down
13 changes: 9 additions & 4 deletions cmd/diff/comp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/alecthomas/kong"
xp "github.com/crossplane-contrib/crossplane-diff/cmd/diff/client/crossplane"
dp "github.com/crossplane-contrib/crossplane-diff/cmd/diff/diffprocessor"
"k8s.io/client-go/rest"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
Expand Down Expand Up @@ -72,11 +71,17 @@ Examples:
}

// AfterApply implements kong's AfterApply method to bind our dependencies.
func (c *CompCmd) AfterApply(ctx *kong.Context, log logging.Logger, config *rest.Config) error {
return c.initializeDependencies(ctx, log, config)
func (c *CompCmd) AfterApply(ctx *kong.Context, log logging.Logger) error {
return c.initializeDependencies(ctx, log)
}

func (c *CompCmd) initializeDependencies(ctx *kong.Context, log logging.Logger, config *rest.Config) error {
func (c *CompCmd) initializeDependencies(ctx *kong.Context, log logging.Logger) error {
// Get the REST config using the context flag from CommonCmdFields
config, err := c.GetRestConfig()
if err != nil {
return errors.Wrap(err, "cannot create kubernetes client config")
}

appCtx, err := initializeSharedDependencies(ctx, log, config)
if err != nil {
return err
Expand Down
18 changes: 8 additions & 10 deletions cmd/diff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ var _ = kong.Must(&cli{})
type (
verboseFlag bool
// KubeContext represents the Kubernetes context name from the kubeconfig.
// Kong will automatically bind this when the --context flag is parsed.
KubeContext string
)

Expand All @@ -49,20 +48,18 @@ type CommonCmdFields struct {
IgnorePaths []string `help:"Paths to ignore in diffs (e.g., 'metadata.annotations[argocd.argoproj.io/tracking-id]')." name:"ignore-paths"`
}

// GetRestConfig creates a Kubernetes REST config using the context specified in the command flags.
func (c *CommonCmdFields) GetRestConfig() (*rest.Config, error) {
return GetRestConfigForContext(c.Context)
}

func (v verboseFlag) BeforeApply(ctx *kong.Context) error { //nolint:unparam // BeforeApply requires this signature.
logger := logging.NewLogrLogger(zap.New(zap.UseDevMode(true)))
ctx.BindTo(logger, (*logging.Logger)(nil))

return nil
}

// BeforeApply binds the context string so it's available to getRestConfig via dependency injection.
func (c *CommonCmdFields) BeforeApply(ctx *kong.Context) error { //nolint:unparam // BeforeApply requires this signature.
// Bind the context string so getRestConfig can use it
ctx.BindTo(c.Context, (*KubeContext)(nil))
return nil
}

// The top-level crossplane CLI.
type cli struct {
// Subcommands and flags will appear in the CLI help output in the same
Expand All @@ -86,7 +83,6 @@ func main() {
// Binding a variable to kong context makes it available to all commands
// at runtime.
kong.BindTo(logger, (*logging.Logger)(nil)),
kong.BindToProvider(getRestConfig),
kong.ConfigureHelp(kong.HelpOptions{
FlagsLast: true,
Compact: true,
Expand All @@ -97,7 +93,9 @@ func main() {
ctx.FatalIfErrorf(err)
}

func getRestConfig(kubeContext KubeContext) (*rest.Config, error) {
// GetRestConfigForContext creates a Kubernetes REST config for the specified context.
// If kubeContext is empty, it uses the current context from kubeconfig.
func GetRestConfigForContext(kubeContext KubeContext) (*rest.Config, error) {
// Use the standard client-go loading rules:
// 1. If KUBECONFIG env var is set, use that
// 2. Otherwise, use ~/.kube/config
Expand Down
13 changes: 9 additions & 4 deletions cmd/diff/xr.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package main
import (
"github.com/alecthomas/kong"
dp "github.com/crossplane-contrib/crossplane-diff/cmd/diff/diffprocessor"
"k8s.io/client-go/rest"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
"github.com/crossplane/crossplane-runtime/v2/pkg/logging"
Expand Down Expand Up @@ -62,11 +61,17 @@ Examples:
}

// AfterApply implements kong's AfterApply method to bind our dependencies.
func (c *XRCmd) AfterApply(ctx *kong.Context, log logging.Logger, config *rest.Config) error {
return c.initializeDependencies(ctx, log, config)
func (c *XRCmd) AfterApply(ctx *kong.Context, log logging.Logger) error {
return c.initializeDependencies(ctx, log)
}

func (c *XRCmd) initializeDependencies(ctx *kong.Context, log logging.Logger, config *rest.Config) error {
func (c *XRCmd) initializeDependencies(ctx *kong.Context, log logging.Logger) error {
// Get the REST config using the context flag from CommonCmdFields
config, err := c.GetRestConfig()
if err != nil {
return errors.Wrap(err, "cannot create kubernetes client config")
}

appCtx, err := initializeSharedDependencies(ctx, log, config)
if err != nil {
return err
Expand Down
Loading