...

Source file src/edge-infra.dev/pkg/edge/controllers/syncedobject/integration/controller_test.go

Documentation: edge-infra.dev/pkg/edge/controllers/syncedobject/integration

     1  package syncedobject
     2  
     3  import (
     4  	"context"
     5  	crypto "crypto/rand"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"math/rand"
    10  	"os"
    11  	"testing"
    12  	"time"
    13  
    14  	"cloud.google.com/go/pubsub"
    15  	"github.com/google/uuid"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/api/option"
    19  	"gopkg.in/yaml.v2"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/types"
    22  	"sigs.k8s.io/controller-runtime/pkg/client"
    23  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    24  
    25  	soapi "edge-infra.dev/pkg/edge/apis/syncedobject/apis/v1alpha1"
    26  	"edge-infra.dev/pkg/edge/chariot"
    27  	chariotClientApi "edge-infra.dev/pkg/edge/chariot/client"
    28  	"edge-infra.dev/pkg/edge/controllers/syncedobject"
    29  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    30  	"edge-infra.dev/pkg/lib/promassert"
    31  	"edge-infra.dev/test/f2"
    32  	"edge-infra.dev/test/f2/x/ktest"
    33  	"edge-infra.dev/test/f2/x/pstest"
    34  )
    35  
    36  const DefaultSecretValue = "mock secret value"
    37  
    38  var testConfig = &syncedobject.Config{
    39  	PublishTopic: "chariot-rides",
    40  	GCPProjectID: "test-foreman0",
    41  	MetricsAddr:  "127.0.0.1:8080",
    42  }
    43  
    44  func init() {
    45  	addr := fmt.Sprintf("http://%s/metrics", testConfig.MetricsAddr)
    46  	promassert.ScrapePrometheusMetrics = promassert.ScrapeHTTP(addr)
    47  }
    48  
    49  var f f2.Framework
    50  
    51  const timeout = 10 * time.Second
    52  const tick = 50 * time.Millisecond
    53  
    54  func TestMain(m *testing.M) {
    55  	var ps = pstest.New(
    56  		pstest.WithProjectID(testConfig.GCPProjectID),
    57  	)
    58  	var k = ktest.New(ktest.WithCtrlManager(testConfig.CreateMgr), ktest.WithKonfigKonnector())
    59  	f = f2.New(context.Background(), f2.WithExtensions(k, ps)).Component("syncedobjectctl")
    60  	os.Exit(f.Run(m))
    61  }
    62  
    63  func TestSyncedObjectReconciliation(t *testing.T) {
    64  	var feat = f2.NewFeature(t.Name()).
    65  		Setup("topic", setupTopic).
    66  		Test("reconciles", func(ctx f2.Context, t *testing.T) f2.Context {
    67  			var so = randomSyncedObject()
    68  			so.Spec.Directory = nil
    69  			createSyncedObject(ctx, t, so)
    70  			return ctx
    71  		}).
    72  		Test("reconciles with dir", func(ctx f2.Context, t *testing.T) f2.Context {
    73  			var so = randomSyncedObject()
    74  			var dir = "myDir"
    75  			so.Spec.Directory = &dir
    76  			createSyncedObject(ctx, t, so)
    77  			return ctx
    78  		}).Feature()
    79  
    80  	f.Test(t, feat)
    81  }
    82  
    83  func TestSyncedObjectInvalidSpecStalled(t *testing.T) {
    84  	var feat = f2.NewFeature(t.Name()).
    85  		Setup("topic", setupTopic).
    86  		Test("reconciles to stalled condition", func(ctx f2.Context, t *testing.T) f2.Context {
    87  			var so = randomSyncedObject()
    88  			so.Spec.Object = "" // an invalid spec.
    89  			require.NoError(t, ktest.FromContextT(ctx, t).Client.Create(ctx, &so))
    90  
    91  			var kso soapi.SyncedObject
    92  			require.Eventually(t, func() bool {
    93  				err := ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
    94  					Name:      so.Name,
    95  					Namespace: so.Namespace,
    96  				}, &kso)
    97  				return err == nil && conditions.IsStalled(&kso)
    98  			}, timeout, tick)
    99  			return ctx
   100  		}).Feature()
   101  
   102  	f.Test(t, feat)
   103  }
   104  
   105  func TestSyncedObjectExpireAtDeletesExpiredObjects(t *testing.T) {
   106  	var so = randomSyncedObject()
   107  	so.Spec.ExpireAt = &metav1.Time{
   108  		Time: time.Now().Add(3 * time.Second),
   109  	}
   110  
   111  	var reqs = make(chan chariot.Request, 2)
   112  
   113  	//nolint:dupl
   114  	var feat = f2.NewFeature(t.Name()).
   115  		Setup("topic", setupTopic).
   116  		Setup("receive", receiveMessages(reqs)).
   117  		Test("reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
   118  			createSyncedObjectAndValidateChariotMessage(ctx, t, reqs, so)
   119  			return ctx
   120  		}).
   121  		Test("is deleted when expired", func(ctx f2.Context, t *testing.T) f2.Context {
   122  			require.Eventually(t, func() bool {
   123  				var kso soapi.SyncedObject
   124  				err := ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
   125  					Name:      so.Name,
   126  					Namespace: so.Namespace,
   127  				}, &kso)
   128  				if time.Now().Before(so.Spec.ExpireAt.Time) {
   129  					// The synced object should still exist before it expires.
   130  					require.NoError(t, err)
   131  				}
   132  				return err != nil && client.IgnoreNotFound(err) == nil
   133  			}, timeout, tick, "SyncedObject never got deleted")
   134  			return ctx
   135  		}).
   136  		Test("sent delete pubsub message", func(ctx f2.Context, t *testing.T) f2.Context {
   137  			select {
   138  			case <-time.After(timeout):
   139  				t.Fatal("never got delete chariot request for expired synced object")
   140  			case req := <-reqs:
   141  				validateChariotMessage(t, chariotClientApi.Delete, so, req)
   142  			}
   143  			return ctx
   144  		}).Feature()
   145  
   146  	f.Test(t, feat)
   147  }
   148  
   149  func TestSyncedObjectAbandonAnnotation(t *testing.T) {
   150  	var so = randomSyncedObject()
   151  	so.ObjectMeta.Annotations = map[string]string{
   152  		soapi.AnnotationDeletionPolicy: soapi.AnnotationDeletionPolicyAbandon,
   153  	}
   154  	so.Spec.ExpireAt = &metav1.Time{
   155  		Time: time.Now().Add(time.Second),
   156  	}
   157  
   158  	var reqs = make(chan chariot.Request, 2)
   159  
   160  	//nolint:dupl
   161  	var feat = f2.NewFeature(t.Name()).
   162  		Setup("topic", setupTopic).
   163  		Setup("receive", receiveMessages(reqs)).
   164  		Test("reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
   165  			createSyncedObjectAndValidateChariotMessage(ctx, t, reqs, so)
   166  			return ctx
   167  		}).
   168  		Test("is deleted when expired", func(ctx f2.Context, t *testing.T) f2.Context {
   169  			require.Eventually(t, func() bool {
   170  				var kso soapi.SyncedObject
   171  				err := ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
   172  					Name:      so.Name,
   173  					Namespace: so.Namespace,
   174  				}, &kso)
   175  				if time.Now().Before(so.Spec.ExpireAt.Time) {
   176  					// The synced object should still exist before it expires.
   177  					require.NoError(t, err)
   178  				}
   179  				return err != nil && client.IgnoreNotFound(err) == nil
   180  			}, timeout, tick, "SyncedObject never got deleted")
   181  			return ctx
   182  		}).
   183  		Test("delete pubsub message is not sent", func(ctx f2.Context, t *testing.T) f2.Context {
   184  			select {
   185  			case <-time.After(2 * time.Second):
   186  			case req := <-reqs:
   187  				t.Fatalf("should not get delete chariot request for expired synced object with abandon annotation: %v", req)
   188  			}
   189  			return ctx
   190  		}).Feature()
   191  
   192  	f.Test(t, feat)
   193  }
   194  
   195  func TestSyncedObjectAbandonAnnotationForOutdated(t *testing.T) {
   196  	var so = randomSyncedObject()
   197  	so.ObjectMeta.Annotations = map[string]string{
   198  		soapi.AnnotationDeletionPolicy: soapi.AnnotationDeletionPolicyAbandon,
   199  	}
   200  
   201  	var reqs = make(chan chariot.Request, 2)
   202  
   203  	var feat = f2.NewFeature(t.Name()).
   204  		Setup("topic", setupTopic).
   205  		Setup("receive", receiveMessages(reqs)).
   206  		Test("reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
   207  			createSyncedObjectAndValidateChariotMessage(ctx, t, reqs, so)
   208  			return ctx
   209  		}).
   210  		Test("update the cluster to change the storage location", func(ctx f2.Context, t *testing.T) f2.Context {
   211  			require.NoError(t, so.SetStatusDetails())
   212  			so.Spec.Cluster = uuid.New().String()
   213  
   214  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   215  			require.NoError(t, err)
   216  			require.True(t, storageLocationOutdated)
   217  
   218  			updateSyncedObject(ctx, t, so)
   219  			return ctx
   220  		}).
   221  		Test("delete pubsub message is not sent", func(ctx f2.Context, t *testing.T) f2.Context {
   222  			select {
   223  			case <-time.After(timeout):
   224  				t.Fatal("timed out waiting for create message")
   225  			case req := <-reqs:
   226  				validateChariotMessage(t, chariotClientApi.Create, so, req)
   227  			}
   228  
   229  			select {
   230  			case <-time.After(2 * time.Second):
   231  			case req := <-reqs:
   232  				t.Fatalf("should not get delete chariot request for updated synced object with abandon annotation: %v", req)
   233  			}
   234  			return ctx
   235  		}).Feature()
   236  
   237  	f.Test(t, feat)
   238  }
   239  
   240  func TestSyncedObjectMetricsWereSet(t *testing.T) {
   241  	var so = randomSyncedObject()
   242  
   243  	// gather metrics before test.
   244  	const met = "edge_soctl_reconcile_condition_status"
   245  	var labels = prometheus.Labels{"status": "True"}
   246  	var initialMetricValue = promassert.Gauge(met).With(labels).TryFold()
   247  
   248  	var feat = f2.NewFeature(t.Name()).
   249  		Setup("topic", setupTopic).
   250  		Test("metrics test", func(ctx f2.Context, t *testing.T) f2.Context {
   251  			createSyncedObject(ctx, t, so)
   252  			return ctx
   253  		}).
   254  		Test("metrics test", func(ctx f2.Context, t *testing.T) f2.Context {
   255  			// Check that the metric value increased by 1 after applying and reconciling the syncedobject crd.
   256  			require.Eventually(t, func() bool {
   257  				finalMetricValue := promassert.Gauge(met).With(labels).TryFold()
   258  				return 1 == finalMetricValue-initialMetricValue
   259  			}, timeout, time.Second, "Metric was never set")
   260  			return ctx
   261  		}).Feature()
   262  
   263  	f.Test(t, feat)
   264  }
   265  
   266  func TestSyncedObjectSpecUpdate(t *testing.T) {
   267  	var (
   268  		origBanner          = "ret-edge-original-banner"
   269  		origCluster         = uuid.New().String()
   270  		origObj             = randomBase64EncodedYamlObject()
   271  		origChariotID, err  = syncedobject.CreateChariotID(origObj)
   272  		origStorageLocation = chariot.FmtStorageLocation(origBanner, origCluster, "", origChariotID)
   273  		origNN              = types.NamespacedName{
   274  			Name:      "guid-abc-12345",
   275  			Namespace: "default",
   276  		}
   277  	)
   278  	require.NoError(t, err)
   279  
   280  	var origSo = soapi.SyncedObject{
   281  		ObjectMeta: metav1.ObjectMeta{
   282  			Name:      origNN.Name,
   283  			Namespace: origNN.Namespace,
   284  		},
   285  		Spec: soapi.SyncedObjectSpec{
   286  			Banner:    origBanner,
   287  			Cluster:   origCluster,
   288  			Object:    origObj,
   289  			Directory: nil,
   290  			ExpireAt:  nil,
   291  		},
   292  		Status: soapi.SyncedObjectStatus{
   293  			StoredObject:    origObj,
   294  			StorageLocation: origStorageLocation,
   295  			Banner:          origBanner,
   296  			Cluster:         origCluster,
   297  			Directory:       nil,
   298  		},
   299  	}
   300  
   301  	var reqs = make(chan chariot.Request, 2)
   302  
   303  	var feat = f2.NewFeature(t.Name()).
   304  		Setup("topic", setupTopic).
   305  		Setup("receive", receiveMessages(reqs)).
   306  		Test("reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
   307  			var so = origSo
   308  
   309  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   310  			require.NoError(t, err)
   311  			require.False(t, storageLocationOutdated)
   312  
   313  			so.Status = soapi.SyncedObjectStatus{} // clear copy for create.
   314  			createSyncedObjectAndValidateChariotMessage(ctx, t, reqs, so)
   315  			return ctx
   316  		}).
   317  		Test("validate banner changed", func(ctx f2.Context, t *testing.T) f2.Context {
   318  			var so = origSo
   319  			so.Spec.Banner = "ret-edge-updated-banner"
   320  
   321  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   322  			require.NoError(t, err)
   323  			require.True(t, storageLocationOutdated)
   324  
   325  			updateAndValidateChariotMessages(ctx, t, reqs, so)
   326  			updateAndValidateChariotMessages(ctx, t, reqs, origSo)
   327  			return ctx
   328  		}).
   329  		Test("validate cluster changed", func(ctx f2.Context, t *testing.T) f2.Context {
   330  			var so = origSo
   331  			so.Spec.Cluster = uuid.New().String()
   332  
   333  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   334  			require.NoError(t, err)
   335  			require.True(t, storageLocationOutdated)
   336  
   337  			updateAndValidateChariotMessages(ctx, t, reqs, so)
   338  			updateAndValidateChariotMessages(ctx, t, reqs, origSo)
   339  			return ctx
   340  		}).
   341  		Test("validate dir changed", func(ctx f2.Context, t *testing.T) f2.Context {
   342  			var so = origSo
   343  			var updatedDir = "McRibIsBack"
   344  			so.Spec.Directory = &updatedDir
   345  
   346  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   347  			require.NoError(t, err)
   348  			require.True(t, storageLocationOutdated)
   349  
   350  			updateAndValidateChariotMessages(ctx, t, reqs, so)
   351  			updateAndValidateChariotMessages(ctx, t, reqs, origSo)
   352  			return ctx
   353  		}).
   354  		Test("validate object changed", func(ctx f2.Context, t *testing.T) f2.Context {
   355  			var so = origSo
   356  			so.Spec.Object = randomBase64EncodedYamlObject()
   357  
   358  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   359  			require.NoError(t, err)
   360  			require.True(t, storageLocationOutdated)
   361  
   362  			updateAndValidateChariotMessages(ctx, t, reqs, so)
   363  			updateAndValidateChariotMessages(ctx, t, reqs, origSo)
   364  			return ctx
   365  		}).
   366  		Test("validate object changed without affecting storage location", func(ctx f2.Context, t *testing.T) f2.Context {
   367  			// The storage location only changes when the object's GVKNN is modified.
   368  			// Changing the object without affecting its GVKNN must not produce a delete message.
   369  			var so = origSo
   370  			var fr, err = DecodeFakeResourceBase64(so.Spec.Object)
   371  			require.NoError(t, err)
   372  			fr.Spec = FakeSpec{
   373  				Foo: "some new foo",
   374  				Bar: "bar changed too",
   375  				Baz: "will this cause a deletion snafu",
   376  			}
   377  			so.Spec.Object = fr.EncodeBase64()
   378  
   379  			storageLocationOutdated, err := syncedobject.IsStorageLocationOutdated(&so)
   380  			require.NoError(t, err)
   381  			require.False(t, storageLocationOutdated)
   382  
   383  			updateSyncedObject(ctx, t, so)
   384  
   385  			select {
   386  			case <-time.After(timeout):
   387  				t.Fatal("timed out waiting for create message")
   388  			case req := <-reqs:
   389  				validateChariotMessage(t, chariotClientApi.Create, so, req)
   390  			}
   391  			return ctx
   392  		}).Feature()
   393  
   394  	f.Test(t, feat)
   395  }
   396  
   397  func setupTopic(ctx f2.Context, t *testing.T) f2.Context {
   398  	var conn = pstest.FromContextT(ctx, t).Conn
   399  
   400  	client, err := pubsub.NewClient(context.Background(), testConfig.GCPProjectID, option.WithGRPCConn(conn))
   401  	require.NoError(t, err)
   402  
   403  	testConfig.PubsubTopic = client.Topic(testConfig.PublishTopic)
   404  
   405  	exists, err := testConfig.PubsubTopic.Exists(ctx)
   406  	require.NoError(t, err)
   407  	if !exists {
   408  		testConfig.PubsubTopic, err = client.CreateTopic(ctx, testConfig.PublishTopic)
   409  		require.NoError(t, err)
   410  	}
   411  
   412  	return ctx
   413  }
   414  
   415  func receiveMessages(ch chan chariot.Request) func(f2.Context, *testing.T) f2.Context {
   416  	return func(ctx f2.Context, t *testing.T) f2.Context {
   417  		pstest.WithNewSubscription("test", testConfig.PublishTopic)(ctx, t)
   418  		var ps = pstest.FromContextT(ctx, t)
   419  
   420  		go func() {
   421  			//nolint: errcheck // error will be returned when the Feature is done.
   422  			ps.Subscribe(context.Background(), "test", func(_ context.Context, msg *pubsub.Message) {
   423  				var req chariot.Request
   424  				if err := json.Unmarshal(msg.Data, &req); err != nil {
   425  					t.Fatal(err)
   426  				}
   427  				// uncomment for debugging:
   428  				//t.Logf("received chariot message: %s", msg.Data)
   429  
   430  				msg.Ack()
   431  				ch <- req
   432  			})
   433  			close(ch)
   434  		}()
   435  		return ctx
   436  	}
   437  }
   438  
   439  func createSyncedObject(ctx f2.Context, t *testing.T, so soapi.SyncedObject) {
   440  	require.NoError(t, ktest.FromContextT(ctx, t).Client.Create(ctx, &so))
   441  	waitForStatusToUpdate(ctx, t, &so)
   442  }
   443  
   444  func createSyncedObjectAndValidateChariotMessage(ctx f2.Context, t *testing.T, reqs chan chariot.Request, so soapi.SyncedObject) {
   445  	createSyncedObject(ctx, t, so)
   446  
   447  	select {
   448  	case <-time.After(timeout):
   449  		t.Fatal("timed out waiting for chariot message")
   450  	case req := <-reqs:
   451  		validateChariotMessage(t, chariotClientApi.Create, so, req)
   452  	}
   453  }
   454  
   455  func validateChariotMessage(t *testing.T, operation chariotClientApi.Operation, so soapi.SyncedObject, req chariot.Request) {
   456  	var dir string
   457  	if so.Spec.Directory != nil {
   458  		dir = *so.Spec.Directory
   459  	}
   460  
   461  	require.Equal(t, operation.String(), req.Operation)
   462  	require.Equal(t, so.Spec.Banner, req.Banner)
   463  	require.Equal(t, so.Spec.Cluster, req.Cluster)
   464  	require.Equal(t, so.Spec.Object, base64.StdEncoding.EncodeToString(req.Objects[0]))
   465  	require.Equal(t, dir, req.Dir)
   466  	require.Equal(t, syncedobject.SyncedObjectChariotOwner, req.Owner)
   467  }
   468  
   469  func checkSpec(expected, actual soapi.SyncedObjectSpec) bool {
   470  	switch {
   471  	case expected.Directory != nil && actual.Directory == nil:
   472  		return false
   473  	case expected.Directory == nil && actual.Directory != nil:
   474  		return false
   475  	case expected.Directory != nil && actual.Directory != nil && *expected.Directory != *actual.Directory:
   476  		return false
   477  	case expected.ExpireAt != nil && actual.ExpireAt == nil:
   478  		return false
   479  	case expected.ExpireAt == nil && actual.ExpireAt != nil:
   480  		return false
   481  	case expected.ExpireAt != nil && actual.ExpireAt != nil && !expected.ExpireAt.Equal(actual.ExpireAt):
   482  		return false
   483  	case expected.Banner != actual.Banner:
   484  		return false
   485  	case expected.Cluster != actual.Cluster:
   486  		return false
   487  	case expected.Object != actual.Object:
   488  		return false
   489  	}
   490  	return true
   491  }
   492  
   493  func waitForStatusToUpdate(ctx f2.Context, t *testing.T, expected *soapi.SyncedObject) {
   494  	require.Eventually(t, func() bool {
   495  		var actual soapi.SyncedObject
   496  		var err = ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
   497  			Name:      expected.ObjectMeta.Name,
   498  			Namespace: expected.ObjectMeta.Namespace,
   499  		}, &actual)
   500  		if err != nil {
   501  			return false
   502  		} else if !conditions.IsReady(&actual) {
   503  			return false
   504  		} else if !controllerutil.ContainsFinalizer(&actual, soapi.Finalizer) {
   505  			return false
   506  		}
   507  
   508  		var expectedDir, actualStatusDir string
   509  		if expected.Spec.Directory != nil {
   510  			expectedDir = *expected.Spec.Directory
   511  		}
   512  		if actual.Status.Directory != nil {
   513  			actualStatusDir = *actual.Status.Directory
   514  		}
   515  
   516  		chariotID, err := syncedobject.CreateChariotID(expected.Spec.Object)
   517  		require.NoError(t, err)
   518  
   519  		switch {
   520  		case err != nil:
   521  			return false
   522  		case actual.Status.Banner != expected.Spec.Banner:
   523  			return false
   524  		case actual.Status.Cluster != expected.Spec.Cluster:
   525  			return false
   526  		case actual.Status.StoredObject != expected.Spec.Object:
   527  			return false
   528  		case actual.Status.StorageLocation != chariot.FmtStorageLocation(expected.Spec.Banner, expected.Spec.Cluster, expectedDir, chariotID):
   529  			return false
   530  		case actualStatusDir != expectedDir:
   531  			return false
   532  		}
   533  		return checkSpec(expected.Spec, actual.Spec)
   534  	}, timeout, tick, "SyncedObject never became Ready")
   535  }
   536  
   537  func updateSyncedObject(ctx f2.Context, t *testing.T, so soapi.SyncedObject) (before, after soapi.SyncedObject) {
   538  	var currentSo soapi.SyncedObject
   539  	require.NoError(t, ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
   540  		Name:      so.ObjectMeta.Name,
   541  		Namespace: so.ObjectMeta.Namespace,
   542  	}, &currentSo))
   543  
   544  	var updateSo = currentSo
   545  	updateSo.Spec = so.Spec // only update the spec.
   546  	require.NoError(t, ktest.FromContextT(ctx, t).Client.Update(ctx, &updateSo))
   547  
   548  	var dir string
   549  	if so.Spec.Directory != nil {
   550  		dir = *so.Spec.Directory
   551  	}
   552  
   553  	var expectedSo = updateSo
   554  	expectedChariotID, err := syncedobject.CreateChariotID(so.Spec.Object)
   555  	expectedSo.Status = soapi.SyncedObjectStatus{
   556  		StoredObject:    so.Spec.Object,
   557  		StorageLocation: chariot.FmtStorageLocation(so.Spec.Banner, so.Spec.Cluster, dir, expectedChariotID),
   558  		Banner:          so.Spec.Banner,
   559  		Cluster:         so.Spec.Cluster,
   560  		Directory:       so.Spec.Directory,
   561  	}
   562  	require.NoError(t, err)
   563  
   564  	waitForStatusToUpdate(ctx, t, &expectedSo)
   565  
   566  	var afterSo soapi.SyncedObject
   567  	require.NoError(t, ktest.FromContextT(ctx, t).Client.Get(ctx, types.NamespacedName{
   568  		Name:      so.ObjectMeta.Name,
   569  		Namespace: so.ObjectMeta.Namespace,
   570  	}, &afterSo))
   571  
   572  	return currentSo, afterSo
   573  }
   574  
   575  func updateAndValidateChariotMessages(ctx f2.Context, t *testing.T, reqs chan chariot.Request, so soapi.SyncedObject) {
   576  	before, after := updateSyncedObject(ctx, t, so)
   577  
   578  	select {
   579  	case <-time.After(timeout):
   580  		t.Fatal("timed out getting delete message for outdated object on update")
   581  	case req := <-reqs:
   582  		validateChariotMessage(t, chariotClientApi.Delete, before, req)
   583  	}
   584  
   585  	select {
   586  	case <-time.After(timeout):
   587  		t.Fatal("timed out getting create message for new object on update")
   588  	case req := <-reqs:
   589  		validateChariotMessage(t, chariotClientApi.Create, after, req)
   590  	}
   591  }
   592  
   593  func randomSyncedObject() soapi.SyncedObject {
   594  	var dir *string
   595  
   596  	//nolint:gosec // not doing cryptography
   597  	if rand.Int63()%2 == 0 {
   598  		var d = uuid.New().String()
   599  		dir = &d
   600  	}
   601  
   602  	var bannerBytes [8]byte
   603  	crypto.Read(bannerBytes[:]) //nolint:errcheck
   604  
   605  	return soapi.SyncedObject{
   606  		ObjectMeta: metav1.ObjectMeta{
   607  			Name:      uuid.New().String(),
   608  			Namespace: "default",
   609  		},
   610  		Spec: soapi.SyncedObjectSpec{
   611  			Banner:    fmt.Sprintf("ret-edge-%x", bannerBytes),
   612  			Cluster:   uuid.New().String(),
   613  			Object:    randomBase64EncodedYamlObject(),
   614  			Directory: dir,
   615  		},
   616  	}
   617  }
   618  
   619  type FakeSpec struct {
   620  	Foo string `yaml:"foo"`
   621  	Bar string `yaml:"bar"`
   622  	Baz string `yaml:"baz"`
   623  }
   624  
   625  type FakeResource struct {
   626  	APIVersion string            `yaml:"apiVersion"`
   627  	Kind       string            `yaml:"kind"`
   628  	Metadata   metav1.ObjectMeta `yaml:"metadata"`
   629  	Spec       FakeSpec          `yaml:"spec"`
   630  }
   631  
   632  func (fr *FakeResource) EncodeBase64() string {
   633  	b, _ := yaml.Marshal(fr) //nolint:errcheck
   634  	return base64.StdEncoding.EncodeToString(b)
   635  }
   636  
   637  func DecodeFakeResourceBase64(object string) (*FakeResource, error) {
   638  	var fr FakeResource
   639  	y, _ := base64.StdEncoding.DecodeString(object) //nolint:errcheck
   640  	if err := yaml.Unmarshal(y, &fr); err != nil {
   641  		return nil, err
   642  	}
   643  	return &fr, nil
   644  }
   645  
   646  // randomBase64EncodedYamlObject creates an object with GVKNN data (and some labels).
   647  func randomBase64EncodedYamlObject() string {
   648  	// Group/Version
   649  	var groupVersion = "clusterregistry.k8s.io/v1alpha1"
   650  
   651  	// Kind
   652  	var kinds = [3]string{"Cluster", "Namespace", "Tenant"}
   653  	var kind = kinds[rand.Int()%len(kinds)] //nolint:gosec // not doing cryptography
   654  
   655  	// Name
   656  	var nameBytes [16]byte
   657  	//nolint:errcheck // not doing cryptography
   658  	crypto.Read(nameBytes[:]) //nolint:errcheck
   659  	var name = fmt.Sprintf("%x", nameBytes)
   660  
   661  	// Namespace
   662  	var namespace string
   663  	if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no namespace
   664  		var namespaces = [5]string{"bulldozer", "ci", "godoc", "jack-bot", "policy-bot"}
   665  		namespace = namespaces[rand.Int()%len(namespaces)] //nolint:gosec // not doing cryptography
   666  	}
   667  
   668  	// Label
   669  	var labels map[string]string
   670  	if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no labels
   671  		var clusterNameBytes [4]byte
   672  		var clusterTypeBytes [4]byte
   673  		var fleetNameBytes [4]byte
   674  		crypto.Read(clusterNameBytes[:]) //nolint:errcheck
   675  		crypto.Read(clusterTypeBytes[:]) //nolint:errcheck
   676  		crypto.Read(fleetNameBytes[:])   //nolint:errcheck
   677  		labels = map[string]string{
   678  			"cluster.edge.ncr.com":      fmt.Sprintf("%x", clusterNameBytes),
   679  			"cluster.edge.ncr.com/type": fmt.Sprintf("%x", clusterTypeBytes),
   680  			"fleet.edge.ncr.com":        fmt.Sprintf("%x", fleetNameBytes),
   681  		}
   682  	}
   683  
   684  	// Spec
   685  	var specBytes [64]byte
   686  	crypto.Read(specBytes[:]) //nolint:errcheck
   687  
   688  	// Build the Yaml object
   689  	var resource = FakeResource{
   690  		APIVersion: groupVersion,
   691  		Kind:       kind,
   692  		Metadata: metav1.ObjectMeta{
   693  			Name:      name,
   694  			Namespace: namespace,
   695  			Labels:    labels,
   696  		},
   697  		Spec: FakeSpec{
   698  			Foo: fmt.Sprintf("%x", specBytes[:16]),
   699  			Bar: fmt.Sprintf("%x", specBytes[16:32]),
   700  			Baz: fmt.Sprintf("%x", specBytes[32:64]),
   701  		},
   702  	}
   703  
   704  	fmt.Println("synced object base64:", resource.EncodeBase64())
   705  	return resource.EncodeBase64()
   706  }
   707  

View as plain text