package releaseimage
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
confv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkecommon/v1beta1"
bkev1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/capbke/v1beta1"
riv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/v1alpha1"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/release/compatibility"
"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/release/manifest"
)
type controllerPuller struct {
files *manifest.BundleFiles
digest string
ref manifest.ReleaseRef
calls int
}
func (p *controllerPuller) Pull(_ context.Context, ref manifest.ReleaseRef) (*manifest.BundleFiles, string, error) {
p.calls++
p.ref = ref
return p.files, p.digest, nil
}
func TestReleaseImageReconcilerMarksValid(t *testing.T) {
reconciler, key, puller, store := newTestReconciler(t, releaseBundleFiles("v3.5.21-of.1"))
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
got := &riv1beta1.ReleaseImage{}
require.NoError(t, reconciler.Get(context.Background(), key, got))
assert.Equal(t, riv1beta1.ReleaseImagePhaseValid, got.Status.Phase)
assert.Equal(t, 2, got.Status.ComponentCount)
assert.Equal(t, []riv1beta1.ComponentStatus{
{Name: "etcd", Version: "v3.5.21-of.1", Type: riv1beta1.ComponentTypeBinary},
{Name: "kubernetes", Version: "v1.29.1-of.1", Type: riv1beta1.ComponentTypeBinary},
}, got.Status.Components)
assert.Equal(t, manifest.SourceOCI, got.Status.Source)
assert.Equal(t, "registry.example.com:5000/openfuyao/release-image:v26.03", puller.ref.OCIRef)
assert.Equal(t, 1, puller.calls)
assert.Contains(t, got.Finalizers, releaseImageFinalizer)
ref := manifest.ReleaseRef{Version: "v26.03"}
cached, err := store.ResolveRelease(context.Background(), ref)
require.NoError(t, err)
assert.Equal(t, puller.digest, cached.Digest)
digestRef := manifest.ReleaseRef{Digest: got.Status.Digest}
_, err = store.ResolveRelease(context.Background(), digestRef)
require.Error(t, err)
assert.Contains(t, err.Error(), "not cached")
}
func TestReleaseImageReconcilerAlwaysRefreshesOnUpdate(t *testing.T) {
reconciler, key, puller, _ := newTestReconciler(t, releaseBundleFiles("v3.5.21-of.1"))
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
_, err = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
assert.Equal(t, 2, puller.calls)
}
func TestReleaseImageReconcilerMarksCompatibilityFailed(t *testing.T) {
reconciler, key, puller, store := newTestReconciler(t, releaseBundleFiles("v3.4.0"))
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
got := &riv1beta1.ReleaseImage{}
require.NoError(t, reconciler.Get(context.Background(), key, got))
assert.Equal(t, riv1beta1.ReleaseImagePhaseCompatibilityFailed, got.Status.Phase)
assert.Contains(t, got.Status.CompatibilityReport, "etcd")
assert.Equal(t, 1, puller.calls)
ref := manifest.ReleaseRef{Version: "v26.03"}
_, err = store.ResolveRelease(context.Background(), ref)
require.Error(t, err)
assert.Contains(t, err.Error(), "not cached")
}
func TestReleaseImageReconcilerCompatibilityFailedDoesNotOverwriteValidCache(t *testing.T) {
goodFiles := releaseBundleFiles("v3.5.21-of.1")
reconciler, key, puller, store := newTestReconciler(t, goodFiles)
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
ref := manifest.ReleaseRef{Version: "v26.03"}
cachedBefore, err := store.ResolveRelease(context.Background(), ref)
require.NoError(t, err)
puller.files = releaseBundleFiles("v3.4.0")
puller.digest = manifest.DigestFiles(puller.files)
_, err = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
got := &riv1beta1.ReleaseImage{}
require.NoError(t, reconciler.Get(context.Background(), key, got))
assert.Equal(t, riv1beta1.ReleaseImagePhaseCompatibilityFailed, got.Status.Phase)
cachedAfter, err := store.ResolveRelease(context.Background(), ref)
require.NoError(t, err)
assert.Equal(t, cachedBefore.Digest, cachedAfter.Digest)
}
func TestReleaseImageReconcilerMarksInvalidWhenImageRepoMissing(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, bkev1beta1.AddToScheme(scheme))
require.NoError(t, riv1beta1.AddToScheme(scheme))
key := types.NamespacedName{Namespace: "default", Name: "release-v26-03"}
ri := &riv1beta1.ReleaseImage{
ObjectMeta: metav1.ObjectMeta{Namespace: key.Namespace, Name: key.Name},
Spec: riv1beta1.ReleaseImageSpec{Version: "v26.03"},
}
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ri).
WithStatusSubresource(ri).
Build()
reconciler := &ReleaseImageReconciler{Client: client, Scheme: scheme}
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
got := &riv1beta1.ReleaseImage{}
require.NoError(t, reconciler.Get(context.Background(), key, got))
assert.Equal(t, riv1beta1.ReleaseImagePhaseInvalid, got.Status.Phase)
assert.Contains(t, got.Status.Message, "BKECluster not found")
}
func TestReleaseImageReconcilerDeleteEvictsBundleCache(t *testing.T) {
reconciler, key, _, store := newTestReconciler(t, releaseBundleFiles("v3.5.21-of.1"))
_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
ref := manifest.ReleaseRef{Version: "v26.03"}
_, err = store.ResolveRelease(context.Background(), ref)
require.NoError(t, err)
ri := &riv1beta1.ReleaseImage{}
require.NoError(t, reconciler.Get(context.Background(), key, ri))
require.NoError(t, reconciler.Delete(context.Background(), ri))
_, err = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: key})
require.NoError(t, err)
got := &riv1beta1.ReleaseImage{}
if err := reconciler.Get(context.Background(), key, got); err == nil {
assert.NotContains(t, got.Finalizers, releaseImageFinalizer)
}
_, err = store.ResolveRelease(context.Background(), ref)
require.Error(t, err)
assert.Contains(t, err.Error(), "not cached")
}
func TestComponentStatusesHandlesNilBundle(t *testing.T) {
assert.Nil(t, componentStatuses(nil))
assert.Nil(t, componentStatuses(&manifest.Bundle{}))
}
func newTestReconciler(
t *testing.T,
files *manifest.BundleFiles,
) (*ReleaseImageReconciler, types.NamespacedName, *controllerPuller, *manifest.Store) {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, bkev1beta1.AddToScheme(scheme))
require.NoError(t, riv1beta1.AddToScheme(scheme))
key := types.NamespacedName{Namespace: "default", Name: "release-v26-03"}
ri := &riv1beta1.ReleaseImage{}
ri.Name = key.Name
ri.Namespace = key.Namespace
ri.Spec.Version = "v26.03"
digest := manifest.DigestFiles(files)
puller := &controllerPuller{files: files, digest: digest}
store := manifest.NewStore(t.TempDir(), puller, nil)
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(ri, testBKECluster(key.Namespace, "cluster-a")).
WithStatusSubresource(ri).
Build()
reconciler := &ReleaseImageReconciler{
Client: client,
Scheme: scheme,
Store: store,
Compatibility: compatibility.NewEngine(),
}
return reconciler, key, puller, store
}
func testBKECluster(namespace, name string) *bkev1beta1.BKECluster {
return &bkev1beta1.BKECluster{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
Spec: confv1beta1.BKEClusterSpec{
ClusterConfig: &confv1beta1.BKEConfig{
Cluster: confv1beta1.Cluster{
ImageRepo: confv1beta1.Repo{
Domain: "registry.example.com",
Port: "5000",
Prefix: "openfuyao",
},
},
},
},
}
}
func releaseBundleFiles(etcdVersion string) *manifest.BundleFiles {
return &manifest.BundleFiles{Files: map[string][]byte{
"release.yaml": []byte(`apiVersion: bke.bocloud.com/v1beta1
kind: ReleaseImage
metadata:
name: release-v26-03
spec:
version: v26.03
ociRef: registry/release:v26.03
upgrade:
components:
- name: kubernetes
version: v1.29.1-of.1
- name: etcd
version: ` + etcdVersion + `
`),
"components/kubernetes/component.yaml": []byte(`apiVersion: bke.bocloud.com/v1beta1
kind: ComponentVersion
metadata:
name: kubernetes
spec:
name: kubernetes
version: v1.29.1-of.1
type: binary
compatibility:
constraints:
- component: etcd
rule: ">=3.5.10 <3.6.0"
`),
"components/etcd/component.yaml": []byte(`apiVersion: bke.bocloud.com/v1beta1
kind: ComponentVersion
metadata:
name: etcd
spec:
name: etcd
version: ` + etcdVersion + `
type: binary
`),
}}
}