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
40 changes: 39 additions & 1 deletion cmd/diff/diffprocessor/function_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
"time"

Expand All @@ -48,6 +49,35 @@ type FunctionProvider interface {
Cleanup(ctx context.Context) error
}

// EnvDockerNetwork is the environment variable that specifies which Docker
// network function containers should join. This is needed when crossplane-diff
// runs inside a Docker container (e.g. a GitHub Actions container job) so that
// function containers are on the same network and reachable via container IP.
const EnvDockerNetwork = "CROSSPLANE_DIFF_DOCKER_NETWORK"

// annotationRuntimeDockerNetwork is the render annotation that configures the
// Docker network for function containers.
const annotationRuntimeDockerNetwork = "render.crossplane.io/runtime-docker-network"

// applyDockerNetworkAnnotation sets the Docker network annotation on functions
// if the CROSSPLANE_DIFF_DOCKER_NETWORK environment variable is set.
func applyDockerNetworkAnnotation(fns []pkgv1.Function, log logging.Logger) {
network := os.Getenv(EnvDockerNetwork)
if network == "" {
return
}

log.Debug("Setting Docker network annotation on functions", "network", network)

for i := range fns {
if fns[i].Annotations == nil {
fns[i].Annotations = make(map[string]string)
}

fns[i].Annotations[annotationRuntimeDockerNetwork] = network
}
}

// DefaultFunctionProvider fetches functions from the cluster on each call.
// This is appropriate for the xr command where each XR is processed independently.
type DefaultFunctionProvider struct {
Expand All @@ -74,6 +104,8 @@ func (p *DefaultFunctionProvider) GetFunctionsForComposition(comp *apiextensions

p.logger.Debug("Fetched functions from pipeline", "composition", comp.GetName(), "count", len(fns))

applyDockerNetworkAnnotation(fns, p.logger)

return fns, nil
}

Expand Down Expand Up @@ -121,9 +153,13 @@ func generateInstanceID() string {
func (p *CachedFunctionProvider) GetFunctionsForComposition(comp *apiextensionsv1.Composition) ([]pkgv1.Function, error) {
compName := comp.GetName()

// Check cache first
// Check cache first. Re-apply the Docker network annotation on every cache
// hit so the annotation reflects the current value of EnvDockerNetwork
// (rather than whatever was in effect when the entry was first cached).
if cached, ok := p.cache[compName]; ok {
p.logger.Debug("Using cached functions", "composition", compName, "count", len(cached))
applyDockerNetworkAnnotation(cached, p.logger)

return cached, nil
}

Expand Down Expand Up @@ -165,6 +201,8 @@ func (p *CachedFunctionProvider) GetFunctionsForComposition(comp *apiextensionsv
p.containerNames = append(p.containerNames, containerName)
}

applyDockerNetworkAnnotation(fns, p.logger)

// Cache for future calls
p.cache[compName] = fns

Expand Down
123 changes: 123 additions & 0 deletions cmd/diff/diffprocessor/function_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,56 @@ func TestCachedFunctionProvider_GetFunctionsForComposition_CacheHit(t *testing.T
}
}

// TestCachedFunctionProvider_DockerNetworkAnnotation_CacheHit verifies that
// the Docker network annotation reflects the *current* value of
// CROSSPLANE_DIFF_DOCKER_NETWORK on every call, including when a cached entry
// is returned. Without this guarantee, a cache populated before the env var
// was set would keep returning unannotated functions for the rest of the
// process, defeating the feature.
func TestCachedFunctionProvider_DockerNetworkAnnotation_CacheHit(t *testing.T) {
functions := []pkgv1.Function{
{ObjectMeta: metav1.ObjectMeta{Name: "function-test"}},
}

fnClient := tu.NewMockFunctionClient().
WithFunctionsFetchCallback(func() ([]pkgv1.Function, error) {
return functions, nil
}).
Build()

logger := tu.TestLogger(t, false)
provider := NewCachedFunctionProvider(fnClient, logger)

comp := &apiextensionsv1.Composition{
ObjectMeta: metav1.ObjectMeta{Name: "test-composition"},
}

// Prime the cache with the env var unset so the cached entry has no
// network annotation written during the miss path.
t.Setenv(EnvDockerNetwork, "")

if _, err := provider.GetFunctionsForComposition(comp); err != nil {
t.Fatalf("priming GetFunctionsForComposition() error = %v", err)
}

// Now set the env var and request the same composition again. The
// returned (cached) functions must carry the annotation.
t.Setenv(EnvDockerNetwork, "ci-network")

fns, err := provider.GetFunctionsForComposition(comp)
if err != nil {
t.Fatalf("cache-hit GetFunctionsForComposition() error = %v", err)
}

if len(fns) != 1 {
t.Fatalf("got %d functions, want 1", len(fns))
}

if got := fns[0].Annotations[annotationRuntimeDockerNetwork]; got != "ci-network" {
t.Errorf("cache-hit network annotation = %q, want %q", got, "ci-network")
}
}

func TestCachedFunctionProvider_GetFunctionsForComposition_Error(t *testing.T) {
fnClient := tu.NewMockFunctionClient().
WithFailedFunctionsFetch("fetch error").
Expand Down Expand Up @@ -671,3 +721,76 @@ func TestGenerateContainerName(t *testing.T) {
})
}
}

func TestApplyDockerNetworkAnnotation(t *testing.T) {
tests := map[string]struct {
envValue string
fns []pkgv1.Function
wantNetwork string
checkExistingPreserved bool
}{
"EnvSet": {
envValue: "github_network_abc123",
fns: []pkgv1.Function{
{ObjectMeta: metav1.ObjectMeta{Name: "function-1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "function-2"}},
},
wantNetwork: "github_network_abc123",
},
"EnvNotSet": {
envValue: "",
fns: []pkgv1.Function{
{ObjectMeta: metav1.ObjectMeta{Name: "function-1"}},
},
wantNetwork: "",
},
"ExistingAnnotations": {
envValue: "my-network",
fns: []pkgv1.Function{
{
ObjectMeta: metav1.ObjectMeta{
Name: "function-1",
Annotations: map[string]string{
"existing-key": "existing-value",
},
},
},
},
wantNetwork: "my-network",
checkExistingPreserved: true,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
// t.Setenv (even with an empty value) snapshots the prior value
// and restores it on subtest cleanup, avoiding leaks into other
// tests in this package or into the developer's shell.
t.Setenv(EnvDockerNetwork, tt.envValue)

logger := tu.TestLogger(t, false)
applyDockerNetworkAnnotation(tt.fns, logger)

for _, fn := range tt.fns {
got := fn.Annotations[annotationRuntimeDockerNetwork]
if got != tt.wantNetwork {
t.Errorf("function %q: network annotation = %q, want %q", fn.Name, got, tt.wantNetwork)
}
}

if tt.checkExistingPreserved {
for _, fn := range tt.fns {
v, ok := fn.Annotations["existing-key"]
if !ok {
t.Errorf("function %q: existing annotation %q was removed", fn.Name, "existing-key")
continue
}

if v != "existing-value" {
t.Errorf("function %q: existing annotation = %q, want %q", fn.Name, v, "existing-value")
}
}
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
dario.cat/mergo v1.0.2
github.com/Masterminds/semver v1.5.0
github.com/alecthomas/kong v1.15.0
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260615182009-ba59fbfac34b
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260617170926-a416505fb016
github.com/crossplane/crossplane-runtime/v2 v2.4.0-rc.0
github.com/crossplane/crossplane/apis/v2 v2.4.0-rc.0
github.com/crossplane/crossplane/v2 v2.3.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260615182009-ba59fbfac34b h1:Ol4CZMZvcS/m3IYRz4YceZGj6pCF1TLyN+nTpgMgDPg=
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260615182009-ba59fbfac34b/go.mod h1:TVfHZdpkSlrfkE6V8VLlTOS/DT4Qsxc+ybMcUnZz3ko=
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260617170926-a416505fb016 h1:UY6gCUfSLuoG4g8WUy9+tYOCXXCPFy9ootsQ+OL2LBk=
github.com/crossplane/cli/v2 v2.4.0-rc.0.0.20260617170926-a416505fb016/go.mod h1:TVfHZdpkSlrfkE6V8VLlTOS/DT4Qsxc+ybMcUnZz3ko=
github.com/crossplane/crossplane-runtime/v2 v2.4.0-rc.0 h1:Zgiq+hrh9lbjWtv8ECCLd1A0I9knt3c8ZUELExw6M1w=
github.com/crossplane/crossplane-runtime/v2 v2.4.0-rc.0/go.mod h1:PAo3zIfmMzrS18HGyHJLXCeXIp0nFW2Md2Fn9gocMaU=
github.com/crossplane/crossplane/apis/v2 v2.4.0-rc.0 h1:4PBahj+tnK9RwSZm1bYGvOkHOU+1CSHjJF2PoPzBMD0=
Expand Down
Loading